pinia不用像Vuex那样,把同步,异步函数分开。
API 风格对比
Vuex 示例(Option 风格)
javascript
// store/index.js
export default new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount(state) {
return state.count * 2
}
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
使用
this.$store.state.count
this.$store.commit('increment')
this.$store.dispatch('incrementAsync')
问题
mutations / actions 区分不直观
字符串形式的 mutation type 易出错
TypeScript 支持差
Pinia 示例(Composition API 风格)
javascript
// stores/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function incrementAsync() {
setTimeout(() => {
count.value++
}, 1000)
}
return {
count,
doubleCount,
increment,
incrementAsync
}
})
使用
const counter = useCounterStore()
counter.count
counter.increment()
counter.incrementAsync()
优点
像写 setup() 一样写 store
无需 commit / dispatch
逻辑更自然
模块化方式对比
Vuex Modules
javascript
modules: {
user,
cart
}
问题:
命名空间复杂
嵌套模块难以维护
Pinia(天然模块化)
javascript
stores/
user.ts
cart.ts
✅ 每个 store 独立
✅ 无需 namespace
✅ 可自由组合
性能与体积
javascript
Pinia 更轻量
无 mutations 逻辑
Tree-shaking 友好
核心概念差异
Vuex
html
State
Getters
Mutations ✅
Actions
Modules
Pinia
html
State
Getters
Actions ✅
📌 关键区别:
Pinia 移除了 Mutations
所有状态变更统一在 actions 中完成
✅ 优点:
减少概念
降低心智负担
更符合组合式 API 风格
总体定位对比

使用pinia

