合并工序看板重排、订单开工与派工列表展示调整。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
44ca42ce18
commit
cf7a8e9cd6
@ -125,7 +125,11 @@ export const orderItemApi = {
|
||||
//派工
|
||||
assignWork(data: any) {
|
||||
return request({ url: '/biz/orderItem/assignWork', method: 'post', data })
|
||||
},
|
||||
},
|
||||
// 开工
|
||||
startWork(id: number) {
|
||||
return request({ url: `/biz/orderItem/${id}/startWork`, method: 'post' })
|
||||
},
|
||||
//根据生产编号查询预生产订单
|
||||
preCreation(data: any) {
|
||||
return request({ url: '/biz/orderItem/preCreation', method: 'post', data })
|
||||
|
||||
@ -475,6 +475,18 @@ export const orderProcessPlanApi = {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 订单级工序顺序重排。
|
||||
* 只提交 orderItemId + 按新顺序排列的 planId,后端重写 sort(10/20/30…)。
|
||||
*/
|
||||
reorder(data: ProcessPlanReorderPayload) {
|
||||
return request({
|
||||
url: '/biz/orderProcessPlan/reorder',
|
||||
method: 'put',
|
||||
data,
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
/** 委外可选工序项 */
|
||||
@ -515,3 +527,16 @@ export interface OutsourcingCreate {
|
||||
remark?: string
|
||||
}
|
||||
|
||||
/** 工序重排明细 */
|
||||
export interface ProcessPlanReorderItem {
|
||||
planId: number
|
||||
/** 可选;不传则按数组顺序由后端分配 sort */
|
||||
sort?: number
|
||||
}
|
||||
|
||||
/** 订单级工序重排请求 */
|
||||
export interface ProcessPlanReorderPayload {
|
||||
orderItemId: number
|
||||
items: ProcessPlanReorderItem[]
|
||||
}
|
||||
|
||||
|
||||
@ -37,10 +37,10 @@
|
||||
<n-button type="error" :disabled="kingdeeLoading" @click="reCrawlReorderForm">
|
||||
重新读取数据
|
||||
</n-button>
|
||||
<n-button type="primary" :disabled="kingdeeLoading" @click="handleSaveDraft">
|
||||
<n-button type="primary" :disabled="kingdeeLoading" :loading="actionLoading" @click="handleSaveDraft">
|
||||
保存草稿
|
||||
</n-button>
|
||||
<n-button type="info" :disabled="kingdeeLoading" @click="handleSyncOrderAndPlan">
|
||||
<n-button type="info" :disabled="kingdeeLoading" :loading="actionLoading" @click="handleSyncOrderAndPlan">
|
||||
同步订单计划
|
||||
</n-button>
|
||||
<n-button @click="$emit('close-modal')">关闭</n-button>
|
||||
@ -332,26 +332,29 @@ function handleGanttSchedule() {
|
||||
router.push('/biz/orderProject/gantt')
|
||||
}
|
||||
|
||||
// 同步订单和计划表
|
||||
// 同步订单和计划表(等待父组件接口完成后再关 loading,避免第一次点击“没反应”)
|
||||
async function handleSyncOrderAndPlan() {
|
||||
if (!props.selectedId) return
|
||||
|
||||
if (!props.selectedId || actionLoading.value) return
|
||||
|
||||
actionLoading.value = true
|
||||
try {
|
||||
// 恢复数据结构,将 processRoutes 转换为 children
|
||||
const draftData = kingdeeTableData.value.map(mo => {
|
||||
const { processRoutes, ...rest } = mo
|
||||
return { ...rest, children: processRoutes } as KingdeePrdMo
|
||||
})
|
||||
|
||||
emit('synchronizationCallback', props.selectedId, draftData, (res) => {
|
||||
if("success" === res){
|
||||
message.success('同步订单计划成功')
|
||||
emit('close-modal')
|
||||
}
|
||||
|
||||
const res = await new Promise<unknown>((resolve, reject) => {
|
||||
emit('synchronizationCallback', props.selectedId, draftData, (result: unknown, err?: unknown) => {
|
||||
if (err) reject(err)
|
||||
else resolve(result)
|
||||
})
|
||||
kingdeeModalVisible.value = false
|
||||
} catch (error) {
|
||||
})
|
||||
|
||||
if (res === 'success') {
|
||||
message.success('同步订单计划成功')
|
||||
emit('close-modal')
|
||||
}
|
||||
} catch {
|
||||
// 错误拦截器已处理
|
||||
} finally {
|
||||
actionLoading.value = false
|
||||
|
||||
@ -1,102 +1,95 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<n-card>
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
<n-form-item label="生产令号">
|
||||
<n-input v-model:value="searchForm.productionCode" placeholder="请输入生产令号" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="物料编码">
|
||||
<n-input v-model:value="searchForm.code" placeholder="请输入物料编码" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="项目名称">
|
||||
<n-input v-model:value="searchForm.projectName" placeholder="请输入产品名称" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="开始时间">
|
||||
<n-date-picker v-model:value="searchForm.beginTime" type="datetime" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="结束时间">
|
||||
<n-date-picker v-model:value="searchForm.endTime" type="datetime" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||
搜索
|
||||
</n-button>
|
||||
<n-button @click="handleReset">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
<div class="order-layout">
|
||||
<!-- 左侧生产令号 -->
|
||||
<n-card class="order-tree-card" size="small">
|
||||
<template #header>
|
||||
<div class="order-tree-header">生产令号</div>
|
||||
</template>
|
||||
<div class="order-search">
|
||||
<n-input v-model:value="projectTreeSearch" placeholder="搜索生产令号" clearable size="small">
|
||||
<template #prefix>
|
||||
<n-icon><SearchOutline /></n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
</div>
|
||||
<div class="order-tree-wrapper">
|
||||
<n-tree
|
||||
:data="projectTreeData"
|
||||
:pattern="projectTreeSearch"
|
||||
:selected-keys="selectedProjectKeys"
|
||||
:node-props="projectNodeProps"
|
||||
key-field="id"
|
||||
label-field="label"
|
||||
selectable
|
||||
block-line
|
||||
/>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<div class="table-toolbar">
|
||||
<n-space>
|
||||
<!--<n-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handleAdd"
|
||||
<!-- 右侧生产订单列表 -->
|
||||
<n-card class="order-list-card" size="small">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ selectedProjectLabel ? `【${selectedProjectLabel}】生产订单` : '全部生产订单' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 搜索表单 -->
|
||||
<div class="search-form">
|
||||
<n-form inline :model="searchForm" label-placement="left">
|
||||
<n-form-item label="物料编码">
|
||||
<n-input v-model:value="searchForm.code" placeholder="请输入物料编码" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="开始时间">
|
||||
<n-date-picker v-model:value="searchForm.beginTime" type="datetime" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item label="结束时间">
|
||||
<n-date-picker v-model:value="searchForm.endTime" type="datetime" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-space>
|
||||
<n-button type="primary" @click="handleSearch">
|
||||
<template #icon><n-icon><SearchOutline /></n-icon></template>
|
||||
搜索
|
||||
</n-button>
|
||||
<n-button @click="handleReset">
|
||||
<template #icon><n-icon><RefreshOutline /></n-icon></template>
|
||||
重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
|
||||
<n-data-table
|
||||
size="small"
|
||||
remote
|
||||
striped
|
||||
:border="false"
|
||||
:single-line="false"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
/>
|
||||
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
||||
<n-pagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
show-size-picker
|
||||
show-quick-jumper
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
<template #icon><n-icon><AddOutline /></n-icon></template>
|
||||
新增/补单
|
||||
</n-button>
|
||||
<n-button @click="importModalVisible = true">
|
||||
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
|
||||
导入
|
||||
</n-button>
|
||||
<n-button @click="handleExport">
|
||||
<template #icon><n-icon><DownloadOutline /></n-icon></template>
|
||||
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
|
||||
</n-button>
|
||||
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
|
||||
<template #icon><n-icon><TrashOutline /></n-icon></template>
|
||||
删除
|
||||
</n-button> -->
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<!--
|
||||
:row-key="(row) => row.id"
|
||||
:scroll-x="1200"
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
@update:checked-row-keys="handleCheck"
|
||||
:pagination="pagination"
|
||||
-->
|
||||
<!--flex-height-->
|
||||
<n-data-table
|
||||
size="small"
|
||||
remote
|
||||
striped
|
||||
:border="false"
|
||||
:single-line="false"
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:loading="loading"
|
||||
/>
|
||||
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
|
||||
<n-pagination
|
||||
v-model:page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:item-count="pagination.itemCount"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
show-size-picker
|
||||
show-quick-jumper
|
||||
@update:page="handlePageChange"
|
||||
@update:page-size="handlePageSizeChange"
|
||||
>
|
||||
<template #prefix>
|
||||
共 {{ pagination.itemCount }} 条
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
</n-card>
|
||||
<template #prefix>
|
||||
共 {{ pagination.itemCount }} 条
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<n-modal
|
||||
@ -385,8 +378,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, h, computed, onMounted } from 'vue'
|
||||
import { NButton, NSpace, NIcon, NTag, useMessage, useDialog, type UploadCustomRequestOptions} from 'naive-ui'
|
||||
import { ref, reactive, h, computed, onMounted, type HTMLAttributes } from 'vue'
|
||||
import { NButton, NSpace, NIcon, NTag, useMessage, useDialog, type UploadCustomRequestOptions, type TreeOption } from 'naive-ui'
|
||||
import { SearchOutline, RefreshOutline, CreateOutline, PlayOutline, DocumentTextOutline } from '@vicons/ionicons5'
|
||||
import { orderItemApi, type OrderItem } from '@/api/orderItem'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
@ -413,7 +407,6 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive<any>({
|
||||
productionCode: '',
|
||||
code: '',
|
||||
projectName: '',
|
||||
orderCode: '',
|
||||
@ -423,6 +416,56 @@ const searchForm = reactive<any>({
|
||||
endTime: null as number | null,
|
||||
})
|
||||
|
||||
// 左侧生产令号树
|
||||
type ProjectTreeNode = {
|
||||
id: number
|
||||
label: string
|
||||
projectCode: string
|
||||
projectName: string
|
||||
}
|
||||
const projectTreeSearch = ref('')
|
||||
const projectTreeData = ref<ProjectTreeNode[]>([])
|
||||
const selectedProjectKeys = ref<Array<string | number>>([])
|
||||
const selectedProjectId = ref<number | undefined>(undefined)
|
||||
const selectedProjectLabel = ref('')
|
||||
|
||||
const projectNodeProps = ({ option }: { option: TreeOption }): HTMLAttributes => {
|
||||
return {
|
||||
onClick() {
|
||||
const id = option.id as number
|
||||
if (selectedProjectId.value === id) {
|
||||
selectedProjectId.value = undefined
|
||||
selectedProjectKeys.value = []
|
||||
selectedProjectLabel.value = ''
|
||||
} else {
|
||||
selectedProjectId.value = id
|
||||
selectedProjectKeys.value = [id]
|
||||
selectedProjectLabel.value = (option.projectCode as string) || (option.label as string) || ''
|
||||
}
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectTree() {
|
||||
try {
|
||||
const data = await orderItemApi.xmoptions({})
|
||||
projectTreeData.value = (Array.isArray(data) ? data : []).map((item: any) => {
|
||||
const projectCode = item?.additionParams?.projectCode || ''
|
||||
const projectName = item?.label || ''
|
||||
return {
|
||||
id: Number(item.value),
|
||||
label: projectCode || projectName,
|
||||
projectCode,
|
||||
projectName,
|
||||
}
|
||||
}).filter((n: ProjectTreeNode) => n.id && n.label)
|
||||
} catch {
|
||||
projectTreeData.value = []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 表格数据
|
||||
@ -487,7 +530,7 @@ const columns = computed(() => [
|
||||
{
|
||||
align:'center',
|
||||
title: '生产令号',
|
||||
key: 'productionCode',
|
||||
key: 'mainCode',
|
||||
width: '140'
|
||||
},
|
||||
{
|
||||
@ -607,7 +650,10 @@ const columns = computed(() => [
|
||||
align:'center',
|
||||
title: '是否开工',
|
||||
key: 'starter',
|
||||
width:"120"
|
||||
width:"120",
|
||||
render(row: any) {
|
||||
return row.starter === 1 ? '已开工' : '未开工'
|
||||
}
|
||||
},
|
||||
{
|
||||
align:'center',
|
||||
@ -619,51 +665,45 @@ const columns = computed(() => [
|
||||
align:'center',
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 190,
|
||||
width: 260,
|
||||
fixed:"right",
|
||||
render(row:any) {
|
||||
const buttons: ReturnType<typeof h>[] = []
|
||||
|
||||
const buttons:any = []
|
||||
|
||||
if(hasPermission('biz:orderItem:edit')){
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
type:'primary',
|
||||
ghost:true,
|
||||
onClick: () => { handleEdit(row) } },
|
||||
{ default: () => '编辑'}
|
||||
))
|
||||
}
|
||||
// if(hasPermission('biz:orderItem:assign')){
|
||||
// buttons.push(h(NButton, {
|
||||
// size: 'small',
|
||||
// type:'success',
|
||||
// ghost:true,
|
||||
// onClick: () => { disphand(row) } },
|
||||
// { default: () => '派工'}
|
||||
// ))
|
||||
// }
|
||||
if(hasPermission('biz:orderItem:reorderForm')){
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
type:'info',
|
||||
ghost:true,
|
||||
onClick: () => handleReorderForm(row, false) }, //, disabled: row.starter !==1
|
||||
{ default: () => '补单'}
|
||||
))
|
||||
}
|
||||
if (hasPermission('biz:orderItem:edit')) {
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
onClick: () => handleEdit(row),
|
||||
}, {
|
||||
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑'],
|
||||
}))
|
||||
}
|
||||
// 未开工且有工艺路线的子订单可开工
|
||||
if (hasPermission('biz:orderItem:startWork') && row.starter !== 1 && row.parentId && row.routeCode) {
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
type: 'warning',
|
||||
onClick: () => handleStartWork(row),
|
||||
}, {
|
||||
default: () => [h(NIcon, null, { default: () => h(PlayOutline) }), ' 开工'],
|
||||
}))
|
||||
}
|
||||
if (hasPermission('biz:orderItem:reorderForm')) {
|
||||
buttons.push(h(NButton, {
|
||||
size: 'small',
|
||||
quaternary: true,
|
||||
type: 'info',
|
||||
onClick: () => handleReorderForm(row, false),
|
||||
}, {
|
||||
default: () => [h(NIcon, null, { default: () => h(DocumentTextOutline) }), ' 补单'],
|
||||
}))
|
||||
}
|
||||
|
||||
return buttons.length > 0 ? h(NSpace, {justify:'center'}, { default: () => buttons }) : '-'
|
||||
|
||||
|
||||
// return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
|
||||
// h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
|
||||
// default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
|
||||
// }),
|
||||
// h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, {
|
||||
// default: () => [h(NIcon, null, { default: () => h(TrashOutline) }), ' 派工']
|
||||
// })
|
||||
// ])
|
||||
return buttons.length > 0
|
||||
? h('div', { style: { display: 'flex', alignItems: 'center', gap: '4px', flexWrap: 'nowrap', justifyContent: 'center' } }, buttons)
|
||||
: '-'
|
||||
}
|
||||
}
|
||||
])
|
||||
@ -688,12 +728,9 @@ async function loadData() {
|
||||
const res = await orderItemApi.page({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
productionCode: searchForm.productionCode || undefined,
|
||||
code: searchForm.code,
|
||||
projectName: searchForm.projectName,
|
||||
orderCode: searchForm.orderCode,
|
||||
routeCode: searchForm.routeCode,
|
||||
proWorkshop: searchForm.proWorkshop,
|
||||
projectId: selectedProjectId.value,
|
||||
code: searchForm.code || undefined,
|
||||
projectName: searchForm.projectName || undefined,
|
||||
beginTime: searchForm.beginTime,
|
||||
endTime: searchForm.endTime
|
||||
})
|
||||
@ -712,7 +749,6 @@ function handleSearch() {
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchForm.productionCode = ''
|
||||
searchForm.code = ''
|
||||
searchForm.projectName = ''
|
||||
searchForm.orderCode = ''
|
||||
@ -720,6 +756,10 @@ function handleReset() {
|
||||
searchForm.proWorkshop = ''
|
||||
searchForm.beginTime = null
|
||||
searchForm.endTime = null
|
||||
selectedProjectId.value = undefined
|
||||
selectedProjectKeys.value = []
|
||||
selectedProjectLabel.value = ''
|
||||
projectTreeSearch.value = ''
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
@ -872,19 +912,14 @@ async function holdTemporarilySave(id: number, data: KingdeePrdMo[]) {
|
||||
// }
|
||||
// }
|
||||
|
||||
// 同步订单和计划表
|
||||
async function synchronizationSave(id: number, data: KingdeePrdMo[], callback:Function) {
|
||||
// 同步订单和计划表(通过 callback 回传结果,供子弹等待接口完成)
|
||||
async function synchronizationSave(id: number, data: KingdeePrdMo[], callback: Function) {
|
||||
try {
|
||||
// 恢复数据结构,将 processRoutes 转换为 children
|
||||
await orderItemApi.synchronizationSave(id, data)
|
||||
.then((rps:any) =>{
|
||||
callback(rps)
|
||||
}).catch((res) => {
|
||||
callback(res)
|
||||
})
|
||||
const rps = await orderItemApi.synchronizationSave(id, data)
|
||||
callback(rps)
|
||||
} catch (error) {
|
||||
// 错误拦截器已处理
|
||||
}
|
||||
callback(null, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1092,6 +1127,25 @@ function handleDelete(row: OrderItem) {
|
||||
})
|
||||
}
|
||||
|
||||
// 开工:生成工单
|
||||
function handleStartWork(row: OrderItem) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: `确定开工订单【${row.orderCode || row.name}】?开工后将生成工单。`,
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await orderItemApi.startWork(row.id!)
|
||||
message.success('开工成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
// 错误已在拦截器处理
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
function handleBatchDelete() {
|
||||
dialog.warning({
|
||||
@ -1182,6 +1236,7 @@ async function loadDictOptions() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadProjectTree()
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
|
||||
@ -1192,6 +1247,41 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.order-layout {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.order-tree-card {
|
||||
width: 280px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-tree-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.order-search {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.order-tree-wrapper {
|
||||
height: calc(100vh - 280px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.order-list-card {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
@ -139,7 +139,14 @@
|
||||
<div class="col-actions">
|
||||
<n-button text type="primary" size="tiny" @click="openDetail(order)">详情</n-button>
|
||||
<n-button text type="info" size="tiny" @click="openPlmModel(order)">模型</n-button>
|
||||
<n-button text type="primary" size="tiny" disabled title="订单级编辑待支持">编辑</n-button>
|
||||
<!-- 订单级编辑:打开工序顺序重排弹窗(不进单工序编辑) -->
|
||||
<n-button
|
||||
text
|
||||
type="primary"
|
||||
size="tiny"
|
||||
:disabled="!hasPermission('biz:orderProcessPlan:edit')"
|
||||
@click="openReorder(order)"
|
||||
>编辑</n-button>
|
||||
<n-dropdown :options="moreOptions" trigger="click" @select="(k) => onMoreSelect(k, order)">
|
||||
<n-button text size="tiny">更多</n-button>
|
||||
</n-dropdown>
|
||||
@ -271,7 +278,81 @@
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
|
||||
<!-- 新增/编辑 -->
|
||||
<!-- 订单级工序顺序重排:拖拽 / 上移下移,只提交 planId 顺序 -->
|
||||
<n-modal
|
||||
v-model:show="reorderVisible"
|
||||
preset="card"
|
||||
title="调整工序顺序"
|
||||
style="width: 640px"
|
||||
:mask-closable="false"
|
||||
@after-leave="resetReorder"
|
||||
>
|
||||
<n-spin :show="reorderLoading">
|
||||
<n-alert type="info" style="margin-bottom: 12px">
|
||||
拖拽行或使用上移/下移调整顺序;保存后按 10、20、30… 重写工序号。存在派工工单或已有转出数量的工序不可调整。
|
||||
</n-alert>
|
||||
<div v-if="reorderOrderMeta" class="reorder-meta">
|
||||
<span>令号:{{ reorderOrderMeta.mainCode || '-' }}</span>
|
||||
<span>物料:{{ reorderOrderMeta.materialName || '-' }}</span>
|
||||
<span>数量:{{ reorderOrderMeta.quantity ?? '-' }}</span>
|
||||
</div>
|
||||
<div v-if="!reorderList.length && !reorderLoading" class="reorder-empty">暂无工序可调整</div>
|
||||
<VueDraggable
|
||||
v-else
|
||||
v-model="reorderList"
|
||||
:animation="150"
|
||||
handle=".reorder-drag-handle"
|
||||
class="reorder-list"
|
||||
>
|
||||
<div
|
||||
v-for="(proc, idx) in reorderList"
|
||||
:key="proc.planId ?? idx"
|
||||
class="reorder-item"
|
||||
>
|
||||
<n-icon class="reorder-drag-handle" size="18" :depth="3">
|
||||
<MenuOutline />
|
||||
</n-icon>
|
||||
<span class="reorder-index">{{ idx + 1 }}</span>
|
||||
<div class="reorder-body">
|
||||
<div class="reorder-title">
|
||||
<em v-if="proc.operNumber != null">{{ proc.operNumber }}</em>
|
||||
{{ proc.processName || '-' }}
|
||||
</div>
|
||||
<div class="reorder-sub">
|
||||
{{ proc.processCode || '-' }} · {{ proc.workOrderCode || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
<n-space :size="4">
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
:disabled="idx === 0"
|
||||
@click="moveReorderItem(idx, -1)"
|
||||
>上移</n-button>
|
||||
<n-button
|
||||
size="tiny"
|
||||
quaternary
|
||||
:disabled="idx === reorderList.length - 1"
|
||||
@click="moveReorderItem(idx, 1)"
|
||||
>下移</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</VueDraggable>
|
||||
</n-spin>
|
||||
<template #footer>
|
||||
<n-space justify="end">
|
||||
<n-button @click="reorderVisible = false">取消</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="reorderSubmitLoading"
|
||||
:disabled="!reorderList.length"
|
||||
@click="submitReorder"
|
||||
>保存顺序</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<!-- 新增/编辑(单工序字段;顺序请用上方重排弹窗) -->
|
||||
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px">
|
||||
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="120px">
|
||||
<n-grid :cols="2">
|
||||
@ -286,8 +367,9 @@
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
<!-- 顺序请走订单行「编辑」重排,避免单条改 sort 撞号断链 -->
|
||||
<n-form-item label="工序顺序" path="sort">
|
||||
<n-input v-model:value="formData.sort" placeholder="请输入工序顺序" />
|
||||
<n-input v-model:value="formData.sort" placeholder="请通过订单编辑调整顺序" disabled />
|
||||
</n-form-item>
|
||||
</n-gi>
|
||||
<n-gi>
|
||||
@ -590,7 +672,9 @@ import {
|
||||
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
|
||||
EllipseOutline, TimeOutline, PrintOutline, SyncOutline, PeopleOutline,
|
||||
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
|
||||
MenuOutline,
|
||||
} from '@vicons/ionicons5'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import {
|
||||
orderProcessPlanApi,
|
||||
toOrderProcessPlanPayload,
|
||||
@ -632,6 +716,15 @@ const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detailData = ref<OrderProcessPlanVO | null>(null)
|
||||
|
||||
/** 订单级工序重排弹窗状态 */
|
||||
const reorderVisible = ref(false)
|
||||
const reorderLoading = ref(false)
|
||||
const reorderSubmitLoading = ref(false)
|
||||
const reorderOrderItemId = ref<number | null>(null)
|
||||
const reorderOrderMeta = ref<OrderProcessPlanVO | null>(null)
|
||||
/** 弹窗内可拖拽的工序列表(按当前展示顺序) */
|
||||
const reorderList = ref<ProcessPlanItemVO[]>([])
|
||||
|
||||
const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const formRef = ref()
|
||||
@ -1032,6 +1125,79 @@ async function openDetail(order: OrderProcessPlanVO) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 打开订单级工序顺序调整(拉详情,按 operNumber 排序后供拖拽) */
|
||||
async function openReorder(order: OrderProcessPlanVO) {
|
||||
if (!order.orderItemId) {
|
||||
message.warning('缺少生产订单id')
|
||||
return
|
||||
}
|
||||
reorderVisible.value = true
|
||||
reorderLoading.value = true
|
||||
reorderOrderItemId.value = order.orderItemId
|
||||
reorderOrderMeta.value = order
|
||||
reorderList.value = []
|
||||
try {
|
||||
const detail = await orderProcessPlanApi.detail(order.orderItemId)
|
||||
reorderOrderMeta.value = detail
|
||||
// 与看板展开区一致:按 operNumber(后端 sort)升序
|
||||
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
|
||||
if (!reorderList.value.length) {
|
||||
message.warning('当前订单暂无工序计划')
|
||||
}
|
||||
} catch {
|
||||
// 详情失败时降级用列表行数据,仍可本地排序后提交
|
||||
reorderList.value = sortedProcessList(order.processList).map((p) => ({ ...p }))
|
||||
} finally {
|
||||
reorderLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function resetReorder() {
|
||||
reorderOrderItemId.value = null
|
||||
reorderOrderMeta.value = null
|
||||
reorderList.value = []
|
||||
reorderSubmitLoading.value = false
|
||||
}
|
||||
|
||||
/** 上移 / 下移(delta: -1 上移,+1 下移) */
|
||||
function moveReorderItem(index: number, delta: number) {
|
||||
const next = index + delta
|
||||
if (next < 0 || next >= reorderList.value.length) return
|
||||
const list = [...reorderList.value]
|
||||
const [item] = list.splice(index, 1)
|
||||
list.splice(next, 0, item)
|
||||
reorderList.value = list
|
||||
}
|
||||
|
||||
/** 提交重排:只传 orderItemId + planId 顺序 */
|
||||
async function submitReorder() {
|
||||
if (!reorderOrderItemId.value) {
|
||||
message.warning('缺少生产订单id')
|
||||
return
|
||||
}
|
||||
if (!reorderList.value.length) {
|
||||
message.warning('暂无工序可保存')
|
||||
return
|
||||
}
|
||||
const missing = reorderList.value.some((p) => p.planId == null)
|
||||
if (missing) {
|
||||
message.error('存在缺少 planId 的工序,请刷新后重试')
|
||||
return
|
||||
}
|
||||
reorderSubmitLoading.value = true
|
||||
try {
|
||||
await orderProcessPlanApi.reorder({
|
||||
orderItemId: reorderOrderItemId.value,
|
||||
items: reorderList.value.map((p) => ({ planId: p.planId! })),
|
||||
})
|
||||
message.success('工序顺序已更新')
|
||||
reorderVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
reorderSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
modalTitle.value = '新增工序计划'
|
||||
Object.assign(formData, defaultFormData)
|
||||
@ -1512,4 +1678,83 @@ onMounted(loadData)
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.reorder-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.reorder-empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: #8c8c8c;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.reorder-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
max-height: 420px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.reorder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: #fafbfc;
|
||||
border: 1px solid #eef0f3;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.reorder-drag-handle {
|
||||
cursor: grab;
|
||||
flex-shrink: 0;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.reorder-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.reorder-index {
|
||||
width: 22px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #2080f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reorder-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reorder-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.reorder-title em {
|
||||
font-style: normal;
|
||||
margin-right: 6px;
|
||||
color: #2080f0;
|
||||
}
|
||||
|
||||
.reorder-sub {
|
||||
margin-top: 2px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
:data="AssingWorkTableData"
|
||||
:loading="tableLoading"
|
||||
:row-key="RowKey"
|
||||
:scroll-x="600"
|
||||
:scroll-x="1800"
|
||||
:max-height="1000"
|
||||
size="small"
|
||||
style="margin-bottom:15px"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user