This commit is contained in:
zlwu committed 2026-05-28 08:37:37 +08:00
1 parent 2020f371f4
commit f1f5d24eff
31 files changed
+13600 -13813

No files matched your search

+2 -2
View File
@@ -378,7 +378,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'qc-work-order',
component: '/quality-inspection/qc-work-order/index',
meta: {
title: '质检管理',
title: '质检工单',
keepAlive: true,
menuCode: 'biz:qcWorkOrder:list'
}
@@ -451,7 +451,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'production-work-order',
component: '/production/production-work-order/index',
meta: {
title: '生产管理',
title: '生产工单',
keepAlive: true,
menuCode: 'production:work-order:list'
}
@@ -33,53 +33,122 @@
</template>
<template #outbound-material>
<div class="view-section-slot outbound-material-view">
<div v-if="!hasOutboundMaterial" class="outbound-material-empty">暂无出库物料准备记录</div>
<div class="outbound-material-view">
<p v-if="!hasOutboundMaterial" class="view-empty-hint">暂无出库物料准备记录</p>
<template v-else>
<div v-if="outboundMaterialCells.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">出库细胞</h4>
<el-table :data="outboundMaterialCells" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="productCode" label="产品编码" min-width="150" show-overflow-tooltip />
<el-table-column prop="productName" label="产品名称" min-width="120" show-overflow-tooltip />
<el-table-column prop="productTypeName" label="类型" min-width="90" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column prop="storageSlotName" label="储物格" min-width="130" show-overflow-tooltip />
<el-table-column label="状态" min-width="90" align="center">
<template #default="{ row }">
<el-tag
v-if="row.productStatus"
:type="cellProductStatusTagType(row.productStatus) as any"
size="small"
>
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
<div class="section-subtitle">出库细胞</div>
<div class="view-subtable-wrap">
<el-table
:data="outboundMaterialCells"
stripe
class="list-table view-subtable"
max-height="320"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="productCode"
label="产品编码"
min-width="150"
show-overflow-tooltip
/>
<el-table-column
prop="productName"
label="产品名称"
min-width="120"
show-overflow-tooltip
/>
<el-table-column prop="productTypeName" label="类型" min-width="90" />
<el-table-column prop="specName" label="规格" min-width="90" />
<el-table-column
prop="storageSlotName"
label="储物格"
min-width="130"
show-overflow-tooltip
/>
<el-table-column label="状态" min-width="90" align="center" header-align="center">
<template #default="{ row }">
<el-tag
v-if="row.productStatus"
:type="cellProductStatusTagType(row.productStatus) as any"
size="small"
>
{{ cellProductStatusLabel(row.productStatus) }}
</el-tag>
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
<div v-if="outboundMaterialQcRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">使用的物料</h4>
<el-table :data="outboundMaterialQcRows" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="materialName" label="物料名称" min-width="140" show-overflow-tooltip />
<el-table-column prop="specModel" label="规格型号" min-width="110" />
<el-table-column label="计划用量" min-width="100" align="center">
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
</el-table-column>
<el-table-column prop="unit" label="单位" min-width="72" align="center" />
</el-table>
<div class="section-subtitle">使用的物料</div>
<div class="view-subtable-wrap">
<el-table
:data="outboundMaterialQcRows"
stripe
class="list-table view-subtable"
max-height="320"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="materialName"
label="物料名称"
min-width="140"
show-overflow-tooltip
/>
<el-table-column prop="specModel" label="规格型号" min-width="110" />
<el-table-column label="计划用量" min-width="100" align="center" header-align="center">
<template #default="{ row }">{{ row.qty ?? '—' }}</template>
</el-table-column>
<el-table-column
prop="unit"
label="单位"
min-width="72"
align="center"
header-align="center"
/>
</el-table>
</div>
</div>
<div v-if="outboundPaperRows.length" class="outbound-material-block">
<h4 class="outbound-material-subtitle">其他纸质材料</h4>
<el-table :data="outboundPaperRows" class="list-table inner-table" stripe size="small">
<el-table-column type="index" label="序号" min-width="56" align="center" />
<el-table-column prop="paperName" label="名称" min-width="140" show-overflow-tooltip />
<el-table-column label="份数" min-width="80" align="center">
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
</el-table-column>
<el-table-column label="附件" min-width="320">
<div class="section-subtitle">其他纸质材料</div>
<div class="view-subtable-wrap">
<el-table
:data="outboundPaperRows"
stripe
class="list-table view-subtable"
max-height="320"
>
<el-table-column
type="index"
label="序号"
min-width="68"
align="center"
header-align="center"
/>
<el-table-column
prop="paperName"
label="名称"
min-width="140"
show-overflow-tooltip
/>
<el-table-column label="份数" min-width="80" align="center" header-align="center">
<template #default="{ row }">{{ row.copies ?? '—' }}</template>
</el-table-column>
<el-table-column label="附件" min-width="320">
<template #default="{ row }">
<div v-if="parsePaperAttachments(row.attachmentUrl).length" class="paper-att-cards">
<div
@@ -113,7 +182,8 @@
<span v-else>—</span>
</template>
</el-table-column>
</el-table>
</el-table>
</div>
</div>
<p v-if="outboundMaterialRemark" class="outbound-material-remark">
备注:{{ outboundMaterialRemark }}
@@ -155,6 +225,7 @@
import type { ViewSection } from '@/components/business/system-view-dialog/index.vue'
import SampleQcInfoCards from '@/components/business/sample-qc-info-cards/index.vue'
import SampleProductionInfoCards from '@/components/business/sample-production-info-cards/index.vue'
import ProtocolAttachmentReadonly from '@/views/cell-storage/protocol/ProtocolAttachmentReadonly.vue'
import {
fetchOutboundReleaseReviewDetail,
type OutboundReleaseReviewDetail
@@ -487,11 +558,6 @@
width: 100%;
}
.outbound-material-empty {
font-size: 13px;
color: #94a3b8;
}
.outbound-material-block {
margin-bottom: 16px;
@@ -500,11 +566,23 @@
}
}
.outbound-material-subtitle {
margin: 0 0 8px;
.section-subtitle {
display: flex;
align-items: center;
margin: 0 0 10px;
font-size: 13px;
font-weight: 600;
color: #475569;
color: #334155;
&::before {
content: '';
width: 3px;
height: 14px;
margin-right: 8px;
flex-shrink: 0;
background: #10b981;
border-radius: 2px;
}
}
.outbound-material-remark {
@@ -381,11 +381,7 @@
/>
</el-form-item>
<el-form-item v-if="isEdit" label="状态" prop="productStatus">
<el-select
v-model="formData.productStatus"
style="width: 100%"
:disabled="formData.productStatus !== 'frozen'"
>
<el-select v-model="formData.productStatus" style="width: 100%">
<el-option
v-for="opt in editStatusOptions"
:key="opt.value"
@@ -1152,9 +1148,7 @@
productImages: formData.productImages?.trim() || undefined,
remark: formData.remark?.trim() || undefined
}
// 仅「已冻存」可变更状态;生产中/已复苏等系统态勿提交,避免后端校验失败
const editableStatuses = ['frozen', 'sold', 'used', 'voided']
if (!isEdit.value || editableStatuses.includes(formData.productStatus)) {
if (isEdit.value) {
shared.productStatus = formData.productStatus || undefined
}
if (isEdit.value && editingId.value) {
@@ -248,6 +248,8 @@
const map: Record<string, string> = {
frozen: 'success',
in_production: 'warning',
sold: 'warning',
used: 'info',
voided: 'danger',
thawed: 'info'
}
@@ -258,6 +260,8 @@
const map: Record<string, string> = {
frozen: '已冻存',
in_production: '生产中',
sold: '已售卖',
used: '已使用',
voided: '已作废',
thawed: '已复苏'
}
@@ -312,6 +312,7 @@
if (sourceType === 'task_complete') return 'primary'
if (sourceType === 'manual_in') return 'success'
if (sourceType === 'manual_out') return 'warning'
if (sourceType === 'outbound_handover') return 'primary'
return 'info'
}
@@ -319,6 +320,7 @@
if (sourceType === 'task_complete') return '任务完成'
if (sourceType === 'manual_in') return '手动入库'
if (sourceType === 'manual_out') return '手动出库'
if (sourceType === 'outbound_handover') return '出库交接'
return sourceType || '-'
}
@@ -1,4 +1,4 @@
<!-- 旧「质检任务」菜单入口:重定向至质检管理 · 质检任务 Tab -->
<!-- 旧「质检任务」菜单入口:重定向至质检工单(层级列表) -->
<template>
<div />
</template>
@@ -10,9 +10,10 @@
const route = useRoute()
onMounted(() => {
const { tab: _tab, ...rest } = route.query
router.replace({
name: 'QualityInspectionQcWorkOrder',
query: { ...route.query, tab: 'task' }
query: rest
})
})
</script>
File diff suppressed because it is too large. Load diff
@@ -3,38 +3,10 @@
<template>
<div class="page-container">
<section class="panel">
<div class="panel-entity-switch" role="tablist" aria-label="视图类型">
<button
type="button"
class="entity-switch-card"
:class="{ 'is-active': mainTab === 'work-order' }"
role="tab"
:aria-selected="mainTab === 'work-order'"
@click="mainTab = 'work-order'"
>
<span class="entity-switch-card__icon"><el-icon><Document /></el-icon></span>
<span class="entity-switch-card__main">
<span class="entity-switch-card__title">质检工单</span>
<span class="entity-switch-card__desc">工单创建、配置与跟踪</span>
</span>
</button>
<button
type="button"
class="entity-switch-card"
:class="{ 'is-active': mainTab === 'task' }"
role="tab"
:aria-selected="mainTab === 'task'"
@click="mainTab = 'task'"
>
<span class="entity-switch-card__icon"><el-icon><Tickets /></el-icon></span>
<span class="entity-switch-card__main">
<span class="entity-switch-card__title">质检任务</span>
<span class="entity-switch-card__desc">任务执行与结果录入</span>
</span>
</button>
<div class="panel-top">
<h2 class="title-bar-heading">质检工单</h2>
</div>
<div v-show="mainTab === 'work-order'" class="panel-entity-body">
<el-tabs
v-model="activeTypeTab"
class="work-order-type-tabs"
@@ -117,12 +89,29 @@
<div class="list-body list-wrap">
<el-table
:class="['list-table', { 'list-table--no-action-col list-table--all-tab': activeTypeTab === 'all' }]"
ref="qcWorkOrderListTableRef"
:class="[
'qc-work-order-table list-table',
{ 'list-table--no-action-col list-table--all-tab': activeTypeTab === 'all' }
]"
:data="tableData"
v-loading="loading"
stripe
row-key="id"
@row-dblclick="handleView"
@expand-change="handleWorkOrderRowExpand"
>
<el-table-column type="expand" min-width="48">
<template #default="{ row }">
<QcWorkOrderTaskSubList
v-if="activeTypeTab !== 'all'"
:work-order="row"
:refresh-token="taskSubListRefreshToken"
@changed="handleWorkOrderTasksChanged"
/>
<div v-else class="qc-work-order-expand-all-hint">全部工单视图请进入具体类型 Tab 查看任务</div>
</template>
</el-table-column>
<el-table-column
type="index"
label="序号"
@@ -487,6 +476,7 @@
width="900px"
class="dialog-form-layout"
align-center
append-to-body
destroy-on-close
:close-on-click-modal="false"
>
@@ -716,16 +706,14 @@
v-model:visible="outboundViewVisible"
:work-order-id="outboundViewId"
/>
</div>
<QcTaskTab v-if="mainTab === 'task'" />
</section>
</div>
</template>
<script setup lang="ts">
import { formatMoneyDisplay } from '@/utils/money-format'
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
import type { TableInstance } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
Close,
@@ -738,8 +726,7 @@
Plus,
EditPen,
Delete,
More,
Tickets
More
} from '@element-plus/icons-vue'
import { fetchQcLabTestItemsActive, type QcLabTestItem } from '@/api/qc-lab-test-item'
import { fetchQcThirdPartyItemsActive, type QcThirdPartyItem } from '@/api/qc-third-party-item'
@@ -817,12 +804,12 @@
import { downloadAttachmentFile } from '@/utils/download-attachment'
import WorkOrderRemarkDialog from '@/components/business/work-order-remark-dialog/index.vue'
import OutboundQcViewDialog from './OutboundQcViewDialog.vue'
import QcTaskTab from './QcTaskTab.vue'
import QcWorkOrderTaskSubList from './QcWorkOrderTaskSubList.vue'
defineOptions({ name: 'QualityInspectionQcWorkOrder' })
const route = useRoute()
const mainTab = ref('work-order')
const qcWorkOrderListTableRef = ref<TableInstance>()
const taskSubListRefreshToken = ref(0)
type WorkOrderTypeTab = 'inbound' | 'temporary' | 'outbound' | 'all'
const activeTypeTab = ref<WorkOrderTypeTab>('inbound')
@@ -1637,12 +1624,28 @@
await fetchConfigureQcWorkOrder(configuringId.value, { ...configureForm })
ElMessage.success(configureIsEdit.value ? '检测项目已更新' : '配置成功,已生成质检任务')
configureVisible.value = false
loadData()
taskSubListRefreshToken.value += 1
await loadData()
} finally {
configureLoading.value = false
}
}
async function layoutWorkOrderTable() {
await nextTick()
qcWorkOrderListTableRef.value?.doLayout?.()
}
async function handleWorkOrderRowExpand(_row: unknown, _expandedRows: unknown[]) {
await layoutWorkOrderTable()
}
async function handleWorkOrderTasksChanged() {
taskSubListRefreshToken.value += 1
await loadData()
await layoutWorkOrderTable()
}
const outboundViewVisible = ref(false)
const outboundViewId = ref('')
@@ -1709,96 +1712,54 @@
)
onMounted(() => {
const tab = route.query.tab
if (tab === 'task') {
mainTab.value = 'task'
}
loadData()
})
</script>
<style scoped lang="scss">
.panel-entity-switch {
display: flex;
justify-content: center;
gap: 12px;
padding: 16px 16px 12px;
.qc-work-order-table :deep(td.el-table__expanded-cell) {
padding: 0 !important;
text-align: left !important;
}
.entity-switch-card {
display: flex;
align-items: center;
gap: 12px;
min-width: 220px;
max-width: 280px;
flex: 1;
padding: 14px 18px;
.qc-work-order-table :deep(td.el-table__expanded-cell > .cell) {
display: block !important;
padding: 0 !important;
text-align: left !important;
}
.qc-work-order-table :deep(.el-table__expand-icon) {
width: 28px;
height: 28px;
border-radius: 6px;
border: 1px solid var(--mint-border, #d9e8e2);
border-radius: 10px;
background: var(--surface, #fff);
cursor: pointer;
text-align: left;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
box-shadow 0.2s ease;
&:hover:not(.is-active) {
border-color: #a7f3d0;
background: #f8fafc;
}
&.is-active {
border-color: var(--primary, #10b981);
background: var(--primary-bg, #ecfdf5);
box-shadow: 0 0 0 1px rgb(16 185 129 / 12%);
}
}
.entity-switch-card__icon {
display: flex;
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
flex-shrink: 0;
border-radius: 8px;
background: #f1f5f9;
color: #64748b;
font-size: 20px;
.entity-switch-card.is-active & {
background: #fff;
color: var(--primary-dark, #059669);
}
}
.entity-switch-card__main {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.entity-switch-card__title {
font-size: 14px;
font-weight: 600;
color: var(--text, #1e293b);
line-height: 1.4;
.entity-switch-card.is-active & {
color: var(--primary-dark, #059669);
}
}
.entity-switch-card__desc {
font-size: 12px;
transition:
background 0.15s ease,
border-color 0.15s ease;
color: var(--mint-text-muted, #64748b);
line-height: 1.4;
background: var(--mint-surface, #fff);
}
.panel-entity-body {
min-width: 0;
.qc-work-order-table :deep(.el-table__expand-icon:hover) {
background: var(--mint-primary-bg, #ecfdf5);
border-color: #10b981;
color: var(--mint-primary-dark, #059669);
}
.qc-work-order-table :deep(.el-table__expand-icon--expanded) {
background: var(--mint-primary-bg, #ecfdf5);
border-color: #10b981;
color: var(--mint-primary-dark, #059669);
}
.qc-work-order-expand-all-hint {
padding: 16px 16px 16px 40px;
font-size: 13px;
color: var(--mint-text-muted, #64748b);
background: #f8fafc;
}
/* 全部工单 Tab 无操作列,略增高行高便于阅读多列信息 */