<script lang="ts" setup>
|
import type { PendingTask } from '../../types';
|
|
import { computed, ref } from 'vue';
|
|
import { Tag } from 'ant-design-vue';
|
|
defineOptions({ name: 'WorkbenchTaskListPanel' });
|
|
const props = defineProps<{
|
list: PendingTask[];
|
selectedId?: number;
|
loading?: boolean;
|
}>();
|
|
const emit = defineEmits<{
|
select: [task: PendingTask];
|
}>();
|
|
const searchKeyword = ref('');
|
|
/** 先按工作站工序过滤,再按搜索关键字匹配 */
|
const filteredList = computed(() => {
|
const kw = searchKeyword.value.trim().toLowerCase();
|
if (!kw) return props.list;
|
return props.list.filter((row) => {
|
const fields = [
|
row.code,
|
row.workOrderCode,
|
row.workOrderName,
|
row.itemCode,
|
row.itemName,
|
row.processName,
|
];
|
return fields.some((f) => String(f ?? '').toLowerCase().includes(kw));
|
});
|
});
|
|
function getProgressColor(rate?: number): string {
|
if (rate === undefined || rate === null) return '#1890ff';
|
if (rate >= 100) return '#52c41a';
|
if (rate >= 60) return '#1890ff';
|
return '#faad14';
|
}
|
</script>
|
|
<template>
|
<section class="task-list-panel">
|
<div class="task-list-panel__head">
|
<span class="task-list-panel__title">工单列表</span>
|
<Tag color="blue">{{ list.length }}</Tag>
|
</div>
|
|
<div class="task-list-panel__search">
|
<input
|
v-model="searchKeyword"
|
class="task-list-panel__input"
|
placeholder="订单/产品/工序"
|
/>
|
</div>
|
|
<div v-if="loading" class="task-list-panel__empty">加载中...</div>
|
<div v-else-if="!list.length" class="task-list-panel__empty">
|
暂无待报工任务
|
</div>
|
<div v-else-if="!filteredList.length" class="task-list-panel__empty">
|
无匹配工单
|
</div>
|
|
<div v-else class="task-list-panel__scroll">
|
<div
|
v-for="item in filteredList"
|
:key="item.id"
|
class="task-card"
|
:class="{ 'task-card--active': item.id === selectedId }"
|
@click="emit('select', item)"
|
>
|
<div class="task-card__accent" />
|
<div class="task-card__inner">
|
<div class="task-card__header">
|
<span class="task-card__code">{{ item.code }}</span>
|
<Tag
|
v-if="item.checkFlag"
|
color="orange"
|
class="task-card__check-tag"
|
>
|
质检
|
</Tag>
|
<span
|
class="task-card__rate"
|
:style="{ color: getProgressColor(item.feedbackRate) }"
|
>
|
{{ item.feedbackRate ?? 0 }}%
|
</span>
|
</div>
|
|
<div class="task-card__progress-row">
|
<span class="task-card__progress-label">报工进度</span>
|
<span class="task-card__progress-qty">
|
{{ item.producedQuantity ?? 0 }} / {{ item.quantity ?? 0 }}
|
</span>
|
</div>
|
<div class="task-card__progress-bar">
|
<div
|
class="task-card__progress-fill"
|
:style="{
|
width: `${Math.min(item.feedbackRate ?? 0, 100)}%`,
|
backgroundColor: getProgressColor(item.feedbackRate),
|
}"
|
/>
|
</div>
|
|
<div class="task-card__info">
|
<div class="task-card__info-row">
|
<span class="task-card__info-label">订单</span>
|
<span class="task-card__info-value">
|
{{ item.workOrderCode }} {{ item.workOrderName }}
|
</span>
|
</div>
|
<div class="task-card__info-row">
|
<span class="task-card__info-label">工序</span>
|
<span class="task-card__info-value">{{ item.processName }}</span>
|
</div>
|
<div class="task-card__info-row">
|
<span class="task-card__info-label">产品</span>
|
<span class="task-card__info-value" :title="item.itemName">
|
{{ item.itemCode }} {{ item.itemName }}
|
</span>
|
</div>
|
<div class="task-card__info-row">
|
<span class="task-card__info-label">可报工</span>
|
<span class="task-card__info-value task-card__info-value--highlight">
|
{{ item.reportableQuantity ?? 0 }} {{ item.unitMeasureName }}
|
</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</section>
|
</template>
|
|
<style lang="scss" scoped>
|
.task-list-panel {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
background: #fff;
|
border-radius: 8px;
|
border: 1px solid #f0f0f0;
|
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
|
overflow: hidden;
|
}
|
|
.task-list-panel__head {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
padding: 12px 14px;
|
flex-shrink: 0;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.task-list-panel__title {
|
font-size: 16px;
|
font-weight: 600;
|
color: #1a1a1a;
|
}
|
|
.task-list-panel__search {
|
flex-shrink: 0;
|
padding: 10px 14px;
|
border-bottom: 1px solid #f0f0f0;
|
}
|
|
.task-list-panel__input {
|
width: 100%;
|
height: 36px;
|
padding: 0 12px;
|
border: 1px solid #d9d9d9;
|
border-radius: 6px;
|
font-size: 14px;
|
outline: none;
|
transition: border-color 0.2s;
|
|
&:focus {
|
border-color: var(--ant-primary-color, #1890ff);
|
box-shadow: 0 0 0 2px rgb(24 144 255 / 10%);
|
}
|
|
&::placeholder {
|
color: #bfbfbf;
|
}
|
}
|
|
.task-list-panel__empty {
|
padding: 32px 0;
|
text-align: center;
|
color: #8c8c8c;
|
font-size: 14px;
|
}
|
|
.task-list-panel__scroll {
|
flex: 1;
|
min-height: 0;
|
overflow-y: auto;
|
padding: 10px;
|
}
|
|
.task-card {
|
position: relative;
|
margin-bottom: 10px;
|
border-radius: 8px;
|
cursor: pointer;
|
border: 1px solid #f0f0f0;
|
background: #fff;
|
transition: all 0.15s ease;
|
|
&:last-child {
|
margin-bottom: 0;
|
}
|
|
&:hover {
|
border-color: var(--ant-primary-color, #1890ff);
|
}
|
|
&--active {
|
border-color: var(--ant-primary-color, #1890ff);
|
box-shadow: 0 1px 8px rgb(24 144 255 / 15%);
|
|
.task-card__accent {
|
opacity: 1;
|
}
|
|
.task-card__inner {
|
background: #f0f7ff;
|
}
|
}
|
}
|
|
.task-card__accent {
|
position: absolute;
|
left: 0;
|
top: 0;
|
bottom: 0;
|
width: 4px;
|
background: var(--ant-primary-color, #1890ff);
|
opacity: 0;
|
border-radius: 8px 0 0 8px;
|
}
|
|
.task-card__inner {
|
padding: 12px 14px 12px 16px;
|
}
|
|
.task-card__header {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 8px;
|
}
|
|
.task-card__code {
|
flex: 1;
|
min-width: 0;
|
font-weight: 600;
|
font-size: 14px;
|
color: var(--ant-primary-color, #1890ff);
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
.task-card__check-tag {
|
flex-shrink: 0;
|
}
|
|
.task-card__rate {
|
flex-shrink: 0;
|
font-size: 16px;
|
font-weight: 700;
|
font-variant-numeric: tabular-nums;
|
}
|
|
.task-card__progress-row {
|
display: flex;
|
align-items: baseline;
|
justify-content: space-between;
|
gap: 8px;
|
margin-bottom: 6px;
|
}
|
|
.task-card__progress-label {
|
font-size: 12px;
|
font-weight: 600;
|
color: #8c8c8c;
|
}
|
|
.task-card__progress-qty {
|
font-size: 13px;
|
font-weight: 600;
|
font-variant-numeric: tabular-nums;
|
color: #1a1a1a;
|
}
|
|
.task-card__progress-bar {
|
height: 8px;
|
background: #f0f0f0;
|
border-radius: 4px;
|
overflow: hidden;
|
margin-bottom: 10px;
|
}
|
|
.task-card__progress-fill {
|
height: 100%;
|
border-radius: 4px;
|
transition: width 0.3s ease;
|
}
|
|
.task-card__info {
|
display: flex;
|
flex-direction: column;
|
gap: 4px;
|
}
|
|
.task-card__info-row {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
font-size: 13px;
|
line-height: 1.4;
|
}
|
|
.task-card__info-label {
|
flex-shrink: 0;
|
width: 48px;
|
color: #8c8c8c;
|
}
|
|
.task-card__info-value {
|
flex: 1;
|
min-width: 0;
|
color: #1a1a1a;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
|
&--highlight {
|
color: var(--ant-primary-color, #1890ff);
|
font-weight: 600;
|
}
|
}
|
</style>
|