feat: 客户签订合同展示、质检合格率卡片、生产订单选择默认状态修正
- CRM 客户列表/详情:新增已签订合同数量与金额,重点客户(A)排前
- MES 首页/分析台 KPI 卡片:新增来料/过程/出货质检合格率
- BI 大屏调整
- 批次追溯:移除「向前追溯」「采购来源」页签,默认向后追溯
- 生产订单选择:IPQC 及 14 处选择器移除默认「已确认」状态过滤,避免工单均为已完成而搜不到订单
Co-Authored-By: Claude Code <noreply@anthropic.com>
| | |
| | | ownerUserDept?: string; // 负责人的部门名称 |
| | | ownerUserDeptName?: string; // 负责人的部门名称 |
| | | lockStatus?: boolean; |
| | | dealStatus?: boolean; |
| | | autoPoolEnabled?: boolean; // 是否流入公海,关闭时取消公海可见度 |
| | | mobile: string; // 手机号 |
| | | telephone: string; // 电话 |
| | |
| | | createTime: Date; // 创建时间 |
| | | updateTime: Date; // 更新时间 |
| | | poolDay?: number; // 距离进入公海天数 |
| | | contractCount?: number; // 已签订合同数量(审批状态为审核通过),没有则为 0 |
| | | contractTotalPrice?: null | number; // 已签订合同金额合计(审核通过),没有则为 null |
| | | } |
| | | |
| | | /** 客户导入请求 */ |
| | |
| | | /** 客户放入公海 */ |
| | | export function putCustomerPool(id: number) { |
| | | return requestClient.put(`/crm/customer/put-pool?id=${id}`); |
| | | } |
| | | |
| | | /** 更新客户的成交状态 */ |
| | | export function updateCustomerDealStatus(id: number, dealStatus: boolean) { |
| | | return requestClient.put( |
| | | `/crm/customer/update-deal-status?id=${id}&dealStatus=${dealStatus}`, |
| | | ); |
| | | } |
| | | |
| | | /** 进入公海客户提醒的客户列表 */ |
| | |
| | | yesterdayOutput: number; // 昨日产量 |
| | | todayQualifiedQuantity: number; // 今日合格品数 |
| | | todayUnqualifiedQuantity: number; // 今日不良品数 |
| | | iqcQualifiedRate: null | number; // 来料检验合格率(%),无检验数据时为 null |
| | | ipqcQualifiedRate: null | number; // 过程检验合格率(%),无检验数据时为 null |
| | | oqcQualifiedRate: null | number; // 出货检验合格率(%),无检验数据时为 null |
| | | iqcCheckQuantity: number; // 来料检验的检验数量合计(合格率分母),无数据时为 0 |
| | | ipqcCheckQuantity: number; // 过程检验的检验数量合计(合格率分母),无数据时为 0 |
| | | oqcCheckQuantity: number; // 出货检验的检验数量合计(合格率分母),无数据时为 0 |
| | | machineryTotal: number; // 设备总数 |
| | | machineryProducing: number; // 生产中设备数 |
| | | machineryStop: number; // 停机设备数 |
| | |
| | | loading.value = false; |
| | | await nextTick(); |
| | | resizeAllCharts(); |
| | | if (!background && dashboardCode.value === 'purchase-sales') { |
| | | if (!background) { |
| | | const intervals = charts.value.map((chart) => chart.refreshInterval).filter((interval) => interval > 0); |
| | | if (intervals.length) dataRefreshTimer = setInterval(() => { |
| | | if (!loading.value && document.visibilityState === 'visible') loadData(true); |
| | |
| | | return val.slice(0, 10); |
| | | } |
| | | return val; |
| | | } |
| | | |
| | | // 大屏表格「实时滚动」:数据复制一份,配合 CSS marquee 做无缝纵向滚动 |
| | | function duplicateRows(rows?: Record<string, unknown>[]): Record<string, unknown>[] { |
| | | return rows ? [...rows, ...rows] : []; |
| | | } |
| | | |
| | | // ======== 看板主题色 ======== |
| | |
| | | >{{ key }}</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody> |
| | | <tbody class="marquee-tbody"> |
| | | <tr |
| | | v-for="(row, ri) in chart.data" |
| | | v-for="(row, ri) in duplicateRows(chart.data)" |
| | | :key="ri" |
| | | :style="{ animationDelay: `${ri * 50}ms` }" |
| | | > |
| | |
| | | } |
| | | |
| | | .table-wrap { |
| | | overflow-x: auto; border-radius: 8px; |
| | | overflow-x: auto; |
| | | overflow-y: hidden; |
| | | max-height: 320px; |
| | | border-radius: 8px; |
| | | background: rgba(4, 14, 36, 0.55); |
| | | backdrop-filter: blur(6px); |
| | | -webkit-backdrop-filter: blur(6px); |
| | |
| | | |
| | | .data-table { |
| | | width: 100%; border-collapse: collapse; font-size: 12px; |
| | | } |
| | | |
| | | .data-table thead { |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .data-table thead th { |
| | |
| | | to { opacity: 1; transform: translateY(0); } |
| | | } |
| | | |
| | | /* 大屏表格「实时滚动」:tbody 复制一份后 translateY(-50%) 无缝循环 */ |
| | | .marquee-tbody { |
| | | animation: table-marquee 15s linear infinite; |
| | | } |
| | | .marquee-tbody:hover { |
| | | animation-play-state: paused; |
| | | } |
| | | @keyframes table-marquee { |
| | | from { transform: translateY(0); } |
| | | to { transform: translateY(-50%); } |
| | | } |
| | | |
| | | /* ======== 空状态 ======== */ |
| | | .empty-state { |
| | | position: relative; z-index: 1; |
| | |
| | | }, |
| | | }, |
| | | { |
| | | field: 'dealStatus', |
| | | title: '成交状态', |
| | | minWidth: 120, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | field: 'contractCount', |
| | | title: '签订合同', |
| | | minWidth: 170, |
| | | slots: { default: 'contracts' }, |
| | | }, |
| | | { |
| | | field: 'contactLastTime', |
| | |
| | | render: (val) => (val === false ? '否' : '是'), |
| | | }, |
| | | { |
| | | field: 'dealStatus', |
| | | label: '成交状态', |
| | | render: (val) => (val ? '已成交' : '未成交'), |
| | | }, |
| | | { |
| | | field: 'ownerUserName', |
| | | label: '负责人', |
| | | }, |
| | |
| | | lockCustomer, |
| | | putCustomerPool, |
| | | receiveCustomer, |
| | | updateCustomerDealStatus, |
| | | } from '#/api/crm/customer'; |
| | | import { getOperateLogPage } from '#/api/crm/operateLog'; |
| | | import { BizTypeEnum } from '#/api/crm/permission'; |
| | |
| | | // return true; |
| | | // } |
| | | |
| | | /** 更新成交状态操作 */ |
| | | async function handleUpdateDealStatus(): Promise<boolean | undefined> { |
| | | const dealStatus = !customer.value.dealStatus; |
| | | try { |
| | | await confirm( |
| | | `确定更新成交状态为【${dealStatus ? '已成交' : '未成交'}】吗?`, |
| | | ); |
| | | } catch { |
| | | return false; |
| | | } |
| | | // 更新成交状态 |
| | | await updateCustomerDealStatus(customerId.value, dealStatus); |
| | | // 提示并返回成功 |
| | | message.success('更新成交状态成功'); |
| | | return true; |
| | | } |
| | | |
| | | /** 加载数据 */ |
| | | onMounted(() => { |
| | | customerId.value = Number(route.params.id); |
| | |
| | | type: 'primary', |
| | | ifShow: permissionListRef?.validateOwnerUser, |
| | | onClick: handleTransfer, |
| | | }, |
| | | { |
| | | label: '更改成交状态', |
| | | type: 'default', |
| | | ifShow: permissionListRef?.validateWrite, |
| | | onClick: handleUpdateDealStatus, |
| | | }, |
| | | { |
| | | label: '锁定', |
| | |
| | | import { useRouter } from 'vue-router'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { downloadFileFromBlobPart, erpNumberFormatter } from '@vben/utils'; |
| | | |
| | | import { Button, message, Tabs } from 'ant-design-vue'; |
| | | |
| | |
| | | {{ row.name }} |
| | | </Button> |
| | | </template> |
| | | <template #contracts="{ row }"> |
| | | <template v-if="row.contractCount"> |
| | | {{ row.contractCount }} 份 · ¥{{ erpNumberFormatter(row.contractTotalPrice, 2) }} |
| | | </template> |
| | | <template v-else>—</template> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | title: '成交状态', |
| | | field: 'dealStatus', |
| | | minWidth: 80, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING }, |
| | | }, |
| | | }, |
| | | { |
| | | title: '最后跟进时间', |
| | | field: 'contactLastTime', |
| | | minWidth: 180, |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | const qualityRate = computed(() => { |
| | | const total = |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity; |
| | | return total === 0 ? 0 : (props.summary.todayQualifiedQuantity / total) * 100; |
| | | }); |
| | | /** |
| | | * 质量合格率:来料 / 过程 / 出货三个检验环节,各自一张卡片 |
| | | * |
| | | * 由后端按「合格数量 / 检验数量」计算(不过滤单据状态,无时间限制), |
| | | * 该环节没有检验数据时后端返回 null。 |
| | | */ |
| | | const qualityRates = computed(() => [ |
| | | { |
| | | key: 'quality-iqc', |
| | | label: '来料合格率', |
| | | rate: props.summary.iqcQualifiedRate, |
| | | checkQuantity: props.summary.iqcCheckQuantity, |
| | | routeName: 'MesQcIqc', |
| | | icon: 'lucide:package-check', |
| | | iconClass: 'bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25', |
| | | textClass: 'text-orange-600 dark:text-orange-400', |
| | | washClass: 'bg-gradient-to-br from-orange-500/5 to-amber-500/5', |
| | | glowClass: 'bg-orange-400/10', |
| | | }, |
| | | { |
| | | key: 'quality-ipqc', |
| | | label: '过程合格率', |
| | | rate: props.summary.ipqcQualifiedRate, |
| | | checkQuantity: props.summary.ipqcCheckQuantity, |
| | | routeName: 'MesQcIpqc', |
| | | icon: 'lucide:workflow', |
| | | iconClass: 'bg-gradient-to-br from-cyan-500 to-teal-400 shadow-lg shadow-cyan-500/25', |
| | | textClass: 'text-cyan-600 dark:text-cyan-400', |
| | | washClass: 'bg-gradient-to-br from-cyan-500/5 to-teal-500/5', |
| | | glowClass: 'bg-cyan-400/10', |
| | | }, |
| | | { |
| | | key: 'quality-oqc', |
| | | label: '出货合格率', |
| | | rate: props.summary.oqcQualifiedRate, |
| | | checkQuantity: props.summary.oqcCheckQuantity, |
| | | routeName: 'MesQcOqc', |
| | | icon: 'lucide:truck', |
| | | iconClass: 'bg-gradient-to-br from-pink-500 to-rose-400 shadow-lg shadow-pink-500/25', |
| | | textClass: 'text-pink-600 dark:text-pink-400', |
| | | washClass: 'bg-gradient-to-br from-pink-500/5 to-rose-500/5', |
| | | glowClass: 'bg-pink-400/10', |
| | | }, |
| | | ]); |
| | | |
| | | /** 合格率展示文案(一位小数,与原「质量合格率」卡片一致),无数据时用「—」而不是 0% */ |
| | | function formatRate(rate: null | number | undefined) { |
| | | return rate === null || rate === undefined ? '—' : rate.toFixed(1); |
| | | } |
| | | |
| | | /** 无检验数据时不渲染百分号,避免出现「—%」 */ |
| | | function hasRate(rate: null | number | undefined) { |
| | | return rate !== null && rate !== undefined; |
| | | } |
| | | |
| | | const equipmentRate = computed(() => { |
| | | if (props.summary.machineryTotal === 0) return 0; |
| | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <!-- 生产订单 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesProWorkOrder') }" |
| | | :style="tiltStyle('order')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'order')" |
| | |
| | | </Col> |
| | | |
| | | <!-- 今日产量 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesProFeedback') }" |
| | | :style="tiltStyle('output')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'output')" |
| | |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- 质量合格率 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <!-- 质量合格率:来料 / 过程 / 出货,各一张卡片 --> |
| | | <Col |
| | | v-for="item in qualityRates" |
| | | :key="item.key" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="4" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesQcRecord') }" |
| | | :style="tiltStyle('quality')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'quality')" |
| | | @mouseleave="onMouseLeave('quality')" |
| | | @click="canNavigate('MesQcRecord') && emit('navigate', 'MesQcRecord')" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate(item.routeName) }" |
| | | :style="tiltStyle(item.key)" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, item.key)" |
| | | @mouseleave="onMouseLeave(item.key)" |
| | | @click="canNavigate(item.routeName) && emit('navigate', item.routeName)" |
| | | > |
| | | <div class="relative z-10 flex items-center gap-4 p-5"> |
| | | <div class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-orange-500 to-amber-400 shadow-lg shadow-orange-500/25 transition-transform duration-300 group-hover:scale-110"> |
| | | <IconifyIcon class="size-6 text-white" icon="lucide:shield-check" /> |
| | | <div |
| | | class="flex size-12 flex-shrink-0 items-center justify-center rounded-xl transition-transform duration-300 group-hover:scale-110" |
| | | :class="item.iconClass" |
| | | > |
| | | <IconifyIcon class="size-6 text-white" :icon="item.icon" /> |
| | | </div> |
| | | <div class="min-w-0 flex-1"> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">质量合格率</div> |
| | | <div class="mb-1 text-xs font-medium text-gray-500 dark:text-gray-400">{{ item.label }}</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="text-2xl font-bold leading-tight" |
| | | :class="qualityRate >= 95 ? 'text-emerald-600 dark:text-emerald-400' : qualityRate >= 80 ? 'text-orange-600 dark:text-orange-400' : 'text-red-600 dark:text-red-400'" |
| | | :decimals="1" :end-val="qualityRate" :duration="1200" |
| | | <span class="text-2xl font-bold leading-tight" :class="item.textClass"> |
| | | {{ formatRate(item.rate) }} |
| | | </span> |
| | | <span v-if="hasRate(item.rate)" class="text-xs text-gray-400">%</span> |
| | | </div> |
| | | <div class="mt-1 flex items-center gap-1 text-xs text-gray-400"> |
| | | <span>检验 {{ item.checkQuantity }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | class="absolute inset-0 -z-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100" |
| | | :class="item.washClass" |
| | | /> |
| | | <span class="text-xs text-gray-400">%</span> |
| | | </div> |
| | | <div class="mt-1 text-xs text-gray-400"> |
| | | 合格 {{ summary.todayQualifiedQuantity }} | 不良 {{ summary.todayUnqualifiedQuantity }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="absolute inset-0 -z-0 bg-gradient-to-br from-orange-500/5 to-amber-500/5 opacity-0 transition-opacity duration-300 group-hover:opacity-100" /> |
| | | <div class="absolute -right-6 -top-6 size-24 rounded-full bg-orange-400/10 blur-xl transition-all duration-300 group-hover:scale-150" /> |
| | | <div |
| | | class="absolute -right-6 -top-6 size-24 rounded-full blur-xl transition-all duration-300 group-hover:scale-150" |
| | | :class="item.glowClass" |
| | | /> |
| | | </div> |
| | | </Col> |
| | | |
| | | <!-- 设备运行率 --> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <div |
| | | class="glass-tilt group overflow-hidden rounded-2xl border-0" |
| | | class="glass-tilt group h-full overflow-hidden rounded-2xl border-0" |
| | | :class="{ 'cursor-pointer': canNavigate('MesDvMachinery') }" |
| | | :style="tiltStyle('equip')" |
| | | @mousemove="(e: MouseEvent) => onMouseMove(e, 'equip')" |
| | |
| | | navigate: [name: string]; |
| | | }>(); |
| | | |
| | | /** 是否有质量数据(合格品 + 不良品 > 0) */ |
| | | const hasQualityData = computed( |
| | | () => |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity > |
| | | 0, |
| | | ); |
| | | /** |
| | | * 质量合格率:来料 / 过程 / 出货三个检验环节,各自一张卡片 |
| | | * |
| | | * 由后端按「合格数量 / 检验数量」计算(不过滤单据状态,无时间限制), |
| | | * 该环节没有检验数据时后端返回 null。 |
| | | */ |
| | | const qualityRates = computed(() => [ |
| | | { |
| | | label: '来料合格率', |
| | | rate: props.summary.iqcQualifiedRate, |
| | | checkQuantity: props.summary.iqcCheckQuantity, |
| | | routeName: 'MesQcIqc', |
| | | icon: 'lucide:package-check', |
| | | color: '#e6a23c', |
| | | colorLight: '#ebb563', |
| | | }, |
| | | { |
| | | label: '过程合格率', |
| | | rate: props.summary.ipqcQualifiedRate, |
| | | checkQuantity: props.summary.ipqcCheckQuantity, |
| | | routeName: 'MesQcIpqc', |
| | | icon: 'lucide:workflow', |
| | | color: '#13c2c2', |
| | | colorLight: '#5cdbd3', |
| | | }, |
| | | { |
| | | label: '出货合格率', |
| | | rate: props.summary.oqcQualifiedRate, |
| | | checkQuantity: props.summary.oqcCheckQuantity, |
| | | routeName: 'MesQcOqc', |
| | | icon: 'lucide:truck', |
| | | color: '#eb2f96', |
| | | colorLight: '#f759ab', |
| | | }, |
| | | ]); |
| | | |
| | | /** 质量合格率 = 合格品 / (合格品 + 不良品) * 100,无数据时为 0 */ |
| | | const qualityRate = computed(() => { |
| | | const total = |
| | | props.summary.todayQualifiedQuantity + |
| | | props.summary.todayUnqualifiedQuantity; |
| | | if (total === 0) { |
| | | return 0; |
| | | /** 合格率展示文案(一位小数,与原「质量合格率」卡片一致),无数据时用「—」而不是 0% */ |
| | | function formatRate(rate: null | number | undefined) { |
| | | return rate === null || rate === undefined ? '—' : rate.toFixed(1); |
| | | } |
| | | return (props.summary.todayQualifiedQuantity / total) * 100; |
| | | }); |
| | | |
| | | /** 无检验数据时不渲染百分号,避免出现「—%」 */ |
| | | function hasRate(rate: null | number | undefined) { |
| | | return rate !== null && rate !== undefined; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Row :gutter="16"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProWorkOrder')" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col |
| | | v-for="item in qualityRates" |
| | | :key="item.label" |
| | | :lg="6" |
| | | :md="12" |
| | | :sm="12" |
| | | :xl="4" |
| | | :xs="24" |
| | | class="mb-4" |
| | | > |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesProFeedback')" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', item.routeName)" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | | <div |
| | | class="flex size-14 flex-shrink-0 items-center justify-center rounded-xl text-white" |
| | | style="background: linear-gradient(135deg, #e6a23c, #ebb563)" |
| | | :style="{ background: `linear-gradient(135deg, ${item.color}, ${item.colorLight})` }" |
| | | > |
| | | <IconifyIcon class="size-7" icon="lucide:circle-check" /> |
| | | <IconifyIcon class="size-7" :icon="item.icon" /> |
| | | </div> |
| | | <div> |
| | | <div class="text-muted-foreground mb-1 text-sm">质量合格率</div> |
| | | <div class="text-muted-foreground mb-1 text-sm">{{ item.label }}</div> |
| | | <div class="flex items-baseline gap-1"> |
| | | <CountTo |
| | | class="text-2xl font-bold leading-tight text-[#e6a23c]" |
| | | :decimals="1" |
| | | :end-val="qualityRate" |
| | | :duration="1500" |
| | | /> |
| | | <span class="text-muted-foreground text-xs">%</span> |
| | | <span |
| | | class="text-2xl font-bold leading-tight" |
| | | :style="{ color: item.color }" |
| | | > |
| | | {{ formatRate(item.rate) }} |
| | | </span> |
| | | <span |
| | | v-if="hasRate(item.rate)" |
| | | class="text-muted-foreground text-xs" |
| | | > |
| | | % |
| | | </span> |
| | | </div> |
| | | <div class="text-muted-foreground mt-1 text-xs"> |
| | | <template v-if="hasQualityData"> |
| | | <span>合格 {{ summary.todayQualifiedQuantity }}</span> |
| | | <Divider type="vertical" /> |
| | | <span>不良 {{ summary.todayUnqualifiedQuantity }}</span> |
| | | </template> |
| | | <span v-else>暂无数据</span> |
| | | <span>检验 {{ item.checkQuantity }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </Card> |
| | | </Col> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="6" :xs="24" class="mb-4"> |
| | | <Col :lg="6" :md="12" :sm="12" :xl="4" :xs="24" class="mb-4"> |
| | | <Card |
| | | class="kpi-card cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | class="kpi-card h-full cursor-pointer transition-all hover:-translate-y-1 hover:shadow-lg" |
| | | @click="emit('navigate', 'MesDvMachinery')" |
| | | > |
| | | <div class="flex items-center gap-4"> |
| | |
| | | |
| | | import { markRaw } from 'vue'; |
| | | |
| | | import { DICT_TYPE, MesProWorkOrderStatusEnum } from '@vben/constants'; |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: '请选择订单(可选)', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | } |
| | | : { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | |
| | | componentProps: { |
| | | disabled: headerReadonly, |
| | | placeholder: '请选择生产订单', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProTaskStatusEnum, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate } from '@vben/utils'; |
| | |
| | | componentProps: { |
| | | disabled: isHeaderReadonly, |
| | | placeholder: '请选择订单', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订单变更:清空任务及任务带出的产品信息、数量区域控制位 |
| | | onChange: async () => { |
| | | await formApi?.setValues({ |
| | |
| | | |
| | | import { useDetailSchema } from '../data'; |
| | | import ProcessTraceList from './process-trace-list.vue'; |
| | | import PurchaseSourceList from './purchase-source-list.vue'; |
| | | import SalesTargetList from './sales-target-list.vue'; |
| | | import TraceList from './trace-list.vue'; |
| | | |
| | | const detailData = ref<MesWmBatchApi.Batch>(); // 当前批次详情 |
| | | const subTabsName = ref< |
| | | | 'backward' |
| | | | 'forward' |
| | | | 'purchase' |
| | | | 'sales' |
| | | | 'process' |
| | | >('forward'); // 当前激活的追溯页签 |
| | | >('backward'); // 当前激活的追溯页签 |
| | | |
| | | const [Descriptions] = useDescription({ |
| | | bordered: true, |
| | |
| | | return; |
| | | } |
| | | detailData.value = modalApi.getData<MesWmBatchApi.Batch>(); |
| | | subTabsName.value = 'forward'; |
| | | subTabsName.value = 'backward'; |
| | | }, |
| | | }); |
| | | </script> |
| | |
| | | class="mx-4 mt-4" |
| | | type="card" |
| | | > |
| | | <Tabs.TabPane key="forward"> |
| | | <template #tab> |
| | | <Tooltip title="顺着生产流往下游查:该批次被哪些工单消耗后,产出成了哪些成品批次"> |
| | | <span>向前追溯</span> |
| | | </Tooltip> |
| | | </template> |
| | | <TraceList :batch-code="detailData.code" direction="forward" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="backward"> |
| | | <template #tab> |
| | | <Tooltip title="逆着生产流往上游查:生产该批次时,消耗了哪些原料批次"> |
| | |
| | | </Tooltip> |
| | | </template> |
| | | <TraceList :batch-code="detailData.code" direction="backward" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="purchase"> |
| | | <template #tab> |
| | | <Tooltip title="原料侧来源:该批次对应的采购入库单与 IQC 来料检验记录"> |
| | | <span>采购来源</span> |
| | | </Tooltip> |
| | | </template> |
| | | <PurchaseSourceList :batch-code="detailData.code" /> |
| | | </Tabs.TabPane> |
| | | <Tabs.TabPane key="sales"> |
| | | <template #tab> |
| | |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProTaskStatusEnum, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: '请选择生产订单', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订单变更时清空生产任务 |
| | | onChange: () => formApi?.setFieldValue('taskId', undefined), |
| | | }, |
| | |
| | | componentProps: (values) => ({ |
| | | disabled: !!values.sourceDocId, |
| | | placeholder: '请选择生产订单', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | onChange: () => formApi?.setFieldValue('taskId', undefined), |
| | | }), |
| | | }, |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmProductIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | |
| | | label: '生产订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmProductIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | label: '生产订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | disabled: true, |
| | | }, |
| | | }, |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmReturnIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | |
| | | label: '生产订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | }, |
| | | { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmReturnIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | |
| | | label: '生产订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | disabled: true, |
| | | }, |
| | | }, |
| | |
| | | BarcodeBizTypeEnum, |
| | | CommonStatusEnum, |
| | | DICT_TYPE, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | import { formatDate } from '@vben/utils'; |
| | |
| | | label: '订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | onChange: (item: any) => syncBizDetail(formApi, item), |
| | | }, |
| | | dependencies: { |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesProWorkOrderTypeEnum, |
| | | } from '@vben/constants'; |
| | | |
| | |
| | | onChange: async (workOrder?: MesProWorkOrderApi.WorkOrder) => { |
| | | await formApi?.setFieldValue('vendorId', workOrder?.vendorId); |
| | | }, |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | type: MesProWorkOrderTypeEnum.OUTSOURCE, |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesProWorkOrderTypeEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | |
| | | onChange: async (workOrder?: MesProWorkOrderApi.WorkOrder) => { |
| | | await formApi?.setFieldValue('vendorId', workOrder?.vendorId); |
| | | }, |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | type: MesProWorkOrderTypeEnum.OUTSOURCE, |
| | | }, |
| | | rules: 'selectRequired', |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: '请选择生产订单', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | placeholder: '请选择生产订单', |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | // 订单变更:清空已选生产任务,避免任务与订单不一致 |
| | | onChange: async () => { |
| | | await formApi?.setFieldValue('taskId', undefined); |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | |
| | | label: '生产订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | checkMethod: (row: any) => row.productionStatus !== 0, |
| | | }, |
| | | }, |
| | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | |
| | | label: '生产订单', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | |
| | | proxy: { |
| | | "/admin-api": { |
| | | changeOrigin: true, |
| | | target: "http://localhost:48080", |
| | | target: "http://localhost:48081", |
| | | ws: true, |
| | | }, |
| | | }, |