设备台账补齐新字段,甘特排产支持指定设备
设备维护增加系统品牌、网口、位置和 IP;排产甘特图可选择设备并展示未完成任务。派工待派数量按已派工计算,兼容补料。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
45e6856b7a
commit
c8b41fffaa
@ -11,13 +11,21 @@ export interface Device{
|
|||||||
sectionName?:string,
|
sectionName?:string,
|
||||||
spec:string,
|
spec:string,
|
||||||
manufacturer:string,
|
manufacturer:string,
|
||||||
manufactureDate:string,
|
manufactureDate?: string | number | null,
|
||||||
workshopId:number,
|
workshopId:number,
|
||||||
remark:string,
|
remark:string,
|
||||||
status:number,
|
status:number,
|
||||||
deviceFlag?:string,
|
deviceFlag?:string,
|
||||||
/** 新代 SynFactory equipID */
|
/** 新代 SynFactory equipID */
|
||||||
synEquipId?: number | null,
|
synEquipId?: number | null,
|
||||||
|
/** 系统品牌 */
|
||||||
|
systemBrand?: string | null,
|
||||||
|
/** 是否有网口 0否 1是 */
|
||||||
|
hasNetworkPort?: number | null,
|
||||||
|
/** 位置 */
|
||||||
|
location?: string | null,
|
||||||
|
/** IP */
|
||||||
|
ip?: string | null,
|
||||||
createTime:string,
|
createTime:string,
|
||||||
updateTime:string
|
updateTime:string
|
||||||
abnormalRecordList:[],
|
abnormalRecordList:[],
|
||||||
@ -78,7 +86,47 @@ export interface DeviceRealtimeVO {
|
|||||||
workshopName?: string
|
workshopName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeviceScheduleItemVO {
|
||||||
|
id: number
|
||||||
|
deviceCode?: string
|
||||||
|
deviceName?: string
|
||||||
|
sectionName?: string
|
||||||
|
deviceFlag?: string
|
||||||
|
deviceFlagLabel?: string
|
||||||
|
runStatus?: 'busy' | 'idle' | string
|
||||||
|
runStatusLabel?: string
|
||||||
|
loadPercent?: number
|
||||||
|
taskCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceScheduleCapableVO {
|
||||||
|
sectionId?: number | null
|
||||||
|
sectionName?: string | null
|
||||||
|
matchMode?: string
|
||||||
|
matchHint?: string
|
||||||
|
devices?: DeviceScheduleItemVO[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeviceScheduleTaskVO {
|
||||||
|
id?: number | string
|
||||||
|
deviceId?: string
|
||||||
|
orderNo?: string
|
||||||
|
materialCode?: string
|
||||||
|
materialName?: string
|
||||||
|
processName?: string
|
||||||
|
operNumber?: number | string | null
|
||||||
|
quantity?: number | null
|
||||||
|
assingStatus?: number | null
|
||||||
|
pauseStatus?: number | null
|
||||||
|
statusKey?: string
|
||||||
|
statusText?: string
|
||||||
|
statusLabel?: string
|
||||||
|
planStartTime?: string | null
|
||||||
|
planFinishTime?: string | null
|
||||||
|
locked?: boolean
|
||||||
|
isCurrent?: boolean
|
||||||
|
durationMs?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface DocumentEntity {
|
export interface DocumentEntity {
|
||||||
id?:number,
|
id?:number,
|
||||||
@ -192,5 +240,23 @@ export const deviceApi = {
|
|||||||
url:`/biz/device/getByid/${deviceId}`,
|
url:`/biz/device/getByid/${deviceId}`,
|
||||||
method:"get"
|
method:"get"
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 按工序工作中心匹配可执行设备/工位 */
|
||||||
|
scheduleCapable(params: { workCenterName?: string | null; departmentName?: string | null }) {
|
||||||
|
return request<DeviceScheduleCapableVO>({
|
||||||
|
url: '/biz/device/schedule-capable',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 指定设备当前未完成任务 */
|
||||||
|
scheduleTasks(deviceId: number) {
|
||||||
|
return request<DeviceScheduleTaskVO[]>({
|
||||||
|
url: '/biz/device/schedule-tasks',
|
||||||
|
method: 'get',
|
||||||
|
params: { deviceId }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,10 +102,11 @@ export function exportNccode(params:any) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//加载全部NC代码
|
// 加载 NC:传设备 id 时按设备查绑定文档;不传则拉全部 NC 代码库
|
||||||
export function loadAllNcCode(id:number) {
|
export function loadAllNcCode(deviceId?: string | number) {
|
||||||
|
const id = deviceId != null && deviceId !== '' ? `/${deviceId}` : ''
|
||||||
return request({
|
return request({
|
||||||
url: `biz/ncCode/loadAllNcCode/${id}`,
|
url: `/biz/ncCode/loadAllNcCode${id}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,6 +93,10 @@ export interface KingdeeProcessRoute {
|
|||||||
qualityInspection?: number | null
|
qualityInspection?: number | null
|
||||||
/** 是否入库 1是 0否(后端:仅末序为 1) */
|
/** 是否入库 1是 0否(后端:仅末序为 1) */
|
||||||
storageEntry?: number | null
|
storageEntry?: number | null
|
||||||
|
/** 排产指定设备/工位 */
|
||||||
|
deviceId?: number | string | null
|
||||||
|
deviceCode?: string | null
|
||||||
|
deviceName?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 解析工序名称,兼容旧字段及后端误映射 */
|
/** 解析工序名称,兼容旧字段及后端误映射 */
|
||||||
|
|||||||
@ -73,7 +73,7 @@
|
|||||||
:data="tableData"
|
:data="tableData"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:row-key="(row) => row.id"
|
:row-key="(row) => row.id"
|
||||||
:scroll-x="1200"
|
:scroll-x="1800"
|
||||||
v-model:checked-row-keys="selectedIds"
|
v-model:checked-row-keys="selectedIds"
|
||||||
/>
|
/>
|
||||||
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
||||||
@ -96,8 +96,8 @@
|
|||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
<!-- 新增/编辑弹窗 -->
|
<!-- 新增/编辑弹窗 -->
|
||||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
|
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 640px">
|
||||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
|
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="110px">
|
||||||
<n-form-item label="设备名称" path="deviceName">
|
<n-form-item label="设备名称" path="deviceName">
|
||||||
<n-input v-model:value="formData.deviceName" placeholder="请输入设备名称"/>
|
<n-input v-model:value="formData.deviceName" placeholder="请输入设备名称"/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@ -143,8 +143,23 @@
|
|||||||
<n-form-item label="生产厂家" path="manufacturer">
|
<n-form-item label="生产厂家" path="manufacturer">
|
||||||
<n-input v-model:value="formData.manufacturer" placeholder="请输入生产厂家"/>
|
<n-input v-model:value="formData.manufacturer" placeholder="请输入生产厂家"/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
|
<n-form-item label="系统品牌" path="systemBrand">
|
||||||
|
<n-input v-model:value="formData.systemBrand" placeholder="请输入系统品牌"/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="网口" path="hasNetworkPort">
|
||||||
|
<n-switch v-model:value="formData.hasNetworkPort" :checked-value="1" :unchecked-value="0">
|
||||||
|
<template #checked>是</template>
|
||||||
|
<template #unchecked>否</template>
|
||||||
|
</n-switch>
|
||||||
|
</n-form-item>
|
||||||
<n-form-item label="出厂日期" path="manufactureDate">
|
<n-form-item label="出厂日期" path="manufactureDate">
|
||||||
<n-date-picker v-model:value="formData.manufactureDate" type="datetime" clearable style="width: 100%"/>
|
<n-date-picker v-model:value="formData.manufactureDate" type="month" clearable style="width: 100%"/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="位置" path="location">
|
||||||
|
<n-input v-model:value="formData.location" placeholder="请输入位置"/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item label="IP" path="ip">
|
||||||
|
<n-input v-model:value="formData.ip" placeholder="请输入IP地址"/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item label="备注" path="remark">
|
<n-form-item label="备注" path="remark">
|
||||||
<n-input v-model:value="formData.remark" type="textarea" placeholder="请输入备注"/>
|
<n-input v-model:value="formData.remark" type="textarea" placeholder="请输入备注"/>
|
||||||
@ -445,6 +460,10 @@ const defaultFormData: Device = {
|
|||||||
spec: '',
|
spec: '',
|
||||||
manufacturer: '',
|
manufacturer: '',
|
||||||
manufactureDate: undefined,
|
manufactureDate: undefined,
|
||||||
|
systemBrand: '',
|
||||||
|
hasNetworkPort: 0,
|
||||||
|
location: '',
|
||||||
|
ip: '',
|
||||||
workshopId: '',
|
workshopId: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
status: 0,
|
status: 0,
|
||||||
@ -497,7 +516,24 @@ const columns: DataTableColumns<Device> = [
|
|||||||
{title: '所属工段', key: 'sectionName'},
|
{title: '所属工段', key: 'sectionName'},
|
||||||
{title: '设备型号', key: 'spec'},
|
{title: '设备型号', key: 'spec'},
|
||||||
{title: '生产厂家', key: 'manufacturer'},
|
{title: '生产厂家', key: 'manufacturer'},
|
||||||
{title: '出厂日期', key: 'manufactureDate'},
|
{title: '系统品牌', key: 'systemBrand', width: 110,
|
||||||
|
render: (row) => row.systemBrand || '-'
|
||||||
|
},
|
||||||
|
{title: '网口', key: 'hasNetworkPort', width: 80,
|
||||||
|
render: (row) => {
|
||||||
|
const yes = Number(row.hasNetworkPort) === 1
|
||||||
|
return h(NTag, {type: yes ? 'success' : 'default', size: 'small'}, {default: () => yes ? '是' : '否'})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{title: '出厂日期', key: 'manufactureDate', width: 110,
|
||||||
|
render: (row) => formatYearMonth(row.manufactureDate)
|
||||||
|
},
|
||||||
|
{title: '位置', key: 'location', width: 120,
|
||||||
|
render: (row) => row.location || '-'
|
||||||
|
},
|
||||||
|
{title: 'IP', key: 'ip', width: 130,
|
||||||
|
render: (row) => row.ip || '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态', key: 'status',
|
title: '状态', key: 'status',
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
@ -650,11 +686,29 @@ function goRealtimeBoard() {
|
|||||||
router.push('/biz/device/board')
|
router.push('/biz/device/board')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatYearMonth(val?: string | number | null) {
|
||||||
|
if (val == null || val === '') return '-'
|
||||||
|
const s = String(val)
|
||||||
|
if (/^\d{4}-\d{2}/.test(s)) return s.slice(0, 7)
|
||||||
|
const d = new Date(typeof val === 'number' ? val : s)
|
||||||
|
if (Number.isNaN(d.getTime())) return '-'
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function toMonthTimestamp(val: any): number | null {
|
||||||
|
if (val == null || val === '') return null
|
||||||
|
if (typeof val === 'number') return val
|
||||||
|
const d = new Date(String(val).replace(' ', 'T'))
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
// 编辑
|
// 编辑
|
||||||
function handleEdit(row: Device) {
|
function handleEdit(row: Device) {
|
||||||
Object.assign(formData, defaultFormData)
|
Object.assign(formData, defaultFormData)
|
||||||
modalTitle.value = '编辑设备表'
|
modalTitle.value = '编辑设备表'
|
||||||
Object.assign(formData, row)
|
Object.assign(formData, row)
|
||||||
|
formData.manufactureDate = toMonthTimestamp(row.manufactureDate) as any
|
||||||
|
formData.hasNetworkPort = Number(row.hasNetworkPort) === 1 ? 1 : 0
|
||||||
const userIdList = []
|
const userIdList = []
|
||||||
if (row.deviceManager) {
|
if (row.deviceManager) {
|
||||||
let manager = row.deviceManager.split(',');
|
let manager = row.deviceManager.split(',');
|
||||||
@ -674,7 +728,9 @@ async function handleSubmit() {
|
|||||||
const submitData = {...formData} as Device
|
const submitData = {...formData} as Device
|
||||||
|
|
||||||
if (typeof submitData.manufactureDate === 'number') {
|
if (typeof submitData.manufactureDate === 'number') {
|
||||||
submitData.manufactureDate = new Date(submitData.manufactureDate).toISOString().slice(0, 19).replace('T', ' ')
|
const d = new Date(submitData.manufactureDate)
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
submitData.manufactureDate = `${d.getFullYear()}-${month}-01 00:00:00`
|
||||||
}
|
}
|
||||||
if (submitData.id) {
|
if (submitData.id) {
|
||||||
await deviceApi.update(submitData)
|
await deviceApi.update(submitData)
|
||||||
|
|||||||
@ -378,10 +378,19 @@
|
|||||||
<span class="pgClass">已完成:</span>
|
<span class="pgClass">已完成:</span>
|
||||||
<span>{{dispatchform.completedQty ?? 0}}</span>
|
<span>{{dispatchform.completedQty ?? 0}}</span>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<span class="pgClass">已派工:</span>
|
||||||
|
<span>{{dispatchform.assignedQty ?? 0}}</span>
|
||||||
|
</n-gi>
|
||||||
|
</n-grid>
|
||||||
|
<n-grid :cols="2">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<span class="pgClass">本次待派:</span>
|
<span class="pgClass">本次待派:</span>
|
||||||
<span style="color: #2080f0; font-weight: 600">{{dispatchform.remainQty}}</span>
|
<span style="color: #2080f0; font-weight: 600">{{dispatchform.remainQty}}</span>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<span class="hint-inline" style="color:#999;font-size:12px">待派 = 计划 − 已派工</span>
|
||||||
|
</n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-grid :cols="2">
|
<n-grid :cols="2">
|
||||||
<n-gi>
|
<n-gi>
|
||||||
@ -758,6 +767,7 @@ const dispatchLoading = ref(false)
|
|||||||
const dispatchformRef = ref()
|
const dispatchformRef = ref()
|
||||||
const dispatchform = reactive<any>({
|
const dispatchform = reactive<any>({
|
||||||
id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null,
|
id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null,
|
||||||
|
completedQty: 0, assignedQty: 0, remainQty: 0,
|
||||||
list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }],
|
list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }],
|
||||||
})
|
})
|
||||||
const dispatchrules = {}
|
const dispatchrules = {}
|
||||||
@ -1326,7 +1336,12 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
|||||||
|
|
||||||
const planQty = Number(entity.quantity) || 0
|
const planQty = Number(entity.quantity) || 0
|
||||||
const completedQty = Number(process.completedQty) || 0
|
const completedQty = Number(process.completedQty) || 0
|
||||||
const remainQty = Math.max(0, planQty - completedQty)
|
// 与后端 checkOverProcessNum 一致:可派 = 计划 − 已派工数量(不是计划 − 已完成)
|
||||||
|
const assignedQty = (process.assignWorkList ?? []).reduce(
|
||||||
|
(s, a) => s + (Number(a.quantity) || 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const remainQty = Math.max(0, planQty - assignedQty)
|
||||||
dispatchmodal.value = true
|
dispatchmodal.value = true
|
||||||
dispatchformRef.value?.restoreValidation()
|
dispatchformRef.value?.restoreValidation()
|
||||||
dispatchform.id = entity.id
|
dispatchform.id = entity.id
|
||||||
@ -1337,12 +1352,13 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
|||||||
// quantity 保持工序计划总数,提交时不能改掉计划数量
|
// quantity 保持工序计划总数,提交时不能改掉计划数量
|
||||||
dispatchform.quantity = entity.quantity
|
dispatchform.quantity = entity.quantity
|
||||||
dispatchform.completedQty = completedQty
|
dispatchform.completedQty = completedQty
|
||||||
|
dispatchform.assignedQty = assignedQty
|
||||||
dispatchform.remainQty = remainQty
|
dispatchform.remainQty = remainQty
|
||||||
dispatchform.orderItemId = entity.orderItemId
|
dispatchform.orderItemId = entity.orderItemId
|
||||||
dispatchform.sort = entity.sort
|
dispatchform.sort = entity.sort
|
||||||
dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null
|
dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null
|
||||||
dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null
|
dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null
|
||||||
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty+'' || '',ncId:'' }]
|
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty > 0 ? String(remainQty) : '', ncId: '' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
function addpgnum() {
|
function addpgnum() {
|
||||||
@ -1360,17 +1376,19 @@ function dispatchSubmit() {
|
|||||||
})
|
})
|
||||||
const remainQty = Number(dispatchform.remainQty)
|
const remainQty = Number(dispatchform.remainQty)
|
||||||
if (remainQty <= 0) {
|
if (remainQty <= 0) {
|
||||||
message.warning('当前工序已全部完成,无需再派工')
|
// 计划已派满时仍可能因补料继续派工,超量由后端校验
|
||||||
return
|
message.warning('当前工序计划数量已派满,若因补料继续派工请确认数量')
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchformRef.value?.validate((errors: any) => {
|
dispatchformRef.value?.validate((errors: any) => {
|
||||||
// Naive UI:有 errors 表示校验失败
|
// Naive UI:有 errors 表示校验失败
|
||||||
if (errors) return
|
if (errors) return
|
||||||
|
// 补料后可超过「计划−已派」待派数量,超量由后端校验
|
||||||
// if (total !== remainQty) {
|
// if (total !== remainQty) {
|
||||||
// message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0})`, {
|
// message.error(
|
||||||
// duration: 4000,
|
// `派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已派 ${dispatchform.assignedQty ?? 0},已完成 ${dispatchform.completedQty ?? 0})`,
|
||||||
// })
|
// { duration: 4000 },
|
||||||
|
// )
|
||||||
// return
|
// return
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|||||||
@ -183,7 +183,8 @@
|
|||||||
</n-spin>
|
</n-spin>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<n-space justify="end">
|
<n-space justify="end" align="center">
|
||||||
|
<span v-if="!canConfirm && confirmDisabledReason" class="confirm-hint">{{ confirmDisabledReason }}</span>
|
||||||
<n-button @click="visible = false">取消</n-button>
|
<n-button @click="visible = false">取消</n-button>
|
||||||
<n-button type="primary" :loading="submitting" :disabled="!canConfirm" @click="confirmSplit">
|
<n-button type="primary" :loading="submitting" :disabled="!canConfirm" @click="confirmSplit">
|
||||||
确认拆分
|
确认拆分
|
||||||
@ -301,36 +302,55 @@ function onKeepQtyUpdate(v: number | null) {
|
|||||||
/**
|
/**
|
||||||
* 等量拆分:把整单计划数量均分成 N 份(含原单)。
|
* 等量拆分:把整单计划数量均分成 N 份(含原单)。
|
||||||
* quantities = [原单保留, 新单1, 新单2, ...]
|
* quantities = [原单保留, 新单1, 新单2, ...]
|
||||||
|
* 若均分后第1份 < 不可拆数量,则从后面的新单挪量补足第1份(每份新单至少留 1)。
|
||||||
*/
|
*/
|
||||||
const equalQuantities = computed(() => {
|
const equalQuantities = computed(() => {
|
||||||
const q = planQty.value
|
const q = Number(planQty.value) || 0
|
||||||
const n = equalCount.value
|
const n = Number(equalCount.value) || 0
|
||||||
if (!q || !n || n < 2 || q < n) return [] as number[]
|
const inv = Number(unsplittableQty.value) || 0
|
||||||
|
if (q < 2 || n < 2 || q < n) return [] as number[]
|
||||||
|
|
||||||
const base = Math.floor(q / n)
|
const base = Math.floor(q / n)
|
||||||
const rem = q % n
|
const rem = q % n
|
||||||
const list = Array.from({ length: n }, () => base)
|
const list = Array.from({ length: n }, () => base)
|
||||||
// 余数加在最后一份,避免中间份变成 0
|
|
||||||
list[n - 1] = base + rem
|
list[n - 1] = base + rem
|
||||||
if (list.some((v) => v <= 0)) return []
|
|
||||||
// 第1份必须盖住不可拆(已进入生产)数量
|
if (list[0] < inv) {
|
||||||
if (list[0] < unsplittableQty.value) return []
|
let need = inv - list[0]
|
||||||
|
list[0] = inv
|
||||||
|
for (let i = n - 1; i >= 1 && need > 0; i--) {
|
||||||
|
const take = Math.min(Math.max(list[i] - 1, 0), need)
|
||||||
|
list[i] -= take
|
||||||
|
need -= take
|
||||||
|
}
|
||||||
|
if (need > 0) return [] as number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.some((v) => v <= 0)) return [] as number[]
|
||||||
|
if (list.reduce((s, v) => s + v, 0) !== q) return [] as number[]
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 在「第1份 ≥ 不可拆」前提下,最多能拆成几份 */
|
/** 在「第1份可覆盖不可拆、且每份新单≥1」前提下,最多能拆成几份 */
|
||||||
const maxEqualParts = computed(() => {
|
const maxEqualParts = computed(() => {
|
||||||
const q = planQty.value
|
const q = Number(planQty.value) || 0
|
||||||
const inv = unsplittableQty.value
|
const inv = Number(unsplittableQty.value) || 0
|
||||||
if (q < 2) return 2
|
if (q < 2) return 2
|
||||||
let max = Math.min(q, Math.max(2, q - inv)) // 至少留 1 给某新单时可拆上限粗估
|
// 新单合计最多拆出 q - max(inv, 1),且至少 1 个新单
|
||||||
// 收紧:floor(q/n) >= inv(余数在末份时首份=floor)
|
let max = Math.min(q, Math.max(2, q - Math.max(inv, 0)))
|
||||||
while (max > 2 && Math.floor(q / max) < inv) {
|
|
||||||
max -= 1
|
|
||||||
}
|
|
||||||
// 还要保证至少能拆出 1 个新单:首份 < q
|
|
||||||
while (max > 2) {
|
while (max > 2) {
|
||||||
const base = Math.floor(q / max)
|
const base = Math.floor(q / max)
|
||||||
if (base >= Math.max(1, inv) && base < q) break
|
const rem = q % max
|
||||||
|
const first = base
|
||||||
|
const last = base + rem
|
||||||
|
// 模拟:若 first < inv,需从后面挪;后面共 (max-1) 份,每份最多让出 (qty-1)
|
||||||
|
let need = Math.max(0, inv - first)
|
||||||
|
let canGive = 0
|
||||||
|
for (let i = 1; i < max; i++) {
|
||||||
|
const qty = i === max - 1 ? last : base
|
||||||
|
canGive += Math.max(qty - 1, 0)
|
||||||
|
}
|
||||||
|
if (need <= canGive && base + (need > 0 ? need : 0) < q) break
|
||||||
max -= 1
|
max -= 1
|
||||||
}
|
}
|
||||||
return Math.max(2, max)
|
return Math.max(2, max)
|
||||||
@ -340,8 +360,8 @@ const equalHint = computed(() => {
|
|||||||
const list = equalQuantities.value
|
const list = equalQuantities.value
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
if (planQty.value < equalCount.value) return `计划数量不足以分成 ${equalCount.value} 份`
|
if (planQty.value < equalCount.value) return `计划数量不足以分成 ${equalCount.value} 份`
|
||||||
if (equalCount.value >= 2 && Math.floor(planQty.value / equalCount.value) < unsplittableQty.value) {
|
if (equalCount.value >= 2) {
|
||||||
return `第1份无法覆盖不可拆数量 ${unsplittableQty.value},请减少份数`
|
return `无法在覆盖不可拆数量 ${unsplittableQty.value} 的前提下分成 ${equalCount.value} 份,请减少份数或改用「指定数量」`
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -599,7 +619,6 @@ const canConfirm = computed(() => {
|
|||||||
if (!preview.value?.canSplit || submitting.value) return false
|
if (!preview.value?.canSplit || submitting.value) return false
|
||||||
|
|
||||||
if (mode.value === 'equal') {
|
if (mode.value === 'equal') {
|
||||||
// 等量:整单均分,列表本身已含原单保留
|
|
||||||
if (!equalQuantities.value.length) return false
|
if (!equalQuantities.value.length) return false
|
||||||
} else {
|
} else {
|
||||||
if (keepQty.value == null || keepQty.value < unsplittableQty.value) return false
|
if (keepQty.value == null || keepQty.value < unsplittableQty.value) return false
|
||||||
@ -613,6 +632,30 @@ const canConfirm = computed(() => {
|
|||||||
return assembleValid.value
|
return assembleValid.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/** 按钮禁用原因,便于排查「点不了」 */
|
||||||
|
const confirmDisabledReason = computed(() => {
|
||||||
|
if (!preview.value) return '正在加载预览'
|
||||||
|
if (!preview.value.canSplit) return preview.value.reason || '当前工单不允许拆单'
|
||||||
|
if (submitting.value) return ''
|
||||||
|
if (mode.value === 'equal' && !equalQuantities.value.length) {
|
||||||
|
return '等量拆分数量无效,请调整份数或改用指定数量'
|
||||||
|
}
|
||||||
|
if (mode.value === 'custom') {
|
||||||
|
if (keepQty.value == null || keepQty.value < unsplittableQty.value) return '原单保留数量不能小于不可拆数量'
|
||||||
|
if (splitOutQty.value <= 0) return '没有可拆出的数量'
|
||||||
|
if (customRows.value.some((v) => v == null || v <= 0)) return '请填写每张新工单数量'
|
||||||
|
if (customSum.value !== splitOutQty.value) return `拆出合计须等于 ${splitOutQty.value}`
|
||||||
|
}
|
||||||
|
if (routeMode.value === 'kingdee') {
|
||||||
|
if (!preview.value.materialCode) return '无物料编码,请改用「创建新工艺路线」'
|
||||||
|
if (!routeList.value.length) return '未查到金蝶工艺路线,请改用「创建新工艺路线」'
|
||||||
|
if (!routingNo.value) return '请先选择一条金蝶工艺路线'
|
||||||
|
} else if (!assembleValid.value) {
|
||||||
|
return '请为每道工序选择或填写工序信息'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [props.show, props.planId] as const,
|
() => [props.show, props.planId] as const,
|
||||||
async ([show, planId]) => {
|
async ([show, planId]) => {
|
||||||
@ -661,6 +704,10 @@ async function loadRouteOptions(materialCode?: string | null) {
|
|||||||
if (!materialCode) return
|
if (!materialCode) return
|
||||||
const list = (await processRouteApi.kingdeeSelect(materialCode)) as ProcessRouteSelectOption[]
|
const list = (await processRouteApi.kingdeeSelect(materialCode)) as ProcessRouteSelectOption[]
|
||||||
routeList.value = (Array.isArray(list) ? list : []).filter((r) => !!r?.routingNo)
|
routeList.value = (Array.isArray(list) ? list : []).filter((r) => !!r?.routingNo)
|
||||||
|
// 默认选中第一条,避免「确认拆分」一直灰着
|
||||||
|
if (!routingNo.value && routeList.value.length) {
|
||||||
|
routingNo.value = routeList.value[0].routingNo || null
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
routeList.value = []
|
routeList.value = []
|
||||||
message.error(e?.message || '加载金蝶工艺路线失败')
|
message.error(e?.message || '加载金蝶工艺路线失败')
|
||||||
@ -918,6 +965,13 @@ async function confirmSplit() {
|
|||||||
.sum-line.error {
|
.sum-line.error {
|
||||||
color: #d03050;
|
color: #d03050;
|
||||||
}
|
}
|
||||||
|
.confirm-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #d03050;
|
||||||
|
margin-right: 8px;
|
||||||
|
max-width: 420px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.kingdee-layout {
|
.kingdee-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@ -245,6 +245,11 @@
|
|||||||
<n-input :value="String(dispatchform.completedQty ?? 0)" disabled />
|
<n-input :value="String(dispatchform.completedQty ?? 0)" disabled />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
|
<n-gi>
|
||||||
|
<n-form-item label="已派工">
|
||||||
|
<n-input :value="String(dispatchform.assignedQty ?? 0)" disabled />
|
||||||
|
</n-form-item>
|
||||||
|
</n-gi>
|
||||||
<n-gi>
|
<n-gi>
|
||||||
<n-form-item label="本次待派">
|
<n-form-item label="本次待派">
|
||||||
<n-input :value="String(dispatchform.remainQty ?? 0)" disabled />
|
<n-input :value="String(dispatchform.remainQty ?? 0)" disabled />
|
||||||
@ -867,6 +872,7 @@ let dispatchform = reactive<any>({
|
|||||||
userid:'',
|
userid:'',
|
||||||
quantity:'',
|
quantity:'',
|
||||||
completedQty: 0,
|
completedQty: 0,
|
||||||
|
assignedQty: 0,
|
||||||
remainQty: 0,
|
remainQty: 0,
|
||||||
list:[{
|
list:[{
|
||||||
userId:'',
|
userId:'',
|
||||||
@ -920,7 +926,12 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
|||||||
const entity = processItemToEntity(process, order?.orderItemId)
|
const entity = processItemToEntity(process, order?.orderItemId)
|
||||||
const planQty = Number(entity.quantity) || 0
|
const planQty = Number(entity.quantity) || 0
|
||||||
const completedQty = Number(process.completedQty) || 0
|
const completedQty = Number(process.completedQty) || 0
|
||||||
const remainQty = Math.max(0, planQty - completedQty)
|
// 与后端 checkOverProcessNum 一致:可派 = 计划 − 已派工数量
|
||||||
|
const assignedQty = (process.assignWorkList ?? []).reduce(
|
||||||
|
(s, a) => s + (Number(a.quantity) || 0),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const remainQty = Math.max(0, planQty - assignedQty)
|
||||||
dispatchmodal.value = true
|
dispatchmodal.value = true
|
||||||
dispatchformRef.value?.restoreValidation()
|
dispatchformRef.value?.restoreValidation()
|
||||||
dispatchform.id = entity.id
|
dispatchform.id = entity.id
|
||||||
@ -929,11 +940,12 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
|
|||||||
dispatchform.endTime = entity.endTime || order?.endTime || ''
|
dispatchform.endTime = entity.endTime || order?.endTime || ''
|
||||||
dispatchform.quantity = entity.quantity
|
dispatchform.quantity = entity.quantity
|
||||||
dispatchform.completedQty = completedQty
|
dispatchform.completedQty = completedQty
|
||||||
|
dispatchform.assignedQty = assignedQty
|
||||||
dispatchform.remainQty = remainQty
|
dispatchform.remainQty = remainQty
|
||||||
dispatchform.userid = ''
|
dispatchform.userid = ''
|
||||||
dispatchform.list = [{
|
dispatchform.list = [{
|
||||||
userId: '',
|
userId: '',
|
||||||
quantity: remainQty || '',
|
quantity: remainQty > 0 ? String(remainQty) : '',
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -944,13 +956,13 @@ function dispatchSubmit() {
|
|||||||
})
|
})
|
||||||
const remainQty = Number(dispatchform.remainQty)
|
const remainQty = Number(dispatchform.remainQty)
|
||||||
if (remainQty <= 0) {
|
if (remainQty <= 0) {
|
||||||
message.warning('当前工序已全部完成,无需再派工')
|
message.warning('当前工序已无待派数量(已派工已达计划数量)')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dispatchformRef.value?.validate((v:any) => {
|
dispatchformRef.value?.validate((v:any) => {
|
||||||
if(!v){
|
if(!v){
|
||||||
if(total != remainQty){
|
if(total != remainQty){
|
||||||
message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已完成 ${dispatchform.completedQty ?? 0})`,{
|
message.error(`派工总数量需等于待派数量 ${remainQty}(计划 ${dispatchform.quantity},已派 ${dispatchform.assignedQty ?? 0},已完成 ${dispatchform.completedQty ?? 0})`,{
|
||||||
duration:4000
|
duration:4000
|
||||||
})
|
})
|
||||||
}else{
|
}else{
|
||||||
|
|||||||
@ -37,7 +37,7 @@
|
|||||||
clearable
|
clearable
|
||||||
remote
|
remote
|
||||||
:clear-filter-after-select="false"
|
:clear-filter-after-select="false"
|
||||||
@change="loadNc"
|
@update:value="loadNc"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
@ -48,12 +48,20 @@
|
|||||||
label="数量"
|
label="数量"
|
||||||
:path="`${listname}[${index}].quantity`"
|
:path="`${listname}[${index}].quantity`"
|
||||||
:rule="{
|
:rule="{
|
||||||
required: true,
|
trigger: ['input', 'blur'],
|
||||||
message: `请输入数量`,
|
validator: (_rule: any, value: any) => {
|
||||||
trigger: 'blur',
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return new Error('请输入数量')
|
||||||
|
}
|
||||||
|
const num = Number(value)
|
||||||
|
if (Number.isNaN(num) || num <= 0) {
|
||||||
|
return new Error('请输入有效数量')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<n-input v-model:value="obj.quantity"/>
|
<n-input :value="quantityText" @update:value="onQuantityUpdate" placeholder="请输入数量"/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi :span="10">
|
<n-gi :span="10">
|
||||||
@ -97,7 +105,7 @@
|
|||||||
|
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {ref, reactive} from 'vue'
|
import {ref, reactive, computed, watch} from 'vue'
|
||||||
import {userApi} from '@/api/system'
|
import {userApi} from '@/api/system'
|
||||||
import {sectionApi, type Section} from '@/api/section'
|
import {sectionApi, type Section} from '@/api/section'
|
||||||
import {
|
import {
|
||||||
@ -121,10 +129,26 @@ const emit = defineEmits<{
|
|||||||
update: [index: any, obj: any]
|
update: [index: any, obj: any]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
/** n-input 需要 string;带出的待派数量可能是 number,统一转成字符串可直接通过校验 */
|
||||||
|
const quantityText = computed(() => {
|
||||||
|
const val = props.obj?.quantity
|
||||||
|
if (val === null || val === undefined || val === '') return ''
|
||||||
|
return String(val)
|
||||||
|
})
|
||||||
|
|
||||||
// function updatehand() {
|
function onQuantityUpdate(val: string) {
|
||||||
// emit('update')
|
props.obj.quantity = val
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.obj?.quantity,
|
||||||
|
(val) => {
|
||||||
|
if (typeof val === 'number') {
|
||||||
|
props.obj.quantity = String(val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
function addpgnum() {
|
function addpgnum() {
|
||||||
emit('addhand')
|
emit('addhand')
|
||||||
@ -146,10 +170,6 @@ let ncList = ref<any>([])
|
|||||||
function slehand(v?: any) {
|
function slehand(v?: any) {
|
||||||
loadingRef.value = true
|
loadingRef.value = true
|
||||||
yglist.value.splice(0)
|
yglist.value.splice(0)
|
||||||
// dispatchform.userlist = [{
|
|
||||||
// id:'',
|
|
||||||
// num:''
|
|
||||||
// }]
|
|
||||||
userApi.page({
|
userApi.page({
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 100,
|
pageSize: 100,
|
||||||
@ -163,7 +183,6 @@ function slehand(v?: any) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
console.log(yglist.value, '12')
|
|
||||||
loadingRef.value = false
|
loadingRef.value = false
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
loadingRef.value = false
|
loadingRef.value = false
|
||||||
@ -192,16 +211,19 @@ async function loadSection() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
//加载下发数据
|
/** 按工位/设备加载已绑定 NC/图纸;无设备时清空 */
|
||||||
async function loadNc(row) {
|
async function loadNc(deviceId?: string | number | null) {
|
||||||
const res = await loadAllNcCode(row)
|
props.obj.ncId = null
|
||||||
ncList.value = res.map((n: any) => {
|
ncList.value = []
|
||||||
|
if (deviceId == null || deviceId === '') return
|
||||||
|
const res = await loadAllNcCode(deviceId)
|
||||||
|
const list = Array.isArray(res) ? res : (res as any)?.records ?? (res as any)?.list ?? []
|
||||||
|
ncList.value = list.map((n: any) => {
|
||||||
return {
|
return {
|
||||||
label: n.name,
|
label: n.name || n.ncName || n.ncCode || String(n.id),
|
||||||
value: n.id
|
value: n.id
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log(ncList.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,8 @@
|
|||||||
<span class="meta-divider">|</span>
|
<span class="meta-divider">|</span>
|
||||||
<span>工序 {{ scheduleStats.routeCount }} 道</span>
|
<span>工序 {{ scheduleStats.routeCount }} 道</span>
|
||||||
<span class="meta-divider">|</span>
|
<span class="meta-divider">|</span>
|
||||||
|
<span>已分配设备 {{ scheduleStats.assignedCount }}</span>
|
||||||
|
<span class="meta-divider">|</span>
|
||||||
<span>今日 {{ todayLabel }}</span>
|
<span>今日 {{ todayLabel }}</span>
|
||||||
<span class="meta-divider">|</span>
|
<span class="meta-divider">|</span>
|
||||||
<span class="meta-legend"><i class="legend-today-line" />今日时间轴</span>
|
<span class="meta-legend"><i class="legend-today-line" />今日时间轴</span>
|
||||||
@ -24,10 +26,29 @@
|
|||||||
<div class="gantt-panel">
|
<div class="gantt-panel">
|
||||||
<div class="gantt-panel-head">
|
<div class="gantt-panel-head">
|
||||||
<span class="panel-title">排产明细</span>
|
<span class="panel-title">排产明细</span>
|
||||||
<span class="panel-tip">主产品行展示物料编码与名称,子行展示工序号与工序名称</span>
|
<span class="panel-tip">点击工序条块打开右侧排产抽屉,选择可执行设备/工位</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gantt-container" ref="ganttContainer" />
|
<div class="gantt-container" ref="ganttContainer" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="gantt-legend">
|
||||||
|
<span class="legend-item"><i class="lg unassigned" />未分配</span>
|
||||||
|
<span class="legend-item"><i class="lg pending" />待排产</span>
|
||||||
|
<span class="legend-item"><i class="lg assigned" />已排产</span>
|
||||||
|
<span class="legend-item"><i class="lg producing" />生产中</span>
|
||||||
|
<span class="legend-item"><i class="lg paused" />暂停</span>
|
||||||
|
<span class="legend-item"><i class="lg done" />已完成</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ProcessScheduleDrawer
|
||||||
|
v-model:show="drawerShow"
|
||||||
|
:process="selectedProcess"
|
||||||
|
:order="selectedOrder"
|
||||||
|
:mo-index="selectedMoIndex"
|
||||||
|
:route-index="selectedRouteIndex"
|
||||||
|
:draft-assigned="draftAssigned"
|
||||||
|
@save="handleScheduleSave"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -37,16 +58,23 @@ import { useMessage } from 'naive-ui'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { gantt } from 'dhtmlx-gantt'
|
import { gantt } from 'dhtmlx-gantt'
|
||||||
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
|
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
|
||||||
import { orderProjectApi, normalizeKingdeeProcessRoute, resolveProcessName, type KingdeePrdMo } from '@/api/orderProject'
|
import { orderProjectApi, normalizeKingdeeProcessRoute, resolveProcessName, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
|
||||||
|
import ProcessScheduleDrawer, { type DraftAssignedItem, type ScheduleSavePayload } from './ProcessScheduleDrawer.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const ganttContainer = ref<HTMLElement | null>(null)
|
const ganttContainer = ref<HTMLElement | null>(null)
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
let ganttEventIds: string[] = []
|
let ganttEventIds: string[] = []
|
||||||
|
let dragEventIds: string[] = []
|
||||||
|
|
||||||
const tableData = ref<any[]>([])
|
const tableData = ref<any[]>([])
|
||||||
const projectId = ref<number | null>(null)
|
const projectId = ref<number | null>(null)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
|
const drawerShow = ref(false)
|
||||||
|
const selectedProcess = ref<KingdeeProcessRoute | null>(null)
|
||||||
|
const selectedOrder = ref<KingdeePrdMo | null>(null)
|
||||||
|
const selectedMoIndex = ref(0)
|
||||||
|
const selectedRouteIndex = ref(0)
|
||||||
|
|
||||||
const PROCESS_COLORS = [
|
const PROCESS_COLORS = [
|
||||||
'#409eff', '#67c23a', '#e6a23c', '#909399', '#f56c6c',
|
'#409eff', '#67c23a', '#e6a23c', '#909399', '#f56c6c',
|
||||||
@ -69,15 +97,39 @@ function formatDayScale(date: Date) {
|
|||||||
|
|
||||||
const scheduleStats = computed(() => {
|
const scheduleStats = computed(() => {
|
||||||
let routeCount = 0
|
let routeCount = 0
|
||||||
|
let assignedCount = 0
|
||||||
tableData.value.forEach((mo) => {
|
tableData.value.forEach((mo) => {
|
||||||
routeCount += mo.processRoutes?.length ?? 0
|
const routes = mo.processRoutes ?? []
|
||||||
|
routeCount += routes.length
|
||||||
|
assignedCount += routes.filter((r: any) => r.deviceId).length
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
moCount: tableData.value.length,
|
moCount: tableData.value.length,
|
||||||
routeCount,
|
routeCount,
|
||||||
|
assignedCount,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const draftAssigned = computed<DraftAssignedItem[]>(() => {
|
||||||
|
const list: DraftAssignedItem[] = []
|
||||||
|
tableData.value.forEach((mo, moIndex) => {
|
||||||
|
(mo.processRoutes ?? []).forEach((route: any, routeIndex: number) => {
|
||||||
|
if (!route.deviceId) return
|
||||||
|
list.push({
|
||||||
|
moIndex,
|
||||||
|
routeIndex,
|
||||||
|
orderNo: mo.billNo || mo.productionOrderNo || '',
|
||||||
|
processName: resolveProcessName(route),
|
||||||
|
quantity: mo.quantity,
|
||||||
|
deviceId: route.deviceId,
|
||||||
|
planStartTime: route.planStartTime,
|
||||||
|
planFinishTime: route.planFinishTime,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return list
|
||||||
|
})
|
||||||
|
|
||||||
const todayLabel = computed(() => {
|
const todayLabel = computed(() => {
|
||||||
const d = new Date()
|
const d = new Date()
|
||||||
const pad = (n: number) => String(n).padStart(2, '0')
|
const pad = (n: number) => String(n).padStart(2, '0')
|
||||||
@ -88,6 +140,58 @@ function handleBack() {
|
|||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openProcessDrawer(task: any) {
|
||||||
|
selectedProcess.value = task?._raw || null
|
||||||
|
selectedMoIndex.value = Number(task?._moIndex ?? 0)
|
||||||
|
selectedRouteIndex.value = Number(task?._routeIndex ?? 0)
|
||||||
|
const parent = task?.parent ? gantt.getTask(task.parent) : null
|
||||||
|
selectedOrder.value = parent?._raw || null
|
||||||
|
drawerShow.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistDraftLocal() {
|
||||||
|
sessionStorage.setItem('gantt_schedule_data', JSON.stringify(tableData.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleScheduleSave(payload: ScheduleSavePayload) {
|
||||||
|
const mo = tableData.value[payload.moIndex]
|
||||||
|
const route = mo?.processRoutes?.[payload.routeIndex]
|
||||||
|
if (!route) {
|
||||||
|
message.warning('未找到当前工序,无法保存排产')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
route.deviceId = payload.deviceId
|
||||||
|
route.deviceCode = payload.deviceCode
|
||||||
|
route.deviceName = payload.deviceName
|
||||||
|
route.planStartTime = payload.planStartTime
|
||||||
|
route.planFinishTime = payload.planFinishTime
|
||||||
|
|
||||||
|
payload.draftUpdates.forEach((item) => {
|
||||||
|
const other = tableData.value[item.moIndex]?.processRoutes?.[item.routeIndex]
|
||||||
|
if (!other) return
|
||||||
|
other.planStartTime = item.planStartTime
|
||||||
|
other.planFinishTime = item.planFinishTime
|
||||||
|
})
|
||||||
|
|
||||||
|
if (mo.processRoutes?.length) {
|
||||||
|
const starts = mo.processRoutes
|
||||||
|
.map((r: any) => r.planStartTime)
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
const ends = mo.processRoutes
|
||||||
|
.map((r: any) => r.planFinishTime)
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort()
|
||||||
|
if (starts.length) mo.planStartTime = starts[0]
|
||||||
|
if (ends.length) mo.planFinishTime = ends[ends.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
drawerShow.value = false
|
||||||
|
persistDraftLocal()
|
||||||
|
renderGanttData()
|
||||||
|
message.success(`已将工序排到 ${payload.deviceName || payload.deviceCode},请再点击「保存排产草稿」落库`)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSaveDraft() {
|
async function handleSaveDraft() {
|
||||||
if (!projectId.value) {
|
if (!projectId.value) {
|
||||||
message.warning('缺少项目ID,无法保存')
|
message.warning('缺少项目ID,无法保存')
|
||||||
@ -213,15 +317,35 @@ function initGantt() {
|
|||||||
return formatGridDate(task.end_date)
|
return formatGridDate(task.end_date)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'scheduleStatus',
|
||||||
|
label: '状态',
|
||||||
|
align: 'center',
|
||||||
|
width: 80,
|
||||||
|
template(task: any) {
|
||||||
|
if (task._type !== 'route') return ''
|
||||||
|
return task._raw?.deviceId ? '已排产' : '待排产'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deviceName',
|
||||||
|
label: '设备/工位',
|
||||||
|
align: 'center',
|
||||||
|
width: 120,
|
||||||
|
template(task: any) {
|
||||||
|
if (task._type !== 'route') return ''
|
||||||
|
return task._raw?.deviceName || '未分配'
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
gantt.config.layout = {
|
gantt.config.layout = {
|
||||||
css: 'gantt_container',
|
css: 'gantt_container',
|
||||||
cols: [
|
cols: [
|
||||||
{
|
{
|
||||||
width: 620,
|
width: 820,
|
||||||
minWidth: 420,
|
minWidth: 520,
|
||||||
maxWidth: 680,
|
maxWidth: 960,
|
||||||
rows: [
|
rows: [
|
||||||
{ view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer' },
|
{ view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer' },
|
||||||
{ view: 'scrollbar', id: 'gridScroll', group: 'horizontal' },
|
{ view: 'scrollbar', id: 'gridScroll', group: 'horizontal' },
|
||||||
@ -240,6 +364,8 @@ function initGantt() {
|
|||||||
|
|
||||||
gantt.config.drag_progress = false
|
gantt.config.drag_progress = false
|
||||||
gantt.config.drag_links = false
|
gantt.config.drag_links = false
|
||||||
|
gantt.config.details_on_dblclick = false
|
||||||
|
gantt.config.details_on_create = false
|
||||||
// 使用自然日历(含周六日),不按工作日历自动顺延
|
// 使用自然日历(含周六日),不按工作日历自动顺延
|
||||||
gantt.config.work_time = false
|
gantt.config.work_time = false
|
||||||
gantt.config.correct_work_time = false
|
gantt.config.correct_work_time = false
|
||||||
@ -260,7 +386,8 @@ function initGantt() {
|
|||||||
gantt.templates.task_class = function(_start, _end, task) {
|
gantt.templates.task_class = function(_start, _end, task) {
|
||||||
if (task._type === 'mo') return 'gantt-task-mo'
|
if (task._type === 'mo') return 'gantt-task-mo'
|
||||||
if (task._type === 'route') {
|
if (task._type === 'route') {
|
||||||
return `gantt-task-route gantt-route-c-${task.colorIndex ?? 0}`
|
const assigned = !!task._raw?.deviceId
|
||||||
|
return `gantt-task-route gantt-route-c-${task.colorIndex ?? 0}${assigned ? '' : ' gantt-task-unassigned'}`
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@ -280,6 +407,18 @@ function initGantt() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gantt.init(ganttContainer.value)
|
gantt.init(ganttContainer.value)
|
||||||
|
|
||||||
|
const evClick = gantt.attachEvent('onTaskClick', (id: string | number) => {
|
||||||
|
const task = gantt.getTask(id)
|
||||||
|
if (task?._type === 'route') {
|
||||||
|
openProcessDrawer(task)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
ganttEventIds.push(evClick)
|
||||||
|
|
||||||
|
const evDbl = gantt.attachEvent('onTaskDblClick', () => false)
|
||||||
|
ganttEventIds.push(evDbl)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 今日时间轴标记(数据刷新后需重新添加) */
|
/** 今日时间轴标记(数据刷新后需重新添加) */
|
||||||
@ -357,6 +496,8 @@ function renderGanttData() {
|
|||||||
end_date: formatGanttDate(rEnd),
|
end_date: formatGanttDate(rEnd),
|
||||||
parent: moId,
|
parent: moId,
|
||||||
_type: 'route',
|
_type: 'route',
|
||||||
|
_moIndex: moIndex,
|
||||||
|
_routeIndex: index,
|
||||||
progress: 0,
|
progress: 0,
|
||||||
_raw: route,
|
_raw: route,
|
||||||
})
|
})
|
||||||
@ -367,11 +508,14 @@ function renderGanttData() {
|
|||||||
gantt.parse({ data: tasks, links: [] })
|
gantt.parse({ data: tasks, links: [] })
|
||||||
addTodayMarker()
|
addTodayMarker()
|
||||||
|
|
||||||
|
dragEventIds.forEach(id => gantt.detachEvent(id))
|
||||||
|
dragEventIds = []
|
||||||
|
|
||||||
const evDragId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
|
const evDragId = gantt.attachEvent('onBeforeTaskDrag', (id) => {
|
||||||
const task = gantt.getTask(id)
|
const task = gantt.getTask(id)
|
||||||
return task.type !== gantt.config.types.project
|
return task.type !== gantt.config.types.project
|
||||||
})
|
})
|
||||||
ganttEventIds.push(evDragId)
|
dragEventIds.push(evDragId)
|
||||||
|
|
||||||
const evId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
|
const evId = gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
|
||||||
const task = gantt.getTask(id)
|
const task = gantt.getTask(id)
|
||||||
@ -417,11 +561,12 @@ function renderGanttData() {
|
|||||||
message.success(`任务 [${label}] 计划时间已更新`)
|
message.success(`任务 [${label}] 计划时间已更新`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
ganttEventIds.push(evId)
|
dragEventIds.push(evId)
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
ganttEventIds.forEach(id => gantt.detachEvent(id))
|
ganttEventIds.forEach(id => gantt.detachEvent(id))
|
||||||
|
dragEventIds.forEach(id => gantt.detachEvent(id))
|
||||||
gantt.clearAll()
|
gantt.clearAll()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@ -604,10 +749,53 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gantt-container :deep(.gantt_task_line.gantt-task-route) {
|
.gantt-container :deep(.gantt_task_line.gantt-task-route) {
|
||||||
border: none !important;
|
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gantt-container :deep(.gantt_task_line.gantt-task-unassigned) {
|
||||||
|
background: #fff !important;
|
||||||
|
border: 1px dashed #c0c4cc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-container :deep(.gantt_task_line.gantt-task-unassigned .gantt_task_content) {
|
||||||
|
color: #86909c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gantt-legend {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item .lg {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item .lg.unassigned {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px dashed #c0c4cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend-item .lg.pending { background: #c0c4cc; }
|
||||||
|
.legend-item .lg.assigned { background: #2080f0; }
|
||||||
|
.legend-item .lg.producing { background: #18a058; }
|
||||||
|
.legend-item .lg.paused { background: #f0a020; }
|
||||||
|
.legend-item .lg.done { background: #8b5cf6; }
|
||||||
|
|
||||||
.gantt-container :deep(.gantt_grid_data .gantt_cell) {
|
.gantt-container :deep(.gantt_grid_data .gantt_cell) {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|||||||
783
src/views/biz/orderProject/components/ProcessScheduleDrawer.vue
Normal file
783
src/views/biz/orderProject/components/ProcessScheduleDrawer.vue
Normal file
@ -0,0 +1,783 @@
|
|||||||
|
<template>
|
||||||
|
<n-drawer v-model:show="visible" :width="760" placement="right" display-directive="show">
|
||||||
|
<n-drawer-content title="工序排产" closable :native-scrollbar="false">
|
||||||
|
<n-spin :show="loading">
|
||||||
|
<!-- A. 基本信息 -->
|
||||||
|
<section class="ps-section">
|
||||||
|
<h3 class="ps-title">基本信息</h3>
|
||||||
|
<div class="ps-info-grid">
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">生产订单</span>
|
||||||
|
<span class="v">{{ orderNo }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">工序号</span>
|
||||||
|
<span class="v">{{ process?.operNumber ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">工作中心</span>
|
||||||
|
<span class="v">{{ process?.workCenterName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">工序类型</span>
|
||||||
|
<span class="v">{{ process?.optCtrlCodeName || process?.processName || '机加' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">计划工时</span>
|
||||||
|
<span class="v">{{ manHourText }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">计划数量</span>
|
||||||
|
<span class="v">{{ order?.quantity ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">计划开始</span>
|
||||||
|
<span class="v">{{ formatDateTime(process?.planStartTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">计划结束</span>
|
||||||
|
<span class="v">{{ formatDateTime(process?.planFinishTime) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="ps-info-item">
|
||||||
|
<span class="k">当前状态</span>
|
||||||
|
<n-tag :type="currentAssigned ? 'info' : 'default'" size="small" :bordered="false">
|
||||||
|
{{ currentAssigned ? '已排产' : '待排产' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- B. 可执行设备/工位 -->
|
||||||
|
<section class="ps-section">
|
||||||
|
<h3 class="ps-title">可执行设备 / 工位</h3>
|
||||||
|
<p v-if="matchHint" class="ps-hint">{{ matchHint }}</p>
|
||||||
|
<n-data-table
|
||||||
|
size="small"
|
||||||
|
:columns="deviceColumns"
|
||||||
|
:data="devices"
|
||||||
|
:row-key="(row: DeviceScheduleItemVO) => row.id"
|
||||||
|
:bordered="false"
|
||||||
|
:single-line="false"
|
||||||
|
:row-class-name="deviceRowClass"
|
||||||
|
:row-props="deviceRowProps"
|
||||||
|
max-height="220"
|
||||||
|
/>
|
||||||
|
<n-empty v-if="!loading && !devices.length" description="未找到可执行设备/工位" size="small" style="margin: 12px 0" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- C. 当前任务 -->
|
||||||
|
<section class="ps-section">
|
||||||
|
<div class="ps-section-head">
|
||||||
|
<h3 class="ps-title">{{ selectedDeviceName }} 当前任务</h3>
|
||||||
|
<n-button size="tiny" quaternary :disabled="!selectedDevice" @click="recalcTimes">
|
||||||
|
刷新计算时间
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<n-tabs v-model:value="taskTab" size="small" type="line" :pane-style="{ display: 'none' }">
|
||||||
|
<n-tab-pane name="all" :tab="`全部任务 (${taskQueue.length})`" />
|
||||||
|
<n-tab-pane name="producing" :tab="`生产中 (${countByKey('producing')})`" />
|
||||||
|
<n-tab-pane name="assigned" :tab="`已排产 (${countByKey('assigned')})`" />
|
||||||
|
<n-tab-pane name="pending" :tab="`待排产 (${countByKey('pending')})`" />
|
||||||
|
</n-tabs>
|
||||||
|
<div v-if="!selectedDevice" class="ps-empty-tip">请先在上方选择一台设备/工位</div>
|
||||||
|
<VueDraggable
|
||||||
|
v-else-if="taskTab === 'all'"
|
||||||
|
v-model="taskQueue"
|
||||||
|
:animation="160"
|
||||||
|
handle=".ps-drag"
|
||||||
|
class="ps-task-list"
|
||||||
|
@end="recalcTimes"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="task in taskQueue"
|
||||||
|
:key="String(task.id)"
|
||||||
|
class="ps-task-row"
|
||||||
|
:class="{ current: task.isCurrent, locked: task.locked }"
|
||||||
|
>
|
||||||
|
<n-icon class="ps-drag" size="16" :depth="3"><MenuOutline /></n-icon>
|
||||||
|
<span class="ps-seq">{{ displayIndex(task) }}</span>
|
||||||
|
<span class="ps-play" :class="task.statusKey">
|
||||||
|
<n-icon size="14"><PlayOutline /></n-icon>
|
||||||
|
</span>
|
||||||
|
<div class="ps-task-main">
|
||||||
|
<div class="ps-task-code">{{ task.materialCode || task.orderNo || '-' }}</div>
|
||||||
|
<div class="ps-task-sub">{{ task.operNumber ?? '-' }} {{ task.processName || '' }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="ps-qty">{{ task.quantity ?? '-' }}</span>
|
||||||
|
<n-tag size="tiny" :type="statusTagType(task.statusKey)" :bordered="false">
|
||||||
|
{{ task.statusText || task.statusLabel || '-' }}
|
||||||
|
</n-tag>
|
||||||
|
<div class="ps-time">
|
||||||
|
<div>{{ formatShort(task.planStartTime) }}</div>
|
||||||
|
<div class="sub">{{ formatShort(task.planFinishTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!taskQueue.length" class="ps-empty-tip">暂无任务</div>
|
||||||
|
</VueDraggable>
|
||||||
|
<div v-else class="ps-task-list">
|
||||||
|
<div
|
||||||
|
v-for="task in filteredTasks"
|
||||||
|
:key="String(task.id)"
|
||||||
|
class="ps-task-row"
|
||||||
|
:class="{ current: task.isCurrent, locked: task.locked }"
|
||||||
|
>
|
||||||
|
<n-icon class="ps-drag" size="16" :depth="3" style="visibility:hidden"><MenuOutline /></n-icon>
|
||||||
|
<span class="ps-seq">{{ displayIndex(task) }}</span>
|
||||||
|
<span class="ps-play" :class="task.statusKey">
|
||||||
|
<n-icon size="14"><PlayOutline /></n-icon>
|
||||||
|
</span>
|
||||||
|
<div class="ps-task-main">
|
||||||
|
<div class="ps-task-code">{{ task.materialCode || task.orderNo || '-' }}</div>
|
||||||
|
<div class="ps-task-sub">{{ task.operNumber ?? '-' }} {{ task.processName || '' }}</div>
|
||||||
|
</div>
|
||||||
|
<span class="ps-qty">{{ task.quantity ?? '-' }}</span>
|
||||||
|
<n-tag size="tiny" :type="statusTagType(task.statusKey)" :bordered="false">
|
||||||
|
{{ task.statusText || task.statusLabel || '-' }}
|
||||||
|
</n-tag>
|
||||||
|
<div class="ps-time">
|
||||||
|
<div>{{ formatShort(task.planStartTime) }}</div>
|
||||||
|
<div class="sub">{{ formatShort(task.planFinishTime) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!filteredTasks.length" class="ps-empty-tip">该筛选下暂无任务</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- D. 时间预览 -->
|
||||||
|
<section class="ps-section">
|
||||||
|
<h3 class="ps-title">时间预览</h3>
|
||||||
|
<div v-if="!previewRows.length" class="ps-empty-tip">选择设备后可预览 24 小时负荷</div>
|
||||||
|
<div v-else class="ps-preview">
|
||||||
|
<div class="ps-preview-head">
|
||||||
|
<span class="ps-preview-label" />
|
||||||
|
<div class="ps-preview-hours">
|
||||||
|
<span v-for="h in hourLabels" :key="h">{{ h }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="row in previewRows" :key="row.date" class="ps-preview-row">
|
||||||
|
<span class="ps-preview-label">{{ row.date }}</span>
|
||||||
|
<div class="ps-preview-track">
|
||||||
|
<div
|
||||||
|
v-for="(seg, i) in row.segments"
|
||||||
|
:key="i"
|
||||||
|
class="ps-block"
|
||||||
|
:class="seg.statusKey"
|
||||||
|
:style="{ left: seg.left, width: seg.width }"
|
||||||
|
:title="seg.title"
|
||||||
|
>
|
||||||
|
{{ seg.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</n-spin>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="visible = false">取消</n-button>
|
||||||
|
<n-button type="primary" :disabled="!selectedDevice" @click="handleSave">
|
||||||
|
保存排产顺序
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, h, ref, watch } from 'vue'
|
||||||
|
import { NButton, NTag, useMessage, type DataTableColumns } from 'naive-ui'
|
||||||
|
import { MenuOutline, PlayOutline } from '@vicons/ionicons5'
|
||||||
|
import { VueDraggable } from 'vue-draggable-plus'
|
||||||
|
import {
|
||||||
|
deviceApi,
|
||||||
|
type DeviceScheduleItemVO,
|
||||||
|
type DeviceScheduleTaskVO,
|
||||||
|
} from '@/api/device'
|
||||||
|
import { resolveProcessName, type KingdeePrdMo, type KingdeeProcessRoute } from '@/api/orderProject'
|
||||||
|
|
||||||
|
export interface DraftAssignedItem {
|
||||||
|
moIndex: number
|
||||||
|
routeIndex: number
|
||||||
|
orderNo: string
|
||||||
|
processName: string
|
||||||
|
quantity?: number | null
|
||||||
|
deviceId: number | string
|
||||||
|
planStartTime?: string | null
|
||||||
|
planFinishTime?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScheduleSavePayload {
|
||||||
|
moIndex: number
|
||||||
|
routeIndex: number
|
||||||
|
deviceId: number
|
||||||
|
deviceCode?: string | null
|
||||||
|
deviceName?: string | null
|
||||||
|
planStartTime: string
|
||||||
|
planFinishTime: string
|
||||||
|
draftUpdates: Array<{
|
||||||
|
moIndex: number
|
||||||
|
routeIndex: number
|
||||||
|
planStartTime: string
|
||||||
|
planFinishTime: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<{
|
||||||
|
show: boolean
|
||||||
|
process?: KingdeeProcessRoute | Record<string, any> | null
|
||||||
|
order?: KingdeePrdMo | Record<string, any> | null
|
||||||
|
moIndex?: number
|
||||||
|
routeIndex?: number
|
||||||
|
draftAssigned?: DraftAssignedItem[]
|
||||||
|
}>(), {
|
||||||
|
process: () => ({}),
|
||||||
|
order: () => ({}),
|
||||||
|
moIndex: 0,
|
||||||
|
routeIndex: 0,
|
||||||
|
draftAssigned: () => [],
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:show': [boolean]
|
||||||
|
save: [payload: ScheduleSavePayload]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const visible = computed({
|
||||||
|
get: () => props.show,
|
||||||
|
set: (v) => emit('update:show', v),
|
||||||
|
})
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const matchHint = ref('')
|
||||||
|
const devices = ref<DeviceScheduleItemVO[]>([])
|
||||||
|
const selectedDevice = ref<DeviceScheduleItemVO | null>(null)
|
||||||
|
const backendTasks = ref<DeviceScheduleTaskVO[]>([])
|
||||||
|
const taskQueue = ref<DeviceScheduleTaskVO[]>([])
|
||||||
|
const taskTab = ref<'all' | 'producing' | 'assigned' | 'pending'>('all')
|
||||||
|
|
||||||
|
const orderNo = computed(() =>
|
||||||
|
props.order?.productionOrderNo || props.order?.billNo || props.order?.materialCode || '-'
|
||||||
|
)
|
||||||
|
|
||||||
|
const manHourText = computed(() => {
|
||||||
|
const qty = props.process?.activityQty
|
||||||
|
const unit = props.process?.activityUnit || 'h'
|
||||||
|
if (qty == null || qty === '') return '-'
|
||||||
|
return `${qty}${unit}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentAssigned = computed(() => !!props.process?.deviceId)
|
||||||
|
const selectedDeviceName = computed(() => selectedDevice.value?.deviceName || selectedDevice.value?.deviceCode || '设备')
|
||||||
|
|
||||||
|
const currentKey = computed(() => `draft_${props.moIndex}_${props.routeIndex}`)
|
||||||
|
|
||||||
|
const filteredTasks = computed(() => {
|
||||||
|
if (taskTab.value === 'all') return taskQueue.value
|
||||||
|
return taskQueue.value.filter((t) => t.statusKey === taskTab.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const hourLabels = ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00']
|
||||||
|
|
||||||
|
const deviceColumns: DataTableColumns<DeviceScheduleItemVO> = [
|
||||||
|
{
|
||||||
|
title: '设备/工位',
|
||||||
|
key: 'deviceName',
|
||||||
|
minWidth: 140,
|
||||||
|
render: (row) => h('div', [
|
||||||
|
h('div', { style: 'font-weight:600;color:#303133' }, row.deviceName || row.deviceCode || '-'),
|
||||||
|
h('div', { style: 'font-size:12px;color:#909399' }, row.deviceCode || row.deviceFlagLabel || ''),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{ title: '工作中心', key: 'sectionName', width: 110, render: (row) => row.sectionName || '-' },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'runStatus',
|
||||||
|
width: 72,
|
||||||
|
render: (row) => h(NTag, {
|
||||||
|
size: 'small',
|
||||||
|
type: row.runStatus === 'busy' ? 'warning' : 'success',
|
||||||
|
bordered: false,
|
||||||
|
}, { default: () => row.runStatusLabel || (row.runStatus === 'busy' ? '忙碌' : '闲置') }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '当前负荷',
|
||||||
|
key: 'loadPercent',
|
||||||
|
width: 80,
|
||||||
|
render: (row) => `${row.loadPercent ?? 0}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 100,
|
||||||
|
render: (row) => h(NButton, {
|
||||||
|
size: 'tiny',
|
||||||
|
type: selectedDevice.value?.id === row.id ? 'primary' : 'default',
|
||||||
|
secondary: selectedDevice.value?.id !== row.id,
|
||||||
|
onClick: (e: MouseEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
selectDevice(row)
|
||||||
|
},
|
||||||
|
}, { default: () => selectedDevice.value?.id === row.id ? '已选择' : '选择排产' }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function deviceRowClass(row: DeviceScheduleItemVO) {
|
||||||
|
return selectedDevice.value?.id === row.id ? 'ps-device-selected' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceRowProps(row: DeviceScheduleItemVO) {
|
||||||
|
return {
|
||||||
|
style: 'cursor: pointer',
|
||||||
|
onClick: () => selectDevice(row),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function countByKey(key: string) {
|
||||||
|
return taskQueue.value.filter((t) => t.statusKey === key).length
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayIndex(task: DeviceScheduleTaskVO) {
|
||||||
|
return taskQueue.value.findIndex((t) => t.id === task.id) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusTagType(key?: string) {
|
||||||
|
if (key === 'producing') return 'success'
|
||||||
|
if (key === 'assigned') return 'info'
|
||||||
|
if (key === 'pending') return 'warning'
|
||||||
|
if (key === 'paused') return 'warning'
|
||||||
|
if (key === 'done') return 'default'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTime(value?: string | null) {
|
||||||
|
if (!value) return null
|
||||||
|
const d = new Date(String(value).replace(' ', 'T'))
|
||||||
|
return Number.isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
function pad(n: number) {
|
||||||
|
return String(n).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateTime(value?: string | null) {
|
||||||
|
const d = parseTime(value)
|
||||||
|
if (!d) return '-'
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatShort(value?: string | null) {
|
||||||
|
const d = parseTime(value)
|
||||||
|
if (!d) return '-'
|
||||||
|
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatGanttDate(date: Date) {
|
||||||
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function durationOf(start?: string | null, end?: string | null, fallback = 8 * 3600000) {
|
||||||
|
const s = parseTime(start)
|
||||||
|
const e = parseTime(end)
|
||||||
|
if (!s || !e || e <= s) return fallback
|
||||||
|
return e.getTime() - s.getTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
function md(date: Date) {
|
||||||
|
return `${pad(date.getMonth() + 1)}-${pad(date.getDate())}`
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
async (show) => {
|
||||||
|
if (!show) return
|
||||||
|
selectedDevice.value = null
|
||||||
|
backendTasks.value = []
|
||||||
|
taskQueue.value = []
|
||||||
|
taskTab.value = 'all'
|
||||||
|
await loadCapable()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async function loadCapable() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await deviceApi.scheduleCapable({
|
||||||
|
workCenterName: props.process?.workCenterName,
|
||||||
|
departmentName: props.process?.departmentName || props.order?.workShopName,
|
||||||
|
})
|
||||||
|
matchHint.value = res?.matchHint || ''
|
||||||
|
devices.value = res?.devices || []
|
||||||
|
const boundId = props.process?.deviceId
|
||||||
|
if (boundId != null) {
|
||||||
|
const hit = devices.value.find((d) => String(d.id) === String(boundId))
|
||||||
|
if (hit) await selectDevice(hit)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
matchHint.value = '加载可执行设备失败'
|
||||||
|
devices.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectDevice(row: DeviceScheduleItemVO) {
|
||||||
|
selectedDevice.value = row
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const list = await deviceApi.scheduleTasks(row.id)
|
||||||
|
backendTasks.value = Array.isArray(list) ? list : []
|
||||||
|
} catch {
|
||||||
|
backendTasks.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
buildQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQueue() {
|
||||||
|
const deviceId = selectedDevice.value?.id
|
||||||
|
const durationMs = durationOf(props.process?.planStartTime, props.process?.planFinishTime)
|
||||||
|
const current: DeviceScheduleTaskVO = {
|
||||||
|
id: currentKey.value,
|
||||||
|
orderNo: orderNo.value,
|
||||||
|
materialCode: props.order?.materialCode,
|
||||||
|
materialName: props.order?.materialName,
|
||||||
|
processName: resolveProcessName(props.process || {}),
|
||||||
|
operNumber: props.process?.operNumber,
|
||||||
|
quantity: props.order?.quantity ?? null,
|
||||||
|
statusKey: 'pending',
|
||||||
|
statusText: '待排产',
|
||||||
|
statusLabel: '待排产',
|
||||||
|
planStartTime: props.process?.planStartTime,
|
||||||
|
planFinishTime: props.process?.planFinishTime,
|
||||||
|
locked: false,
|
||||||
|
isCurrent: true,
|
||||||
|
durationMs,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromBackend = backendTasks.value.map((t) => ({
|
||||||
|
...t,
|
||||||
|
isCurrent: false,
|
||||||
|
durationMs: durationOf(t.planStartTime, t.planFinishTime),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const fromDraft = (props.draftAssigned || []).filter((t) => {
|
||||||
|
if (String(t.deviceId) !== String(deviceId)) return false
|
||||||
|
if (t.moIndex === props.moIndex && t.routeIndex === props.routeIndex) return false
|
||||||
|
return true
|
||||||
|
}).map((t) => ({
|
||||||
|
id: `draft_${t.moIndex}_${t.routeIndex}`,
|
||||||
|
orderNo: t.orderNo,
|
||||||
|
processName: t.processName,
|
||||||
|
quantity: t.quantity,
|
||||||
|
deviceId: String(t.deviceId),
|
||||||
|
statusKey: 'assigned',
|
||||||
|
statusText: '已排产',
|
||||||
|
statusLabel: '已排产',
|
||||||
|
planStartTime: t.planStartTime,
|
||||||
|
planFinishTime: t.planFinishTime,
|
||||||
|
locked: false,
|
||||||
|
isCurrent: false,
|
||||||
|
durationMs: durationOf(t.planStartTime, t.planFinishTime),
|
||||||
|
moIndex: t.moIndex,
|
||||||
|
routeIndex: t.routeIndex,
|
||||||
|
} as DeviceScheduleTaskVO & { moIndex: number; routeIndex: number }))
|
||||||
|
|
||||||
|
const merged: DeviceScheduleTaskVO[] = []
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const t of [...fromBackend, ...fromDraft]) {
|
||||||
|
const key = String(t.id)
|
||||||
|
if (seen.has(key)) continue
|
||||||
|
seen.add(key)
|
||||||
|
merged.push(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
const already = merged.findIndex((t) =>
|
||||||
|
String(t.materialCode) === String(props.order?.materialCode)
|
||||||
|
&& String(t.operNumber) === String(props.process?.operNumber)
|
||||||
|
)
|
||||||
|
if (already >= 0) {
|
||||||
|
merged[already] = { ...merged[already], ...current, id: merged[already].id }
|
||||||
|
} else {
|
||||||
|
const insertAt = merged.findIndex((t) => !t.locked)
|
||||||
|
if (insertAt < 0) merged.push(current)
|
||||||
|
else merged.splice(insertAt, 0, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
taskQueue.value = merged
|
||||||
|
recalcTimes()
|
||||||
|
}
|
||||||
|
|
||||||
|
function recalcTimes() {
|
||||||
|
const list = taskQueue.value
|
||||||
|
if (!list.length) return
|
||||||
|
let cursor: Date | null = null
|
||||||
|
for (const task of list) {
|
||||||
|
const dur = task.durationMs || durationOf(task.planStartTime, task.planFinishTime)
|
||||||
|
if (task.locked && parseTime(task.planStartTime) && parseTime(task.planFinishTime)) {
|
||||||
|
const end = parseTime(task.planFinishTime)!
|
||||||
|
if (!cursor || end > cursor) cursor = end
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!cursor) {
|
||||||
|
cursor = parseTime(task.planStartTime) || new Date()
|
||||||
|
}
|
||||||
|
const start = new Date(cursor)
|
||||||
|
const end = new Date(start.getTime() + dur)
|
||||||
|
task.planStartTime = formatGanttDate(start)
|
||||||
|
task.planFinishTime = formatGanttDate(end)
|
||||||
|
cursor = end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewRows = computed(() => {
|
||||||
|
const map = new Map<string, { date: string; segments: Array<{
|
||||||
|
left: string; width: string; statusKey: string; label: string; title: string
|
||||||
|
}> }>()
|
||||||
|
for (const task of taskQueue.value) {
|
||||||
|
const start = parseTime(task.planStartTime)
|
||||||
|
const end = parseTime(task.planFinishTime)
|
||||||
|
if (!start || !end || end <= start) continue
|
||||||
|
let cursor = new Date(start)
|
||||||
|
while (cursor < end) {
|
||||||
|
const dayStart = new Date(cursor.getFullYear(), cursor.getMonth(), cursor.getDate())
|
||||||
|
const dayEnd = new Date(dayStart.getTime() + 86400000)
|
||||||
|
const segEnd = end < dayEnd ? end : dayEnd
|
||||||
|
const leftMin = (cursor.getTime() - dayStart.getTime()) / 60000
|
||||||
|
const widthMin = Math.max(20, (segEnd.getTime() - cursor.getTime()) / 60000)
|
||||||
|
const date = md(cursor)
|
||||||
|
if (!map.has(date)) map.set(date, { date, segments: [] })
|
||||||
|
map.get(date)!.segments.push({
|
||||||
|
left: `${(leftMin / 1440) * 100}%`,
|
||||||
|
width: `${Math.min(100, (widthMin / 1440) * 100)}%`,
|
||||||
|
statusKey: task.isCurrent ? 'pending' : (task.statusKey || 'assigned'),
|
||||||
|
label: String(task.materialCode || task.orderNo || task.processName || ''),
|
||||||
|
title: `${task.materialCode || task.orderNo || ''} ${task.processName || ''} ${formatShort(task.planStartTime)} ~ ${formatShort(task.planFinishTime)}`,
|
||||||
|
})
|
||||||
|
cursor = segEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...map.values()]
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
if (!selectedDevice.value) {
|
||||||
|
message.warning('请先选择可执行设备/工位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const current = taskQueue.value.find((t) => t.isCurrent)
|
||||||
|
if (!current?.planStartTime || !current?.planFinishTime) {
|
||||||
|
message.warning('请先刷新计算时间')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('save', {
|
||||||
|
moIndex: props.moIndex,
|
||||||
|
routeIndex: props.routeIndex,
|
||||||
|
deviceId: selectedDevice.value.id,
|
||||||
|
deviceCode: selectedDevice.value.deviceCode,
|
||||||
|
deviceName: selectedDevice.value.deviceName,
|
||||||
|
planStartTime: current.planStartTime,
|
||||||
|
planFinishTime: current.planFinishTime,
|
||||||
|
draftUpdates: taskQueue.value
|
||||||
|
.filter((t) => !t.isCurrent && String(t.id).startsWith('draft_') && t.planStartTime && t.planFinishTime)
|
||||||
|
.map((t) => {
|
||||||
|
const parts = String(t.id).split('_')
|
||||||
|
return {
|
||||||
|
moIndex: Number(parts[1]),
|
||||||
|
routeIndex: Number(parts[2]),
|
||||||
|
planStartTime: t.planStartTime as string,
|
||||||
|
planFinishTime: t.planFinishTime as string,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((u) => Number.isFinite(u.moIndex) && Number.isFinite(u.routeIndex)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.ps-section {
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.ps-section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.ps-title {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
padding-left: 8px;
|
||||||
|
border-left: 3px solid #2f6fed;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.ps-hint {
|
||||||
|
margin: -4px 0 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.ps-info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 10px 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.ps-info-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.ps-info-item .k {
|
||||||
|
color: #909399;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.ps-info-item .v {
|
||||||
|
color: #303133;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.ps-empty-tip {
|
||||||
|
padding: 16px 0;
|
||||||
|
text-align: center;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.ps-task-list {
|
||||||
|
margin-top: 8px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ps-task-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 22px 28px 22px minmax(0, 1fr) 48px 64px 92px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.ps-task-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.ps-task-row.current {
|
||||||
|
background: #fff8e6;
|
||||||
|
}
|
||||||
|
.ps-drag {
|
||||||
|
cursor: grab;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
.ps-seq {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.ps-play {
|
||||||
|
display: inline-flex;
|
||||||
|
color: #c0c4cc;
|
||||||
|
}
|
||||||
|
.ps-play.producing { color: #18a058; }
|
||||||
|
.ps-play.assigned { color: #2080f0; }
|
||||||
|
.ps-play.pending { color: #f0a020; }
|
||||||
|
.ps-task-code {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #303133;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.ps-task-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.ps-qty {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.ps-time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.ps-time .sub {
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.ps-preview {
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.ps-preview-head,
|
||||||
|
.ps-preview-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 56px 1fr;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.ps-preview-head {
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.ps-preview-hours {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.ps-preview-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
.ps-preview-row {
|
||||||
|
min-height: 36px;
|
||||||
|
border-bottom: 1px solid #f5f7fa;
|
||||||
|
padding: 6px 8px;
|
||||||
|
}
|
||||||
|
.ps-preview-track {
|
||||||
|
position: relative;
|
||||||
|
height: 24px;
|
||||||
|
background:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
#f5f7fa 0,
|
||||||
|
#f5f7fa calc(100% / 6 - 1px),
|
||||||
|
#ebeef5 calc(100% / 6 - 1px),
|
||||||
|
#ebeef5 calc(100% / 6)
|
||||||
|
);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.ps-block {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.ps-block.producing { background: #18a058; }
|
||||||
|
.ps-block.assigned { background: #2080f0; }
|
||||||
|
.ps-block.pending { background: #f0a020; }
|
||||||
|
.ps-block.paused { background: #f0a020; }
|
||||||
|
.ps-block.done { background: #8b5cf6; }
|
||||||
|
|
||||||
|
:deep(.ps-device-selected td) {
|
||||||
|
background: #ecf5ff !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -16,7 +16,7 @@ export default defineConfig({
|
|||||||
emptyOutDir: true
|
emptyOutDir: true
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
//host:'192.168.12.4', //true,
|
host: true, // 监听 0.0.0.0,避免本机 IP 变化导致 EADDRNOTAVAIL
|
||||||
port: 3000,
|
port: 3000,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user