stores/production.js
javascript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
/**
* 生产管理 Store
* 统一管理:生产工单、工序排程、设备状态 三类业务数据
* 采用 Setup Store 写法:state 用 ref,getters 用 computed,actions 用普通函数
*/
export const useProductionStore = defineStore('production', () => {
/* ===================== 常量 ===================== */
/** 工单状态流转 */
const orderStatusFlow = ['待排产', '已排产', '生产中', '已完工', '已取消']
/** 工序状态 */
const processStatusList = ['未开始', '进行中', '已完成', '已延期', '已取消']
/** 设备状态 */
const equipmentStatusList = ['运行', '停机', '维修', '保养', '故障']
/* ===================== State:生产工单 ===================== */
/**
* 生产工单列表
* 结构: {
* id, orderNo(工单号), productName(产品名称), productCode(产品编码), spec(规格型号),
* quantity(计划数量), completedQty(已完成数量), unit(单位),
* priority(紧急/高/中/低), type(新产品/常规/返修/来料加工),
* status(待排产/已排产/生产中/已完工/已取消),
* line(产线), planStart, planEnd, actualStart, actualEnd,
* bom: [{ name, qty, unit, code }], // 物料清单
* qualityStandard(质量标准), remark
* }
*/
const workOrders = ref([
{
id: 1, orderNo: 'WO-2026-0901', productName: '减速器总成 A型', productCode: 'RDC-A-001', spec: '1:30',
quantity: 200, completedQty: 120, unit: '台', priority: '高', type: '常规', status: '生产中',
line: '一号装配线', planStart: '2026-09-01', planEnd: '2026-09-15', actualStart: '2026-09-02', actualEnd: '',
bom: [
{ name: '齿轮箱外壳', code: 'HO-001', qty: 1, unit: '个' },
{ name: '齿轮副', code: 'GR-002', qty: 1, unit: '套' },
{ name: '轴承', code: 'BR-6205', qty: 4, unit: '个' },
{ name: '润滑脂', code: 'GS-001', qty: 0.2, unit: 'kg' },
],
qualityStandard: '噪音 ≤ 65dB,扭矩偏差 ≤ 5%', remark: '需全检',
},
{
id: 2, orderNo: 'WO-2026-0902', productName: '铝合金支架 B型', productCode: 'AL-B-022', spec: '300×200×5mm',
quantity: 500, completedQty: 500, unit: '件', priority: '中', type: '常规', status: '已完工',
line: '二号焊接线', planStart: '2026-09-03', planEnd: '2026-09-08', actualStart: '2026-09-03', actualEnd: '2026-09-07',
bom: [
{ name: '铝合金型材', code: 'AL-6063', qty: 2.5, unit: 'kg' },
{ name: '不锈钢螺钉', code: 'SC-M4', qty: 8, unit: '个' },
],
qualityStandard: '尺寸公差 ±0.1mm,表面无划痕', remark: '',
},
{
id: 3, orderNo: 'WO-2026-0903', productName: '控制电路板 C型', productCode: 'PCB-C-100', spec: '4层板',
quantity: 300, completedQty: 0, unit: '块', priority: '紧急', type: '新产品', status: '待排产',
line: '', planStart: '2026-09-12', planEnd: '2026-09-20', actualStart: '', actualEnd: '',
bom: [
{ name: 'PCB 基板', code: 'PCB-4L', qty: 1, unit: '块' },
{ name: '贴片元件包', code: 'SMD-KIT', qty: 1, unit: '套' },
{ name: '连接器', code: 'CN-8P', qty: 2, unit: '个' },
],
qualityStandard: '焊点无虚焊,通电测试通过率 ≥ 99%', remark: '首次试产,需工艺验证',
},
{
id: 4, orderNo: 'WO-2026-0904', productName: '不锈钢外壳 D型', productCode: 'SS-D-050', spec: '1.2mm 304',
quantity: 150, completedQty: 45, unit: '个', priority: '低', type: '返修', status: '生产中',
line: '三号喷涂线', planStart: '2026-09-05', planEnd: '2026-09-18', actualStart: '2026-09-06', actualEnd: '',
bom: [
{ name: '304 不锈钢板', code: 'SS-304-12', qty: 0.8, unit: 'kg' },
{ name: '粉末涂料', code: 'POW-W', qty: 0.05, unit: 'kg' },
],
qualityStandard: '涂层厚度 60-80μm,附着力 1 级', remark: '返修批次,加强外观检验',
},
])
/* ===================== State:工序排程 ===================== */
/**
* 工序排程列表
* 结构: {
* id, scheduleNo, workOrderId(关联工单), workOrderNo,
* line, processName(工序名), processSeq(工序序号),
* operator(操作工), planStart/planEnd/actualStart/actualEnd,
* status(未开始/进行中/已完成/已延期/已取消),
* durationHours(计划工时), actualHours(实际工时),
* resources(资源: [{ name, type }]), remark
* }
*/
const processSchedules = ref([
{
id: 1, scheduleNo: 'PS-2026-0901-01', workOrderId: 1, workOrderNo: 'WO-2026-0901',
line: '一号装配线', processName: '齿轮装配', processSeq: 1, operator: '王强',
planStart: '2026-09-02 08:00', planEnd: '2026-09-03 18:00', actualStart: '2026-09-02 08:15', actualEnd: '',
status: '进行中', durationHours: 16, actualHours: 8,
resources: [{ name: '装配工作台 A1', type: '设备' }, { name: '扭力扳手 TN-01', type: '工具' }],
remark: '',
},
{
id: 2, scheduleNo: 'PS-2026-0901-02', workOrderId: 1, workOrderNo: 'WO-2026-0901',
line: '一号装配线', processName: '整机装配', processSeq: 2, operator: '张伟',
planStart: '2026-09-04 08:00', planEnd: '2026-09-08 18:00', actualStart: '', actualEnd: '',
status: '未开始', durationHours: 32, actualHours: 0,
resources: [{ name: '装配工作台 A2', type: '设备' }],
remark: '等待齿轮装配完成',
},
{
id: 3, scheduleNo: 'PS-2026-0901-03', workOrderId: 1, workOrderNo: 'WO-2026-0901',
line: '一号装配线', processName: '性能测试', processSeq: 3, operator: '李娜',
planStart: '2026-09-09 08:00', planEnd: '2026-09-10 18:00', actualStart: '', actualEnd: '',
status: '未开始', durationHours: 16, actualHours: 0,
resources: [{ name: '测试台 TT-02', type: '设备' }, { name: '噪音检测仪', type: '仪器' }],
remark: '',
},
{
id: 4, scheduleNo: 'PS-2026-0904-01', workOrderId: 4, workOrderNo: 'WO-2026-0904',
line: '三号喷涂线', processName: '喷涂烘干', processSeq: 1, operator: '赵敏',
planStart: '2026-09-06 08:00', planEnd: '2026-09-07 12:00', actualStart: '2026-09-06 08:00', actualEnd: '2026-09-07 10:00',
status: '已完成', durationHours: 12, actualHours: 10,
resources: [{ name: '喷涂机器人 RB-01', type: '设备' }, { name: '烘干炉 OV-03', type: '设备' }],
remark: '提前 2 小时完成',
},
])
/* ===================== State:设备状态 ===================== */
/**
* 设备台账
* 结构: {
* id, equipmentNo, name, model, line, type,
* status(运行/停机/维修/保养/故障),
* temperature(℃), speed(转速/速率), runHours(运行小时),
* utilizationRate(利用率 %), lastMaintainDate, nextMaintainDate,
* currentTask(当前任务), errorCount(本月故障次数)
* }
*/
const equipments = ref([
{ id: 1, equipmentNo: 'EQ-001', name: '装配工作台 A1', model: 'ASM-A1', line: '一号装配线', type: '装配设备', status: '运行', temperature: 42, speed: 120, runHours: 1280, utilizationRate: 88, lastMaintainDate: '2026-08-10', nextMaintainDate: '2026-09-25', currentTask: '齿轮装配', errorCount: 0 },
{ id: 2, equipmentNo: 'EQ-002', name: '装配工作台 A2', model: 'ASM-A2', line: '一号装配线', type: '装配设备', status: '空闲', temperature: 28, speed: 0, runHours: 980, utilizationRate: 45, lastMaintainDate: '2026-08-20', nextMaintainDate: '2026-09-30', currentTask: '', errorCount: 1 },
{ id: 3, equipmentNo: 'EQ-003', name: '焊接机器人 RB-01', model: 'ABB-IRB6700', line: '二号焊接线', type: '焊接设备', status: '运行', temperature: 56, speed: 85, runHours: 2100, utilizationRate: 92, lastMaintainDate: '2026-07-15', nextMaintainDate: '2026-09-15', currentTask: '铝合金支架焊接', errorCount: 0 },
{ id: 4, equipmentNo: 'EQ-004', name: '喷涂机器人 RB-01', model: 'FANUC-P250', line: '三号喷涂线', type: '喷涂设备', status: '维修', temperature: 0, speed: 0, runHours: 1560, utilizationRate: 0, lastMaintainDate: '2026-09-10', nextMaintainDate: '2026-09-10', currentTask: '', errorCount: 2 },
{ id: 5, equipmentNo: 'EQ-005', name: '烘干炉 OV-03', model: 'OV-300', line: '三号喷涂线', type: '热处理设备', status: '运行', temperature: 180, speed: 0, runHours: 3400, utilizationRate: 76, lastMaintainDate: '2026-08-01', nextMaintainDate: '2026-10-01', currentTask: '外壳烘干', errorCount: 0 },
{ id: 6, equipmentNo: 'EQ-006', name: '测试台 TT-02', model: 'TEST-200', line: '一号装配线', type: '测试设备', status: '保养', temperature: 0, speed: 0, runHours: 650, utilizationRate: 0, lastMaintainDate: '2026-09-12', nextMaintainDate: '2026-09-12', currentTask: '', errorCount: 0 },
])
/* ===================== State:设备维修记录 ===================== */
/**
* 设备维修/保养记录
* 结构: { id, equipmentNo, equipmentName, type(故障维修/定期保养/点检), description, handler, startTime, endTime, durationHours, cost(元), parts(更换配件[]), status(处理中/已完成) }
*/
const maintenanceRecords = ref([
{ id: 1, equipmentNo: 'EQ-004', equipmentName: '喷涂机器人 RB-01', type: '故障维修', description: '喷枪堵塞,雾化不良', handler: '陈刚', startTime: '2026-09-10 09:00', endTime: '', durationHours: 0, cost: 0, parts: [{ name: '喷枪', qty: 1 }], status: '处理中' },
{ id: 2, equipmentNo: 'EQ-006', equipmentName: '测试台 TT-02', type: '定期保养', description: '季度校准与清洁', handler: '赵敏', startTime: '2026-09-12 08:00', endTime: '', durationHours: 0, cost: 0, parts: [], status: '处理中' },
{ id: 3, equipmentNo: 'EQ-002', equipmentName: '装配工作台 A2', type: '故障维修', description: '气动夹爪漏气', handler: '王强', startTime: '2026-09-08 14:00', endTime: '2026-09-08 16:30', durationHours: 2.5, cost: 320, parts: [{ name: '密封圈', qty: 2 }], status: '已完成' },
])
/** 自增 id */
const seq = ref(1000)
/* ===================== Getters ===================== */
/** 工单统计:按状态分类 */
const orderStats = computed(() => {
const stats = { 待排产: 0, 已排产: 0, 生产中: 0, 已完工: 0, 已取消: 0 }
workOrders.value.forEach(o => { stats[o.status] = (stats[o.status] || 0) + 1 })
return stats
})
/** 工单总计划数 / 已完工数 / 完成率 */
const orderSummary = computed(() => {
const total = workOrders.value.reduce((s, o) => s + o.quantity, 0)
const completed = workOrders.value.reduce((s, o) => s + o.completedQty, 0)
return {
total,
completed,
completionRate: total > 0 ? Math.round((completed / total) * 1000) / 10 : 0,
}
})
/** 工序统计:进行中 / 已延期 */
const processStats = computed(() => ({
total: processSchedules.value.length,
inProgress: processSchedules.value.filter(p => p.status === '进行中').length,
completed: processSchedules.value.filter(p => p.status === '已完成').length,
delayed: processSchedules.value.filter(p => p.status === '已延期').length,
}))
/** 设备状态统计 */
const equipmentStats = computed(() => {
const stats = { 运行: 0, 空闲: 0, 停机: 0, 维修: 0, 保养: 0, 故障: 0 }
equipments.value.forEach(e => { stats[e.status] = (stats[e.status] || 0) + 1 })
return stats
})
/** 设备平均利用率 */
const avgUtilization = computed(() => {
const running = equipments.value.filter(e => e.status === '运行')
if (running.length === 0) return 0
return Math.round(running.reduce((s, e) => s + e.utilizationRate, 0) / running.length)
})
/** 待处理维修单数 */
const pendingMaintenance = computed(() =>
maintenanceRecords.value.filter(m => m.status === '处理中').length
)
/** 工单号 → 工单对象映射 */
const orderMap = computed(() => {
const map = {}
workOrders.value.forEach(o => { map[o.id] = o })
return map
})
/** 设备编号 → 设备对象映射 */
const equipmentMap = computed(() => {
const map = {}
equipments.value.forEach(e => { map[e.equipmentNo] = e })
return map
})
/** 产线列表(工单下拉 / 排程下拉用) */
const lineOptions = computed(() =>
Array.from(new Set([...workOrders.value.map(o => o.line), ...equipments.value.map(e => e.line)]))
.filter(Boolean)
.map(l => ({ label: l, value: l }))
)
/** 工单号下拉选项 */
const orderOptions = computed(() =>
workOrders.value
.filter(o => o.status !== '已完工' && o.status !== '已取消')
.map(o => ({ label: `${o.orderNo} ${o.productName}`, value: o.id, orderNo: o.orderNo, line: o.line }))
)
/** 设备下拉选项 */
const equipmentOptions = computed(() =>
equipments.value.map(e => ({ label: `${e.equipmentNo} ${e.name}`, value: e.equipmentNo, name: e.name }))
)
/* ===================== Actions:生产工单 ===================== */
/** 新增工单 */
function addWorkOrder(form) {
workOrders.value.unshift({
id: ++seq.value,
orderNo: form.orderNo,
productName: form.productName,
productCode: form.productCode,
spec: form.spec,
quantity: Number(form.quantity),
completedQty: 0,
unit: form.unit,
priority: form.priority,
type: form.type,
status: form.status || '待排产',
line: form.line,
planStart: form.planStart,
planEnd: form.planEnd,
actualStart: '',
actualEnd: '',
bom: [...form.bom],
qualityStandard: form.qualityStandard,
remark: form.remark,
})
}
/** 更新工单 */
function updateWorkOrder(form) {
const target = workOrders.value.find(o => o.id === form.id)
if (target) {
Object.assign(target, {
orderNo: form.orderNo,
productName: form.productName,
productCode: form.productCode,
spec: form.spec,
quantity: Number(form.quantity),
unit: form.unit,
priority: form.priority,
type: form.type,
status: form.status,
line: form.line,
planStart: form.planStart,
planEnd: form.planEnd,
actualStart: form.actualStart,
actualEnd: form.actualEnd,
bom: [...form.bom],
qualityStandard: form.qualityStandard,
remark: form.remark,
})
}
}
/** 删除工单 */
function deleteWorkOrder(id) {
const i = workOrders.value.findIndex(o => o.id === id)
if (i !== -1) workOrders.value.splice(i, 1)
}
/** 更新工单完工数量(支持进度填写) */
function updateCompletedQty(id, qty) {
const target = workOrders.value.find(o => o.id === id)
if (target) target.completedQty = Math.min(Number(qty), target.quantity)
}
/* ===================== Actions:工序排程 ===================== */
/** 新增工序排程 */
function addProcessSchedule(form) {
const order = orderMap.value[form.workOrderId]
processSchedules.value.unshift({
id: ++seq.value,
scheduleNo: form.scheduleNo,
workOrderId: form.workOrderId,
workOrderNo: order ? order.orderNo : '',
line: form.line,
processName: form.processName,
processSeq: Number(form.processSeq),
operator: form.operator,
planStart: form.planStart,
planEnd: form.planEnd,
actualStart: form.actualStart || '',
actualEnd: form.actualEnd || '',
status: form.status || '未开始',
durationHours: Number(form.durationHours),
actualHours: Number(form.actualHours) || 0,
resources: [...form.resources],
remark: form.remark,
})
// 若工单还在"待排产",自动推进到"已排产"
if (order && order.status === '待排产') order.status = '已排产'
}
/** 更新工序 */
function updateProcessSchedule(form) {
const target = processSchedules.value.find(p => p.id === form.id)
if (target) {
Object.assign(target, {
line: form.line,
processName: form.processName,
processSeq: Number(form.processSeq),
operator: form.operator,
planStart: form.planStart,
planEnd: form.planEnd,
actualStart: form.actualStart,
actualEnd: form.actualEnd,
status: form.status,
durationHours: Number(form.durationHours),
actualHours: Number(form.actualHours) || 0,
resources: [...form.resources],
remark: form.remark,
})
}
}
/** 删除工序 */
function deleteProcessSchedule(id) {
const i = processSchedules.value.findIndex(p => p.id === id)
if (i !== -1) processSchedules.value.splice(i, 1)
}
/* ===================== Actions:设备维修 ===================== */
/** 新增维修/保养记录,并同步设备状态 */
function addMaintenanceRecord(form) {
const equipment = equipmentMap.value[form.equipmentNo]
maintenanceRecords.value.unshift({
id: ++seq.value,
equipmentNo: form.equipmentNo,
equipmentName: equipment ? equipment.name : '',
type: form.type,
description: form.description,
handler: form.handler,
startTime: form.startTime,
endTime: '',
durationHours: 0,
cost: 0,
parts: [...form.parts],
status: '处理中',
})
// 同步设备状态
if (equipment) {
equipment.status = form.type === '故障维修' ? '维修' : '保养'
equipment.currentTask = ''
equipment.temperature = 0
equipment.speed = 0
equipment.utilizationRate = 0
}
}
/** 完成维修记录,恢复设备状态为"空闲" */
function completeMaintenanceRecord(id, form) {
const record = maintenanceRecords.value.find(m => m.id === id)
if (record) {
record.endTime = form.endTime
record.durationHours = Number(form.durationHours)
record.cost = Number(form.cost)
record.status = '已完成'
// 恢复设备状态
const equipment = equipmentMap.value[record.equipmentNo]
if (equipment) {
equipment.status = '空闲'
equipment.lastMaintainDate = form.endTime.slice(0, 10)
}
}
}
return {
// 常量
orderStatusFlow,
processStatusList,
equipmentStatusList,
// State
workOrders,
processSchedules,
equipments,
maintenanceRecords,
// Getters
orderStats,
orderSummary,
processStats,
equipmentStats,
avgUtilization,
pendingMaintenance,
orderMap,
equipmentMap,
lineOptions,
orderOptions,
equipmentOptions,
// Actions
addWorkOrder,
updateWorkOrder,
deleteWorkOrder,
updateCompletedQty,
addProcessSchedule,
updateProcessSchedule,
deleteProcessSchedule,
addMaintenanceRecord,
completeMaintenanceRecord,
}
})
ProductionManagement.vue
javascript
<!--
生产管理模块(基于 Ant Design Vue + Pinia)
三个子功能:生产工单(CRUD + BOM 物料清单 + 进度填写)、工序排程(甘特式时间线 + 复杂表单)、设备状态(实时卡片 + 维修记录)
-->
<template>
<div class="prod-wrap">
<a-tabs v-model:activeKey="activeTab">
<!-- ==================== 生产工单 ==================== -->
<a-tab-pane key="order" tab="生产工单">
<!-- 顶部统计 -->
<a-row :gutter="16" class="stat-row">
<a-col :span="5"><a-card size="small"><a-statistic title="工单总数" :value="production.workOrders.length" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="生产中" :value="production.orderStats['生产中']" :value-style="{ color: '#1677ff' }" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="已完工" :value="production.orderStats['已完工']" :value-style="{ color: '#2eb85c' }" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="待排产" :value="production.orderStats['待排产']" :value-style="{ color: '#e6a23c' }" /></a-card></a-col>
<a-col :span="4" class="add-col">
<a-button type="primary" block @click="openOrderModal()">
<template #icon><PlusOutlined /></template>
新建工单
</a-button>
</a-col>
</a-row>
<!-- 筛选栏 -->
<a-card size="small" class="filter-card">
<a-form layout="inline" :model="orderFilter">
<a-form-item label="状态">
<a-select v-model:value="orderFilter.status" :options="statusFilterOptions" allow-clear placeholder="全部" style="width: 120px" />
</a-form-item>
<a-form-item label="优先级">
<a-select v-model:value="orderFilter.priority" :options="priorityOptions" allow-clear placeholder="全部" style="width: 120px" />
</a-form-item>
<a-form-item label="关键词">
<a-input v-model:value="orderFilter.keyword" placeholder="工单号/产品名" allow-clear style="width: 180px" />
</a-form-item>
</a-form>
</a-card>
<!-- 工单表格 -->
<a-table
:columns="orderColumns"
:data-source="filteredOrders"
row-key="id"
:pagination="{ pageSize: 4 }"
:scroll="{ x: 1400 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'priority'">
<a-tag :color="priorityColor[record.priority]">{{ record.priority }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="statusColor[record.status]">{{ record.status }}</a-tag>
</template>
<template v-else-if="column.key === 'progress'">
<a-progress
:percent="Math.round(record.completedQty / record.quantity * 100)"
size="small"
:status="record.status === '已完工' ? 'success' : (record.completedQty > 0 ? 'active' : 'normal')"
/>
<span class="qty-text">{{ record.completedQty }}/{{ record.quantity }} {{ record.unit }}</span>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="openOrderDetail(record)">详情</a-button>
<a-button type="link" size="small" @click="openOrderModal(record)">编辑</a-button>
<a-button type="link" size="small" @click="openProgressModal(record)">报工</a-button>
<a-popconfirm title="确定删除该工单吗?" @confirm="production.deleteWorkOrder(record.id)">
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-tab-pane>
<!-- ==================== 工序排程 ==================== -->
<a-tab-pane key="process" tab="工序排程">
<a-row :gutter="16" class="stat-row">
<a-col :span="5"><a-card size="small"><a-statistic title="排程总数" :value="production.processStats.total" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="进行中" :value="production.processStats.inProgress" :value-style="{ color: '#1677ff' }" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="已完成" :value="production.processStats.completed" :value-style="{ color: '#2eb85c' }" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="已延期" :value="production.processStats.delayed" :value-style="{ color: '#e74c3c' }" /></a-card></a-col>
<a-col :span="4" class="add-col">
<a-button type="primary" block @click="openProcessModal()">
<template #icon><PlusOutlined /></template>
新增工序
</a-button>
</a-col>
</a-row>
<!-- 排程甘特式时间线 -->
<a-card size="small" title="排程时间线" class="timeline-card">
<a-timeline mode="left" class="gantt-timeline">
<a-timeline-item
v-for="p in processSchedules"
:key="p.id"
:color="timelineColor[p.status]"
>
<template #label>{{ p.planStart.slice(0, 10) }}</template>
<div class="tl-item">
<div class="tl-head">
<a-tag color="blue">{{ p.workOrderNo }}</a-tag>
<strong>{{ p.processName }}</strong>
<a-tag :color="statusColor[p.status]">{{ p.status }}</a-tag>
</div>
<div class="tl-meta">
产线:{{ p.line }} | 操作:{{ p.operator }} | 计划:{{ p.planStart.slice(5) }} ~ {{ p.planEnd.slice(5) }}
| 工时:{{ p.actualHours }}/{{ p.durationHours }}h
</div>
<div class="tl-progress">
<a-progress
:percent="p.durationHours > 0 ? Math.round(p.actualHours / p.durationHours * 100) : 0"
size="small"
/>
</div>
<div class="tl-resources" v-if="p.resources.length">
<a-tag v-for="r in p.resources" :key="r.name" class="res-tag">
<template #icon><ToolOutlined /></template>
{{ r.name }}
</a-tag>
</div>
</div>
</a-timeline-item>
</a-timeline>
</a-card>
<!-- 工序表格 -->
<a-table
:columns="processColumns"
:data-source="processSchedules"
row-key="id"
:pagination="{ pageSize: 5 }"
:scroll="{ x: 1300 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="statusColor[record.status]">{{ record.status }}</a-tag>
</template>
<template v-else-if="column.key === 'action'">
<a-space>
<a-button type="link" size="small" @click="openProcessModal(record)">编辑</a-button>
<a-popconfirm title="删除该工序排程?" @confirm="production.deleteProcessSchedule(record.id)">
<a-button type="link" size="small" danger>删除</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-tab-pane>
<!-- ==================== 设备状态 ==================== -->
<a-tab-pane key="equipment" tab="设备状态">
<a-row :gutter="16" class="stat-row">
<a-col :span="5"><a-card size="small"><a-statistic title="设备总数" :value="production.equipments.length" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="运行中" :value="production.equipmentStats['运行']" :value-style="{ color: '#2eb85c' }" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="维修/保养" :value="(production.equipmentStats['维修']||0) + (production.equipmentStats['保养']||0)" :value-style="{ color: '#e6a23c' }" /></a-card></a-col>
<a-col :span="5"><a-card size="small"><a-statistic title="平均利用率" :value="production.avgUtilization" suffix="%" :value-style="{ color: '#1677ff' }" /></a-card></a-col>
<a-col :span="4" class="add-col">
<a-button type="primary" block @click="openMaintenanceModal()">
<template #icon><ToolOutlined /></template>
报修/保养
</a-button>
</a-col>
</a-row>
<!-- 设备卡片网格 -->
<a-row :gutter="[16, 16]">
<a-col :xs="24" :sm="12" :lg="8" v-for="eq in equipments" :key="eq.id">
<a-card size="small" class="eq-card">
<div class="eq-head">
<div>
<div class="eq-name">{{ eq.name }}</div>
<div class="eq-no">{{ eq.equipmentNo }} · {{ eq.model }}</div>
</div>
<a-tag :color="eqStatusColor[eq.status]" class="eq-status">{{ eq.status }}</a-tag>
</div>
<a-row :gutter="8" class="eq-metrics">
<a-col :span="8"><div class="metric">{{ eq.temperature }}<span class="unit">℃</span></div><div class="metric-label">温度</div></a-col>
<a-col :span="8"><div class="metric">{{ eq.speed }}<span class="unit">{{ eq.type==='测试设备'?'':'rpm' }}</span></div><div class="metric-label">转速</div></a-col>
<a-col :span="8"><div class="metric">{{ eq.runHours }}<span class="unit">h</span></div><div class="metric-label">累计运行</div></a-col>
</a-row>
<a-progress
:percent="eq.utilizationRate"
size="small"
:status="eq.status === '运行' ? 'active' : 'normal'"
/>
<div class="eq-foot">
<span>产线:{{ eq.line }}</span>
<span v-if="eq.currentTask">当前:{{ eq.currentTask }}</span>
<span>下次保养:{{ eq.nextMaintainDate }}</span>
</div>
</a-card>
</a-col>
</a-row>
<!-- 维修记录 -->
<a-card size="small" title="维修 / 保养记录" style="margin-top: 1rem">
<a-table
:columns="maintenanceColumns"
:data-source="maintenanceRecords"
row-key="id"
:pagination="{ pageSize: 5 }"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="record.type === '故障维修' ? 'red' : 'blue'">{{ record.type }}</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === '已完成' ? 'green' : 'orange'">{{ record.status }}</a-tag>
</template>
<template v-else-if="column.key === 'parts'">
<a-tag v-for="p in record.parts" :key="p.name" class="res-tag">{{ p.name }} ×{{ p.qty }}</a-tag>
<span v-if="!record.parts.length" class="muted">---</span>
</template>
<template v-else-if="column.key === 'action'">
<a-popconfirm
v-if="record.status === '处理中'"
title="确认完成维修?"
@confirm="openCompleteModal(record)"
>
<a-button type="link" size="small">完成</a-button>
</a-popconfirm>
<span v-else class="muted">---</span>
</template>
</template>
</a-table>
</a-card>
</a-tab-pane>
</a-tabs>
</div>
<!-- ============ 工单 新增/编辑 弹窗(含动态 BOM 行) ============ -->
<a-modal
v-model:open="orderModalOpen"
:title="orderModalTitle"
:width="720"
@ok="submitOrder"
@cancel="orderModalOpen = false"
>
<a-form ref="orderFormRef" :model="orderForm" :rules="orderRules" layout="vertical">
<a-row :gutter="12">
<a-col :span="8">
<a-form-item label="工单号" name="orderNo">
<a-input v-model:value="orderForm.orderNo" placeholder="如 WO-2026-0910" />
</a-form-item>
</a-col>
<a-col :span="10">
<a-form-item label="产品名称" name="productName">
<a-input v-model:value="orderForm.productName" placeholder="产品名称" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="产品编码" name="productCode">
<a-input v-model:value="orderForm.productCode" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item label="规格型号" name="spec">
<a-input v-model:value="orderForm.spec" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="计划数量" name="quantity">
<a-input-number v-model:value="orderForm.quantity" :min="1" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="4">
<a-form-item label="单位" name="unit">
<a-input v-model:value="orderForm.unit" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="类型" name="type">
<a-select v-model:value="orderForm.type" :options="orderTypeOptions" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item label="优先级" name="priority">
<a-select v-model:value="orderForm.priority" :options="priorityOptions" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="状态" name="status">
<a-select v-model:value="orderForm.status" :options="orderStatusOptions" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="产线" name="line">
<a-select v-model:value="orderForm.line" :options="production.lineOptions" allow-clear />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="计划开始" name="planStart">
<a-date-picker v-model:value="orderForm.planStart" value-format="YYYY-MM-DD" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划结束" name="planEnd">
<a-date-picker v-model:value="orderForm.planEnd" value-format="YYYY-MM-DD" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<!-- BOM 物料清单(动态增减行) -->
<a-divider orientation="left">BOM 物料清单</a-divider>
<div v-for="(item, idx) in orderForm.bom" :key="idx" class="bom-row">
<a-row :gutter="8">
<a-col :span="8"><a-input v-model:value="item.name" placeholder="物料名称" /></a-col>
<a-col :span="5"><a-input v-model:value="item.code" placeholder="物料编码" /></a-col>
<a-col :span="4"><a-input-number v-model:value="item.qty" :min="0" :step="0.1" style="width: 100%" /></a-col>
<a-col :span="4"><a-input v-model:value="item.unit" placeholder="单位" /></a-col>
<a-col :span="3">
<a-button type="link" danger @click="removeBomRow(idx)">删除</a-button>
</a-col>
</a-row>
</div>
<a-button type="dashed" block @click="addBomRow" style="margin-top: 8px">
<template #icon><PlusOutlined /></template>
添加物料
</a-button>
<a-form-item label="质量标准" name="qualityStandard" style="margin-top: 16px">
<a-textarea v-model:value="orderForm.qualityStandard" :rows="2" placeholder="如:噪音 ≤ 65dB,扭矩偏差 ≤ 5%" />
</a-form-item>
<a-form-item label="备注">
<a-input v-model:value="orderForm.remark" placeholder="可选" />
</a-form-item>
</a-form>
</a-modal>
<!-- ============ 工单详情抽屉 ============ -->
<a-drawer v-model:open="orderDetailOpen" title="工单详情" :width="520">
<template v-if="currentOrder">
<a-descriptions :column="2" bordered size="small">
<a-descriptions-item label="工单号">{{ currentOrder.orderNo }}</a-descriptions-item>
<a-descriptions-item label="状态"><a-tag :color="statusColor[currentOrder.status]">{{ currentOrder.status }}</a-tag></a-descriptions-item>
<a-descriptions-item label="产品名称">{{ currentOrder.productName }}</a-descriptions-item>
<a-descriptions-item label="产品编码">{{ currentOrder.productCode }}</a-descriptions-item>
<a-descriptions-item label="规格">{{ currentOrder.spec }}</a-descriptions-item>
<a-descriptions-item label="数量">{{ currentOrder.quantity }} {{ currentOrder.unit }}</a-descriptions-item>
<a-descriptions-item label="优先级"><a-tag :color="priorityColor[currentOrder.priority]">{{ currentOrder.priority }}</a-tag></a-descriptions-item>
<a-descriptions-item label="类型">{{ currentOrder.type }}</a-descriptions-item>
<a-descriptions-item label="产线">{{ currentOrder.line || '---' }}</a-descriptions-item>
<a-descriptions-item label="计划周期">{{ currentOrder.planStart }} ~ {{ currentOrder.planEnd }}</a-descriptions-item>
</a-descriptions>
<a-divider orientation="left">生产进度</a-divider>
<a-progress
:percent="Math.round(currentOrder.completedQty / currentOrder.quantity * 100)"
:status="currentOrder.status === '已完工' ? 'success' : 'active'"
/>
<p class="progress-text">已完成 {{ currentOrder.completedQty }} / 计划 {{ currentOrder.quantity }} {{ currentOrder.unit }}</p>
<a-divider orientation="left">BOM 物料清单</a-divider>
<a-table
:data-source="currentOrder.bom"
:pagination="false"
size="small"
:columns="bomColumns"
/>
<a-divider orientation="left">质量标准</a-divider>
<p>{{ currentOrder.qualityStandard || '---' }}</p>
<a-divider orientation="left">备注</a-divider>
<p>{{ currentOrder.remark || '---' }}</p>
</template>
</a-drawer>
<!-- ============ 报工弹窗 ============ -->
<a-modal v-model:open="progressModalOpen" title="生产报工" @ok="submitProgress" @cancel="progressModalOpen = false">
<a-form layout="vertical">
<a-form-item label="当前工单">
<a-input :value="currentOrder?.orderNo" disabled />
</a-form-item>
<a-form-item label="已完成数量">
<a-input-number
v-model:value="progressForm.completedQty"
:min="0"
:max="currentOrder?.quantity"
style="width: 100%"
/>
<div class="muted">计划数量:{{ currentOrder?.quantity }} {{ currentOrder?.unit }}</div>
</a-form-item>
<a-form-item label="是否完工">
<a-switch v-model:checked="progressForm.finished" />
<span class="muted" style="margin-left: 8px">开启则工单状态自动变为「已完工」</span>
</a-form-item>
</a-form>
</a-modal>
<!-- ============ 工序 新增/编辑 弹窗 ============ -->
<a-modal
v-model:open="processModalOpen"
:title="processModalTitle"
:width="680"
@ok="submitProcess"
@cancel="processModalOpen = false"
>
<a-form ref="processFormRef" :model="processForm" :rules="processRules" layout="vertical">
<a-row :gutter="12">
<a-col :span="10">
<a-form-item label="关联工单" name="workOrderId">
<a-select v-model:value="processForm.workOrderId" :options="production.orderOptions" @change="onOrderChange" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item label="工序序号" name="processSeq">
<a-input-number v-model:value="processForm.processSeq" :min="1" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="工序名称" name="processName">
<a-input v-model:value="processForm.processName" placeholder="如:齿轮装配" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="产线" name="line">
<a-select v-model:value="processForm.line" :options="production.lineOptions" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="操作工" name="operator">
<a-input v-model:value="processForm.operator" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="计划开始" name="planStart">
<a-date-picker v-model:value="processForm.planStart" show-time value-format="YYYY-MM-DD HH:mm" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="计划结束" name="planEnd">
<a-date-picker v-model:value="processForm.planEnd" show-time value-format="YYYY-MM-DD HH:mm" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="实际开始">
<a-date-picker v-model:value="processForm.actualStart" show-time value-format="YYYY-MM-DD HH:mm" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="实际结束">
<a-date-picker v-model:value="processForm.actualEnd" show-time value-format="YYYY-MM-DD HH:mm" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="12">
<a-col :span="8">
<a-form-item label="计划工时(h)" name="durationHours">
<a-input-number v-model:value="processForm.durationHours" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="实际工时(h)">
<a-input-number v-model:value="processForm.actualHours" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="状态" name="status">
<a-select v-model:value="processForm.status" :options="processStatusOptions" />
</a-form-item>
</a-col>
</a-row>
<!-- 资源配置(动态行) -->
<a-divider orientation="left">资源配置</a-divider>
<div v-for="(r, idx) in processForm.resources" :key="idx" class="bom-row">
<a-row :gutter="8">
<a-col :span="10"><a-input v-model:value="r.name" placeholder="资源名称" /></a-col>
<a-col :span="8">
<a-select v-model:value="r.type" :options="resourceTypeOptions" />
</a-col>
<a-col :span="6">
<a-button type="link" danger @click="removeResource(idx)">删除</a-button>
</a-col>
</a-row>
</div>
<a-button type="dashed" block @click="addResource" style="margin-top: 8px">
<template #icon><PlusOutlined /></template>
添加资源
</a-button>
<a-form-item label="备注" style="margin-top: 16px">
<a-textarea v-model:value="processForm.remark" :rows="2" />
</a-form-item>
</a-form>
</a-modal>
<!-- ============ 报修/保养 弹窗 ============ -->
<a-modal
v-model:open="maintenanceModalOpen"
title="设备报修 / 保养登记"
@ok="submitMaintenance"
@cancel="maintenanceModalOpen = false"
>
<a-form ref="maintenanceFormRef" :model="maintenanceForm" :rules="maintenanceRules" layout="vertical">
<a-form-item label="设备" name="equipmentNo">
<a-select v-model:value="maintenanceForm.equipmentNo" :options="production.equipmentOptions" show-search option-filter-prop="label" />
</a-form-item>
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="类型" name="type">
<a-select v-model:value="maintenanceForm.type" :options="maintenanceTypeOptions" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="处理人" name="handler">
<a-input v-model:value="maintenanceForm.handler" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="开始时间" name="startTime">
<a-date-picker v-model:value="maintenanceForm.startTime" show-time value-format="YYYY-MM-DD HH:mm" style="width: 100%" />
</a-form-item>
<a-form-item label="故障/保养描述" name="description">
<a-textarea v-model:value="maintenanceForm.description" :rows="3" placeholder="详细描述故障现象或保养内容" />
</a-form-item>
<!-- 更换配件(动态行) -->
<a-divider orientation="left">更换配件</a-divider>
<div v-for="(p, idx) in maintenanceForm.parts" :key="idx" class="bom-row">
<a-row :gutter="8">
<a-col :span="14"><a-input v-model:value="p.name" placeholder="配件名称" /></a-col>
<a-col :span="6"><a-input-number v-model:value="p.qty" :min="1" style="width: 100%" /></a-col>
<a-col :span="4"><a-button type="link" danger @click="removePart(idx)">删除</a-button></a-col>
</a-row>
</div>
<a-button type="dashed" block @click="addPart" style="margin-top: 8px">
<template #icon><PlusOutlined /></template>
添加配件
</a-button>
</a-form>
</a-modal>
<!-- ============ 完成维修 弹窗 ============ -->
<a-modal
v-model:open="completeModalOpen"
title="完成维修"
@ok="submitComplete"
@cancel="completeModalOpen = false"
>
<a-form layout="vertical">
<a-form-item label="完成时间" name="endTime">
<a-date-picker v-model:value="completeForm.endTime" show-time value-format="YYYY-MM-DD HH:mm" style="width: 100%" />
</a-form-item>
<a-row :gutter="12">
<a-col :span="12">
<a-form-item label="维修耗时(h)">
<a-input-number v-model:value="completeForm.durationHours" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="费用(元)">
<a-input-number v-model:value="completeForm.cost" :min="0" style="width: 100%" />
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-modal>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { message } from 'ant-design-vue'
import { PlusOutlined, ToolOutlined } from '@ant-design/icons-vue'
import dayjs from 'dayjs'
import { useProductionStore } from '../stores/production'
const production = useProductionStore()
const { workOrders, processSchedules, equipments, maintenanceRecords } = storeToRefs(production)
/* 当前 Tab */
const activeTab = ref('order')
/* ===================== 颜色映射 ===================== */
const priorityColor = { '紧急': 'red', '高': 'orange', '中': 'blue', '低': 'default' }
const statusColor = {
'待排产': 'default', '已排产': 'cyan', '生产中': 'blue', '已完工': 'green', '已取消': 'red',
'未开始': 'default', '进行中': 'blue', '已完成': 'green', '已延期': 'red',
}
const eqStatusColor = {
'运行': 'green', '空闲': 'default', '停机': 'orange', '维修': 'red', '保养': 'blue', '故障': 'red',
}
const timelineColor = {
'未开始': 'gray', '进行中': 'blue', '已完成': 'green', '已延期': 'red', '已取消': 'gray',
}
/* ===================== 选项常量 ===================== */
const priorityOptions = [
{ label: '紧急', value: '紧急' }, { label: '高', value: '高' },
{ label: '中', value: '中' }, { label: '低', value: '低' },
]
const orderTypeOptions = [
{ label: '新产品', value: '新产品' }, { label: '常规', value: '常规' },
{ label: '返修', value: '返修' }, { label: '来料加工', value: '来料加工' },
]
const orderStatusOptions = production.orderStatusFlow.map(s => ({ label: s, value: s }))
const statusFilterOptions = orderStatusOptions
const processStatusOptions = production.processStatusList.map(s => ({ label: s, value: s }))
const resourceTypeOptions = [
{ label: '设备', value: '设备' }, { label: '工具', value: '工具' },
{ label: '仪器', value: '仪器' }, { label: '工装', value: '工装' },
]
const maintenanceTypeOptions = [
{ label: '故障维修', value: '故障维修' }, { label: '定期保养', value: '定期保养' }, { label: '点检', value: '点检' },
]
/* ===================== 表格列 ===================== */
const orderColumns = [
{ title: '工单号', dataIndex: 'orderNo', key: 'orderNo', fixed: 'left', width: 130 },
{ title: '产品名称', dataIndex: 'productName', key: 'productName', width: 140 },
{ title: '规格', dataIndex: 'spec', key: 'spec', width: 110 },
{ title: '数量', key: 'progress', width: 180 },
{ title: '优先级', key: 'priority', width: 70 },
{ title: '类型', dataIndex: 'type', key: 'type', width: 80 },
{ title: '状态', key: 'status', width: 80 },
{ title: '产线', dataIndex: 'line', key: 'line', width: 110 },
{ title: '计划周期', key: 'plan', width: 180, customRender: ({ record }) => `${record.planStart} ~ ${record.planEnd}` },
{ title: '操作', key: 'action', fixed: 'right', width: 200 },
]
const processColumns = [
{ title: '排程号', dataIndex: 'scheduleNo', key: 'scheduleNo', width: 150 },
{ title: '关联工单', dataIndex: 'workOrderNo', key: 'workOrderNo', width: 130 },
{ title: '工序', key: 'process', width: 130, customRender: ({ record }) => `#${record.processSeq} ${record.processName}` },
{ title: '产线', dataIndex: 'line', key: 'line', width: 110 },
{ title: '操作工', dataIndex: 'operator', key: 'operator', width: 80 },
{ title: '计划区间', key: 'planRange', width: 200, customRender: ({ record }) => `${record.planStart.slice(5)} ~ ${record.planEnd.slice(5)}` },
{ title: '工时(h)', key: 'hours', width: 90, customRender: ({ record }) => `${record.actualHours}/${record.durationHours}` },
{ title: '状态', key: 'status', width: 80 },
{ title: '操作', key: 'action', width: 110 },
]
const maintenanceColumns = [
{ title: '设备', dataIndex: 'equipmentName', key: 'equipmentName', width: 180 },
{ title: '类型', key: 'type', width: 90 },
{ title: '描述', dataIndex: 'description', key: 'description' },
{ title: '处理人', dataIndex: 'handler', key: 'handler', width: 80 },
{ title: '开始时间', dataIndex: 'startTime', key: 'startTime', width: 140 },
{ title: '耗时(h)', dataIndex: 'durationHours', key: 'durationHours', width: 80 },
{ title: '费用', dataIndex: 'cost', key: 'cost', width: 80 },
{ title: '更换配件', key: 'parts', width: 160 },
{ title: '状态', key: 'status', width: 80 },
{ title: '操作', key: 'action', width: 70 },
]
const bomColumns = [
{ title: '物料名称', dataIndex: 'name', key: 'name' },
{ title: '编码', dataIndex: 'code', key: 'code' },
{ title: '用量', dataIndex: 'qty', key: 'qty', width: 80 },
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 70 },
]
/* ===================== 工单筛选 ===================== */
const orderFilter = reactive({ status: undefined, priority: undefined, keyword: '' })
const filteredOrders = computed(() =>
workOrders.value.filter(o =>
(!orderFilter.status || o.status === orderFilter.status) &&
(!orderFilter.priority || o.priority === orderFilter.priority) &&
(!orderFilter.keyword || o.orderNo.includes(orderFilter.keyword) || o.productName.includes(orderFilter.keyword))
)
)
/* ===================== 工单 新增/编辑 ===================== */
const orderModalOpen = ref(false)
const orderFormRef = ref(null)
const editingOrderId = ref(null)
const orderModalTitle = computed(() => editingOrderId.value === null ? '新建工单' : '编辑工单')
function emptyOrderForm() {
return {
orderNo: '', productName: '', productCode: '', spec: '',
quantity: 1, unit: '件', priority: '中', type: '常规', status: '待排产',
line: undefined, planStart: '', planEnd: '',
bom: [{ name: '', code: '', qty: 1, unit: '' }],
qualityStandard: '', remark: '',
}
}
const orderForm = reactive(emptyOrderForm())
const orderRules = {
orderNo: [{ required: true, message: '请输入工单号' }],
productName: [{ required: true, message: '请输入产品名称' }],
productCode: [{ required: true, message: '请输入产品编码' }],
quantity: [{ required: true, message: '请输入数量' }],
unit: [{ required: true, message: '请输入单位' }],
type: [{ required: true, message: '请选择类型' }],
priority: [{ required: true, message: '请选择优先级' }],
status: [{ required: true, message: '请选择状态' }],
planStart: [{ required: true, message: '请选择计划开始' }],
planEnd: [{ required: true, message: '请选择计划结束' }],
}
function openOrderModal(record) {
if (record) {
editingOrderId.value = record.id
Object.assign(orderForm, {
orderNo: record.orderNo, productName: record.productName, productCode: record.productCode,
spec: record.spec, quantity: record.quantity, unit: record.unit, priority: record.priority,
type: record.type, status: record.status, line: record.line,
planStart: record.planStart, planEnd: record.planEnd,
bom: record.bom.map(b => ({ ...b })),
qualityStandard: record.qualityStandard, remark: record.remark,
})
} else {
editingOrderId.value = null
Object.assign(orderForm, emptyOrderForm())
}
orderModalOpen.value = true
}
async function submitOrder() {
await orderFormRef.value.validate()
if (editingOrderId.value === null) {
production.addWorkOrder({ ...orderForm })
message.success('工单创建成功')
} else {
const original = production.orderMap[editingOrderId.value]
production.updateWorkOrder({
id: editingOrderId.value,
...orderForm,
actualStart: original.actualStart,
actualEnd: original.actualEnd,
completedQty: original.completedQty,
})
message.success('工单已更新')
}
orderModalOpen.value = false
}
/* BOM 行操作 */
function addBomRow() { orderForm.bom.push({ name: '', code: '', qty: 1, unit: '' }) }
function removeBomRow(idx) { orderForm.bom.splice(idx, 1) }
/* ===================== 工单详情 / 报工 ===================== */
const orderDetailOpen = ref(false)
const currentOrder = ref(null)
function openOrderDetail(record) {
currentOrder.value = record
orderDetailOpen.value = true
}
const progressModalOpen = ref(false)
const progressForm = reactive({ completedQty: 0, finished: false })
function openProgressModal(record) {
currentOrder.value = record
progressForm.completedQty = record.completedQty
progressForm.finished = record.status === '已完工'
progressModalOpen.value = true
}
function submitProgress() {
production.updateCompletedQty(currentOrder.value.id, progressForm.completedQty)
if (progressForm.finished) {
const order = production.orderMap[currentOrder.value.id]
order.status = '已完工'
order.actualEnd = dayjs().format('YYYY-MM-DD')
order.completedQty = order.quantity
}
message.success('报工成功')
progressModalOpen.value = false
}
/* ===================== 工序 新增/编辑 ===================== */
const processModalOpen = ref(false)
const processFormRef = ref(null)
const editingProcessId = ref(null)
const processModalTitle = computed(() => editingProcessId.value === null ? '新增工序排程' : '编辑工序排程')
function emptyProcessForm() {
return {
workOrderId: undefined, processSeq: 1, processName: '', line: undefined, operator: '',
planStart: '', planEnd: '', actualStart: '', actualEnd: '',
status: '未开始', durationHours: 8, actualHours: 0,
resources: [{ name: '', type: '设备' }], remark: '',
}
}
const processForm = reactive(emptyProcessForm())
const processRules = {
workOrderId: [{ required: true, message: '请选择关联工单' }],
processSeq: [{ required: true, message: '请输入工序序号' }],
processName: [{ required: true, message: '请输入工序名称' }],
line: [{ required: true, message: '请选择产线' }],
operator: [{ required: true, message: '请输入操作工' }],
planStart: [{ required: true, message: '请选择计划开始' }],
planEnd: [{ required: true, message: '请选择计划结束' }],
durationHours: [{ required: true, message: '请输入计划工时' }],
status: [{ required: true, message: '请选择状态' }],
}
function openProcessModal(record) {
if (record) {
editingProcessId.value = record.id
Object.assign(processForm, {
workOrderId: record.workOrderId, processSeq: record.processSeq, processName: record.processName,
line: record.line, operator: record.operator,
planStart: record.planStart, planEnd: record.planEnd,
actualStart: record.actualStart, actualEnd: record.actualEnd,
status: record.status, durationHours: record.durationHours, actualHours: record.actualHours,
resources: record.resources.map(r => ({ ...r })), remark: record.remark,
})
} else {
editingProcessId.value = null
Object.assign(processForm, emptyProcessForm())
}
processModalOpen.value = true
}
/* 选择工单时自动带出产线 */
function onOrderChange(orderId) {
const opt = production.orderOptions.find(o => o.value === orderId)
if (opt && !processForm.line) processForm.line = opt.line
}
async function submitProcess() {
await processFormRef.value.validate()
// 自动生成排程号
const scheduleNo = `PS-${dayjs().format('YYYYMMDD')}-${String(Math.floor(Math.random() * 900) + 100)}`
if (editingProcessId.value === null) {
production.addProcessSchedule({ scheduleNo, ...processForm })
message.success('工序排程已创建')
} else {
production.updateProcessSchedule({ id: editingProcessId.value, ...processForm })
message.success('工序排程已更新')
}
processModalOpen.value = false
}
/* 资源行操作 */
function addResource() { processForm.resources.push({ name: '', type: '设备' }) }
function removeResource(idx) { processForm.resources.splice(idx, 1) }
/* ===================== 报修/保养 ===================== */
const maintenanceModalOpen = ref(false)
const maintenanceFormRef = ref(null)
function emptyMaintenanceForm() {
return { equipmentNo: undefined, type: '故障维修', handler: '', startTime: '', description: '', parts: [] }
}
const maintenanceForm = reactive(emptyMaintenanceForm())
const maintenanceRules = {
equipmentNo: [{ required: true, message: '请选择设备' }],
type: [{ required: true, message: '请选择类型' }],
handler: [{ required: true, message: '请输入处理人' }],
startTime: [{ required: true, message: '请选择开始时间' }],
description: [{ required: true, message: '请填写描述' }],
}
function openMaintenanceModal() {
Object.assign(maintenanceForm, emptyMaintenanceForm())
maintenanceModalOpen.value = true
}
async function submitMaintenance() {
await maintenanceFormRef.value.validate()
production.addMaintenanceRecord({ ...maintenanceForm })
message.success('维修/保养登记成功,设备状态已同步更新')
maintenanceModalOpen.value = false
}
/* 配件行操作 */
function addPart() { maintenanceForm.parts.push({ name: '', qty: 1 }) }
function removePart(idx) { maintenanceForm.parts.splice(idx, 1) }
/* ===================== 完成维修 ===================== */
const completeModalOpen = ref(false)
const completingRecord = ref(null)
const completeForm = reactive({ endTime: '', durationHours: 0, cost: 0 })
function openCompleteModal(record) {
completingRecord.value = record
completeForm.endTime = dayjs().format('YYYY-MM-DD HH:mm')
completeForm.durationHours = 1
completeForm.cost = 0
completeModalOpen.value = true
}
function submitComplete() {
production.completeMaintenanceRecord(completingRecord.value.id, { ...completeForm })
message.success('维修已完成,设备已恢复空闲状态')
completeModalOpen.value = false
}
</script>
<style scoped>
/* 外层包裹:占满父容器剩余高度,自身不滚动 */
.prod-wrap {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
/* antd Tabs 撑满包裹层:Tab 头固定,内容区独立滚动,外层不出滚动条 */
.prod-wrap :deep(.ant-tabs) {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.prod-wrap :deep(.ant-tabs-nav) {
margin-bottom: 12px;
flex-shrink: 0;
}
.prod-wrap :deep(.ant-tabs-content-holder) {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
/* Firefox:细滚动条 */
scrollbar-width: thin;
scrollbar-color: rgba(128, 128, 128, 0.35) transparent;
}
/* ------ 长椭圆形(胶囊形)滚动条:WebKit 内核(Chrome/Edge/Safari)------ */
.prod-wrap :deep(.ant-tabs-content-holder::-webkit-scrollbar) {
width: 8px;
height: 8px;
}
/* 轨道:透明,视觉上只留滑块 */
.prod-wrap :deep(.ant-tabs-content-holder::-webkit-scrollbar-track) {
background: transparent;
}
/* 滑块:两端完全圆角 => 长椭圆胶囊形 */
.prod-wrap :deep(.ant-tabs-content-holder::-webkit-scrollbar-thumb) {
background: rgba(128, 128, 128, 0.35);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
transition: background 0.2s;
}
.prod-wrap :deep(.ant-tabs-content-holder::-webkit-scrollbar-thumb:hover) {
background: rgba(100, 108, 255, 0.6);
background-clip: padding-box;
}
/* 角落(横竖滚动条交汇处)透明 */
.prod-wrap :deep(.ant-tabs-content-holder::-webkit-scrollbar-corner) {
background: transparent;
}
.prod-wrap :deep(.ant-tabs-content),
.prod-wrap :deep(.ant-tabs-tabpane) {
height: 100%;
}
/* 统计行紧凑排列,不随内容滚动 */
.stat-row { margin-bottom: 0.8rem; flex-shrink: 0; }
.add-col { display: flex; align-items: center; }
.filter-card { margin-bottom: 1rem; }
.qty-text { font-size: 0.78rem; color: var(--text-secondary); }
.muted { color: var(--text-secondary); font-size: 0.82rem; }
/* 甘特时间线 */
.timeline-card { margin-bottom: 1rem; }
.gantt-timeline { padding-left: 0; }
.tl-item { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 0.6rem 0.8rem; }
.tl-head { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 4px; }
.tl-meta { font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 6px; }
.tl-progress { margin-bottom: 6px; }
.tl-resources { display: flex; gap: 4px; flex-wrap: wrap; }
.res-tag { font-size: 0.72rem; }
/* 设备卡片 */
.eq-card { height: 100%; }
.eq-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 0.6rem; }
.eq-name { font-weight: 600; font-size: 0.95rem; }
.eq-no { font-size: 0.75rem; color: var(--text-secondary); }
.eq-status { font-size: 0.72rem; }
.eq-metrics { text-align: center; margin-bottom: 0.5rem; }
.metric { font-size: 1.1rem; font-weight: 600; color: var(--primary); }
.metric .unit { font-size: 0.7rem; color: var(--text-secondary); font-weight: normal; margin-left: 2px; }
.metric-label { font-size: 0.7rem; color: var(--text-secondary); }
.eq-foot { display: flex; justify-content: space-between; font-size: 0.75rem; color: var(--text-secondary); margin-top: 0.5rem; flex-wrap: wrap; gap: 4px; }
/* 动态行(BOM / 资源 / 配件) */
.bom-row { margin-bottom: 8px; }
.bom-row .ant-input-number,
.bom-row .ant-select { width: 100%; }
.progress-text { color: var(--text-secondary); font-size: 0.85rem; margin-top: 4px; }
</style>