This commit is contained in:
andy 2026-08-07 14:58:46 +08:00
commit fed651a202
32 changed files with 3152 additions and 716 deletions

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ AGENTS.md
*.njsproj
*.sln
*.sw?
/src/views/biz/tool/usage.vue

View File

@ -72,10 +72,11 @@ export const assingWorkDetailApi = {
//根据派工id获取详情信息
selectAssingWorkDetailListByAssingWorkId(assingWorkId:number) {
selectAssingWorkDetailListByAssingWorkId(params:{page?:number;pageSize?:number;assingWorkId?:number}) {
return request({
url:`/biz/assingWorkDetail/getAssingWorkDetailList/${assingWorkId}`,
method:"get"
url:`/biz/assingWorkDetail/getAssingWorkDetailList`,
method:"get",
params
})
}
}

View File

@ -22,6 +22,14 @@ export interface BasicProcessPlan {
departmentName?: string
sectionId?: number | null
workShopId?: number | null
sectionName?: string
workShopName?: string
operDescription?: string
createBy?: number

View File

@ -31,15 +31,51 @@ export interface DeviceRealtimeVO {
displayName: string
synEquipId: number
bound: boolean
/** connected机台是否在线 */
connected?: number | null
/** cnc_status控制器状态 */
cncStatus?: number | null
cncStatusText?: string
/** part_count目前工件数 */
partCount?: number | null
/** part_count_required需求工件数 */
partCountRequired?: number | null
/** main_program各轴群的当前加工号码 */
mainProgram?: string | null
/** feedrate_act轴群的实际进给速率 */
feedrateAct?: number | null
/** feedrate_override轴群的实际进给倍率 */
feedrateOverride?: number | null
/** speed_act轴群的加工主轴转速 */
speedAct?: number | null
/** speed_cmd轴群的加工主轴转速命令 */
speedCmd?: number | null
/** load轴群的加工主轴负载率 */
load?: number | null
/** speed_override轴群的加工主轴速度百分比 */
speedOverride?: number | null
/** tool_id各轴群的加工主轴刀号 */
toolId?: string | null
/** torque_load轴向负载率 */
torqueLoad?: number | null
/** span_cutting_cycle单件加工时间 */
spanCuttingCycle?: number | null
/** span_cutting_acc累计加工时间 */
spanCuttingAcc?: number | null
/** span_power_on_acc累计开机时间 */
spanPowerOnAcc?: number | null
/** span_cutting_install安装累计加工时间 */
spanCuttingInstall?: number | null
/** span_power_on开机时间 */
spanPowerOn?: number | null
/** alarm_current警报相关 */
alarmCount?: number
alarmMessages?: string[]
dataTimestamp?: number
sectionId?: number | null
sectionName?: string
workshopId?: number | null
workshopName?: string
}

View File

@ -14,15 +14,15 @@ export interface MaintenanceRecord {
executorId?: number
startTime?: string
startTime?: string | number
endTime?: string
endTime?: string | number
recordStatus?: string
createTime?: string
createTime?: string | number
updateTime?: string
updateTime?: string | number
}

View File

@ -101,3 +101,11 @@ export function exportNccode(params:any) {
params
})
}
//加载全部NC代码
export function loadAllNcCode() {
return request({
url: `biz/ncCode/loadAllNcCode`,
method: 'get'
})
}

View File

@ -52,6 +52,9 @@ export interface OrderItem {
routeCode?: string
workshopId?: number | null
/** 开工后关联的执行侧 mes_order_item.id */
orderItemId?: number | null
}
export interface KingdeePrdMo {
@ -131,8 +134,8 @@ export const orderItemApi = {
return request({ url: `/biz/orderItem/${id}/startWork`, method: 'post' })
},
/** 按工艺路线编码查询工艺路线(含工序);可带 orderItemId 精确匹配 */
getRoute(params: { routeCode?: string; orderItemId?: number | string }) {
/** 按工艺路线编码查询orderItemId/planId 均为计划订单 idmes_order_item_plan.id */
getRoute(params: { routeCode?: string; orderItemId?: number | string; planId?: number | string }) {
return request({ url: '/biz/orderItem/route', method: 'get', params })
},
//根据生产编号查询预生产订单

View File

@ -70,6 +70,9 @@ export interface ProcessPlanItemVO {
workOrderCode?: string
/** 拆单来源工序计划ID */
splitFromId?: number | null
processCode?: string
operNumber?: number
@ -150,6 +153,9 @@ export interface OrderProcessPlanVO {
orderItemId?: number
/** 工单号(列表按工单分组) */
workOrderCode?: string
mainCode?: string
materialCode?: string
@ -290,7 +296,7 @@ export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderP
const displayKeys = [
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'workshopName',
'mainCode', 'materialCode', 'materialName', 'orderCode', 'routeCode', 'workshopName', 'workOrderCode',
'actualStartTime', 'actualEndTime', 'completedQty', 'orderStatus', 'currentProcessName',
@ -302,7 +308,7 @@ export function toOrderProcessPlanPayload(data: Record<string, unknown>): OrderP
'transferOutQty', 'scrapQty', 'reworkQty', 'assign', 'assignByName', 'assignTime',
'processStatus', 'prevPlanId', 'nextPlanId', 'assignWorkList',
'processStatus', 'prevPlanId', 'nextPlanId', 'assignWorkList', 'splitFromId',
]
@ -354,9 +360,9 @@ export const orderProcessPlanApi = {
// 获取详情(路径参数为生产订单 id
// 获取详情(路径参数为生产订单 id;可选 workOrderCode 隔离拆单工单
detail(orderItemId: number | string) {
detail(orderItemId: number | string, workOrderCode?: string) {
return request<OrderProcessPlanVO>({
@ -364,6 +370,8 @@ export const orderProcessPlanApi = {
method: 'get',
params: workOrderCode ? { workOrderCode } : undefined,
})
},
@ -493,6 +501,81 @@ export const orderProcessPlanApi = {
})
},
/** 工序工单拆单预览 */
splitPreview(planId: number | string) {
return request<ProcessPlanSplitPreview>({
url: `/biz/orderProcessPlan/split/preview/${planId}`,
method: 'get',
})
},
/** 工序工单拆单 */
split(data: ProcessPlanSplitPayload) {
return request<ProcessPlanSplitResult>({
url: '/biz/orderProcessPlan/split',
method: 'post',
data,
})
},
}
/** 拆单预览 */
export interface ProcessPlanSplitPreview {
planId?: number
workOrderCode?: string
orderItemId?: number
orderItemName?: string
/** 物料编码,用于查金蝶工艺路线 */
materialCode?: string
processCount?: number
processNames?: string[]
/** 工单计划数量 */
quantity?: number
/** 已进入生产数量(不可拆)= MAX(各工序投入) */
investedQty?: number
/** 可拆 = 计划 已进入生产数量 */
splittableQty?: number
canSplit?: boolean
reason?: string | null
}
/** 拆单请求quantities[0]=原单保留(≥不可拆),其余=新工单;新工艺二选一 */
export interface ProcessPlanSplitPayload {
planId: number
quantities: number[]
/** kingdee=金蝶工艺路线assemble=工序表组装 */
routeMode: 'kingdee' | 'assemble'
/** routeMode=kingdee金蝶工艺路线编码 FNumber */
routingNo?: string | null
/** routeMode=assemble完整工序明细推荐 */
assembleSteps?: ProcessPlanSplitStep[]
/** 兼容旧版:仅工序 id */
processIds?: number[]
}
/** 拆单组装工序行(对齐金蝶工艺路线主要字段) */
export interface ProcessPlanSplitStep {
processId?: number | null
operNumber?: number | null
processCode?: string | null
processName?: string | null
operDescription?: string | null
sectionId?: number | null
workshopId?: number | null
activityUnit?: string | null
optCtrlCodeName?: string | null
activityQty?: number | null
qualityInspection?: number | null
storageEntry?: number | null
procurement?: number | null
isOutsource?: number | null
}
/** 拆单结果 */
export interface ProcessPlanSplitResult {
source?: ProcessPlanItemVO
created?: ProcessPlanItemVO[]
}
/** 委外可选工序项 */

View File

@ -0,0 +1,72 @@
import { request } from '@/utils/request'
// 数据权限配置 类型定义
export interface PermissionConfig {
id?: number
mapperKey?: string
controlMethod?: string
permissionControl?: string
}
// 数据权限配置 API
export const permissionConfigApi = {
// 分页查询
page(params: { page: number; pageSize: number; mapperKey?: string; controlMethod?: string }) {
return request({ url: '/biz/permissionConfig/page', method: 'get', params })
},
// 新增
options() {
return request({ url: '/biz/permissionConfig/options', method: 'get' })
},
// 获取详情
detail(id: string) {
return request({ url: `/biz/permissionConfig/${id}`, method: 'get' })
},
// 新增
create(data: PermissionConfig) {
return request({ url: '/biz/permissionConfig', method: 'post', data })
},
// 修改
update(data: PermissionConfig) {
return request({ url: '/biz/permissionConfig', method: 'put', data })
},
// 删除
delete(ids: string[]) {
return request({ url: `/biz/permissionConfig/${ids.join(',')}`, method: 'delete' })
},
// 导出
export(params?: { ids?: string[]; mapperKey?: string; controlMethod?: string }) {
const p: Record<string, any> = {}
if (params?.ids?.length) p.ids = params.ids.join(',')
if (params?.mapperKey !== undefined && params?.mapperKey !== null) p.mapperKey = params.mapperKey
if (params?.controlMethod !== undefined && params?.controlMethod !== null) p.controlMethod = params.controlMethod
return request({ url: `/biz/permissionConfig/export`, method: 'get', params: p, responseType: 'blob' })
},
// 导入
importData(file: File) {
const formData = new FormData()
formData.append('file', file)
return request<{ success: number; fail: number; errors: string[] }>({
url: `/biz/permissionConfig/import`,
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
})
},
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/permissionConfig/template`, method: 'get', responseType: 'blob' })
}
}

View File

@ -88,5 +88,43 @@ export const processRouteApi = {
// 下载导入模板
downloadTemplate() {
return request({ url: `/biz/processRoute/template`, method: 'get', responseType: 'blob' })
}
},
/** 按物料编码查询金蝶工艺路线选项(含工艺路线编码) */
kingdeeSelect(materialCode: string) {
return request<ProcessRouteSelectOption[]>({
url: '/biz/processRoute/kingdee/select',
method: 'get',
params: { materialCode },
})
},
}
/** 金蝶工艺路线下拉选项 */
export interface ProcessRouteSelectOption {
routingNo?: string
materialCode?: string
materialName?: string
stepCount?: number
processNames?: string[]
/** 工序明细 */
steps?: KingdeeProcessRouteStep[]
}
/** 金蝶工艺路线工序行 */
export interface KingdeeProcessRouteStep {
routingNo?: string
materialCode?: string
materialName?: string
operNumber?: number
processCode?: string
processName?: string
operDescription?: string
workCenterName?: string
departmentName?: string
optCtrlCodeName?: string
activityUnit?: string
activityQty?: number
qualityInspection?: number
storageEntry?: number
}

View File

@ -106,7 +106,7 @@ export const qualityTestingApi = {
},
//加载质检单
loadQualityTestingList(assingId:number) {
return request({url:`/mes/qc/loadQualityTestingList/${assingId}`,method:'get'})
loadQualityTestingList(params:{page?:number,pageSize?:number,assingId?:number}) {
return request({url:`/mes/qc/loadQualityTestingList`,method:'get', params})
}
}

View File

@ -99,7 +99,8 @@ export const submitLogApi = {
},
//加载汇报列表
loadSumbitLog(assingId?:number){
return request({url:`/mes/report/loadSumbitLog/${assingId}`,method:'get'})
loadSumbitLog(params:{assingId:number,page?:number,pageSize?:number}){
console.log(params)
return request({url:`/mes/report/loadSumbitLog`,method:'get',params})
}
}

View File

@ -201,7 +201,27 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/views/test/test/index.vue'),
meta: { title: '测试菜单', icon: 'StarOutline' }
},
// 设备维修保养记录
{
path: 'biz/maintenanceRecord',
name: 'MaintenanceRecord',
component: () => import('@/views/biz/maintenanceRecord/index.vue'),
meta: { title: '设备维修保养记录', icon: 'BuildOutline' }
},
// 设备状态监控(独立页)
{
path: 'biz/device/board',
name: 'deviceRealtimeBoard',
component: () => import('@/views/biz/device/board.vue'),
meta: { title: '设备状态监控', icon: 'PulseOutline' }
},
// 开发工具
{
path: 'biz/permissionConfig',
name: 'permissionConfig',
component: () => import('@/views/biz/permissionConfig/index.vue'),
meta: { title: '数据权限配置', icon: 'ListOutline' }
},
{
path: 'tool/gen',
name: 'ToolGen',

View File

@ -35,8 +35,12 @@
value: '2'
},
{
label: '其他异常',
label: '设备异常',
value: '3'
},
{
label: '其他异常',
value: '4'
}
]
@ -170,16 +174,11 @@
<n-form inline :model="searchForm" label-placement="left">
<n-grid :x-gap="8" :cols="4">
<n-gi>
<n-form-item label="异常编号">
<n-input v-model:value="searchForm.abnormalCode" placeholder="请输入异常编号" clearable style="width: 200px"/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="操作人员">
<n-form-item label="报警异常">
<n-select
v-model:value="searchForm.abnormalType"
:options="abnormalTypes"
placeholder="请选择异常类型"
placeholder="请选择报警异常"
clearable
style="width: 200px"
/>

File diff suppressed because it is too large Load Diff

View File

@ -379,7 +379,13 @@ const columns: DataTableColumns<Device> = [
title: '新代ID', key: 'synEquipId', width: 90,
render: (row) => row.synEquipId ?? '-'
},
{title: '设备类型', key: 'deviceType'},
{title: '设备类型', key: 'deviceTypeId',
render: (row) =>{
let val = row.deviceTypeId
const opt = deviceTypeList.value.find(o => o.value === val || String(o.value) === String(val))
return h(NTag, {type: opt.class, size: 'small'}, {default: () => opt.label})
}
},
{title: '设备负责人', key: 'deviceManagerName'},
{title: '所属工段', key: 'sectionName'},
{title: '设备型号', key: 'spec'},

View File

@ -50,7 +50,7 @@
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="(row) => row.recordId"
:row-key="(row: MaintenanceRecord) => row.recordId"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@ -59,31 +59,31 @@
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
<n-form ref="formRef" :model="formData" label-placement="left" label-width="100px">
<n-form-item label="设备ID关联mes_equipment设备表主键" path="equipmentId">
<n-input v-model:value="formData.equipmentId" placeholder="请输入设备ID关联mes_equipment设备表主键" />
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 560px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-form-item label="设备ID" path="equipmentId">
<n-input v-model:value="formData.equipmentId" placeholder="请输入设备ID" />
</n-form-item>
<n-form-item label="记录类型maintain=保养repair=维修" path="recordType">
<n-select v-model:value="formData.recordType" placeholder="请选择记录类型maintain=保养repair=维修" :options="[]" />
<n-form-item label="记录类型" path="recordType">
<n-select v-model:value="formData.recordType" placeholder="请选择记录类型" :options="recordTypeOptions" />
</n-form-item>
<n-form-item label="故障现象/保养项目详情" path="itemContent">
<n-input v-model:value="formData.itemContent" type="textarea" placeholder="请输入故障现象/保养项目详情" />
<n-form-item label="故障现象" path="itemContent">
<n-input v-model:value="formData.itemContent" type="textarea" placeholder="请输入故障现象/保养项目" :autosize="{ minRows: 2, maxRows: 4 }" />
</n-form-item>
<n-form-item label="维修处理措施/保养执行内容" path="handleContent">
<n-input v-model:value="formData.handleContent" type="textarea" placeholder="请输入维修处理措施/保养执行内容" />
<n-form-item label="处理措施" path="handleContent">
<n-input v-model:value="formData.handleContent" type="textarea" placeholder="请输入维修处理措施/保养执行内容" :autosize="{ minRows: 2, maxRows: 4 }" />
</n-form-item>
<n-form-item label="执行人ID关联sys_user系统用户表主键" path="executorId">
<n-input v-model:value="formData.executorId" placeholder="请输入执行人ID关联sys_user系统用户表主键" />
<n-form-item label="执行人" path="executorId">
<n-input v-model:value="formData.executorId" placeholder="请输入执行人ID" />
</n-form-item>
<n-form-item label="维保作业开始时间" path="startTime">
<n-date-picker v-model:value="formData.startTime" type="datetime" clearable style="width: 100%" />
<n-form-item label="开始时间" path="startTime">
<n-date-picker v-model:value="formData.startTime" type="datetime" placeholder="请选择开始时间" clearable style="width: 100%" />
</n-form-item>
<n-form-item label="维保作业结束时间,待处理单据为空" path="endTime">
<n-date-picker v-model:value="formData.endTime" type="datetime" clearable style="width: 100%" />
<n-form-item label="结束时间" path="endTime">
<n-date-picker v-model:value="formData.endTime" type="datetime" placeholder="请选择结束时间" clearable style="width: 100%" />
</n-form-item>
<n-form-item label="单据状态pending=待处理finished=已完成" path="recordStatus">
<n-select v-model:value="formData.recordStatus" placeholder="请选择单据状态pending=待处理finished=已完成" :options="recordStatusOptions" />
<n-form-item label="单据状态" path="recordStatus">
<n-select v-model:value="formData.recordStatus" placeholder="请选择单据状态" :options="recordStatusOptions" />
</n-form-item>
</n-form>
<template #footer>
@ -132,7 +132,6 @@ import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
import { maintenanceRecordApi, type MaintenanceRecord } from '@/api/maintenanceRecord'
import { dictDataApi } from '@/api/org'
const message = useMessage()
const dialog = useDialog()
@ -145,7 +144,7 @@ const searchForm = reactive({
//
const tableData = ref<MaintenanceRecord[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const selectedIds = ref<string[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
@ -171,8 +170,15 @@ const defaultFormData: MaintenanceRecord = {
}
const formData = reactive<MaintenanceRecord>({ ...defaultFormData })
// //使
const recordStatusOptions = ref<{ label: string; value: any }[]>([])
//
const recordTypeOptions = ref<{ label: string; value: any }[]>([
{ label: '保养', value: 'maintain' },
{ label: '维修', value: 'repair' }
])
const recordStatusOptions = ref<{ label: string; value: any }[]>([
{ label: '待处理', value: 'pending' },
{ label: '已完成', value: 'finished' }
])
//
const formRules = {
@ -185,23 +191,27 @@ const formRules = {
//
const columns: DataTableColumns<MaintenanceRecord> = [
{ type: 'selection' },
{ title: '维保记录编号', key: 'recordId' },
{ title: '设备ID', key: 'equipmentId' },
{ title: '记录类型', key: 'recordType' },
{ title: '故障现象/保养项目', key: 'itemContent' },
{ title: '维修处理措施/保养执行内容', key: 'handleContent' },
{ title: '执行人ID', key: 'executorId' },
{ title: '维保作业开始时间', key: 'startTime' },
{ title: '维保作业结束时间', key: 'endTime' },
{ title: '单据状态', key: 'recordStatus',
{ title: '维保记录编号', key: 'recordId', width: 160 },
{ title: '设备ID', key: 'equipmentId', width: 100 },
{ title: '记录类型', key: 'recordType', width: 100,
render(row) {
const opt = recordTypeOptions.value.find(o => o.value === row.recordType)
return opt ? opt.label : (row.recordType ?? '-')
}
},
{ title: '故障现象/保养项目', key: 'itemContent', ellipsis: { tooltip: true } },
{ title: '维修处理措施/保养执行内容', key: 'handleContent', ellipsis: { tooltip: true } },
{ title: '执行人ID', key: 'executorId', width: 100 },
{ title: '维保作业开始时间', key: 'startTime', width: 170 },
{ title: '维保作业结束时间', key: 'endTime', width: 170 },
{ title: '单据状态', key: 'recordStatus', width: 100,
render(row) {
const val = row.recordStatus
const opt = recordStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
return opt ? opt.label : (val ?? '-')
}
},
{ title: '单据创建时间', key: 'createTime', width: 180 },
{ title: '单据最后更新时间', key: 'updateTime', width: 180 },
{ title: '单据创建时间', key: 'createTime', width: 170 },
{
title: '操作',
key: 'actions',
@ -229,8 +239,8 @@ async function loadData() {
pageSize: pagination.pageSize,
recordId: searchForm.recordId || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
tableData.value = res.records || res.list || []
pagination.itemCount = res.total || 0
} finally {
loading.value = false
}
@ -263,7 +273,7 @@ function handlePageSizeChange(pageSize: number) {
//
function handleCheck(keys: Array<string | number>) {
selectedIds.value = keys as number[]
selectedIds.value = keys.map(String)
}
//
@ -416,17 +426,8 @@ async function handleImportUpload({ file }: UploadCustomRequestOptions) {
}
}
//
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('sys_status')
recordStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: d.dictValue }))
} catch {}
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
@ -439,19 +440,3 @@ onMounted(() => {
margin-bottom: 16px;
}
</style>
<style scoped lang="scss">
.maintenance-page {
padding: 16px;
.search-card, .btn-card {
margin-bottom: 16px;
}
//
:deep(.el-card) {
border-radius: 8px;
}
//
:deep(.el-table__cell) {
padding: 12px 10px !important;
}
}
</style>

View File

@ -0,0 +1,52 @@
<template>
<div style="padding: 20px;">
<h2>设备维修保养记录 - 简易版</h2>
<n-button type="primary" :loading="loading" @click="loadData">加载数据</n-button>
<n-data-table :columns="columns" :data="tableData" :loading="loading" style="margin-top: 20px;" />
<pre style="margin-top: 20px; white-space: pre-wrap; background: #f5f5f5; padding: 10px; border-radius: 4px;">调试信息: {{ debug }}</pre>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { NButton, useMessage, type DataTableColumns } from 'naive-ui'
import { request } from '@/utils/request'
const message = useMessage()
const loading = ref(false)
const tableData = ref<any[]>([])
const debug = ref('')
const columns: DataTableColumns<any> = [
{ title: '记录编号', key: 'recordId' },
{ title: '设备ID', key: 'equipmentId' },
{ title: '记录类型', key: 'recordType' },
{ title: '内容', key: 'itemContent' },
{ title: '状态', key: 'recordStatus' },
{ title: '创建时间', key: 'createTime' },
]
async function loadData() {
loading.value = true
debug.value = '请求中...'
try {
const res = await request({
url: '/biz/maintenanceRecord/page',
method: 'get',
params: { page: 1, pageSize: 10 }
})
debug.value = '成功: ' + JSON.stringify(res).substring(0, 500)
tableData.value = res.records || res.list || []
message.success('加载成功')
} catch (err: any) {
debug.value = '失败: ' + (err?.message || String(err))
message.error('加载失败: ' + (err?.message || String(err)))
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
})
</script>

View File

@ -619,7 +619,7 @@ async function openRouteDrawer(row: any) {
try {
const data: any = await orderItemApi.getRoute({
routeCode: code,
orderItemId: row.id,
planId: row.id,
})
if (data) {
routeDetail.value = data
@ -1278,7 +1278,7 @@ function handleDelete(row: OrderItem) {
function handleStartWork(row: OrderItem) {
dialog.warning({
title: '提示',
content: `确定开工订单【${row.orderCode || row.name}】?开工后将生成工单。`,
content: `确定开工订单【${row.orderCode || row.name}】?开工后将生成执行订单与工单,派工请在工单侧操作`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {

View File

@ -62,7 +62,6 @@
<!-- 表头 -->
<div class="board-table-head">
<div class="col-check" />
<div class="col-expand" />
<div class="col-product">产品 / 工单编号</div>
<div class="col-qty">数量</div>
<div class="col-mode">模式</div>
@ -80,29 +79,13 @@
</div>
<div v-for="order in tableData" :key="orderRowKey(order)" class="order-block">
<div class="order-row" :class="{ expanded: isExpanded(order), 'derived-expanded': isDerivedExpanded(order) }">
<div class="order-row" :class="{ expanded: isExpanded(order) }">
<div class="col-check">
<n-checkbox
:checked="selectedOrderIds.includes(order.orderItemId!)"
@update:checked="(v: boolean) => toggleSelect(order.orderItemId!, v)"
:checked="selectedOrderIds.includes(orderRowKey(order))"
@update:checked="(v: boolean) => toggleSelect(orderRowKey(order), v)"
/>
</div>
<div class="col-expand">
<n-button
quaternary
circle
size="tiny"
title="展开返工单 / 拆分工单"
@click="toggleDerivedExpand(order)"
>
<template #icon>
<n-icon>
<RemoveOutline v-if="isDerivedExpanded(order)" />
<AddOutline v-else />
</n-icon>
</template>
</n-button>
</div>
<div class="col-product">
<div class="product-name">{{ order.materialName || '-' }}</div>
<div class="product-code">
@ -153,43 +136,6 @@
</div>
</div>
<div v-if="isDerivedExpanded(order)" class="order-derived-expand">
<div class="derived-section">
<div class="derived-section-head">
<span class="derived-section-title">返工单</span>
<n-button text type="primary" size="tiny" @click="onMoreSelect('createRework', order)">创建返工单</n-button>
</div>
<div v-if="!getDerivedOrders(order).rework.length" class="derived-empty">暂无返工单</div>
<div v-else class="derived-list">
<div
v-for="item in getDerivedOrders(order).rework"
:key="item.id"
class="derived-item"
>
<span class="derived-code">{{ item.code }}</span>
<span class="derived-meta">{{ item.quantity ?? '-' }} · {{ item.statusLabel }}</span>
</div>
</div>
</div>
<div class="derived-section">
<div class="derived-section-head">
<span class="derived-section-title">拆分工单</span>
<n-button text type="primary" size="tiny" @click="onMoreSelect('splitWorkOrder', order)">拆分工单</n-button>
</div>
<div v-if="!getDerivedOrders(order).split.length" class="derived-empty">暂无拆分工单</div>
<div v-else class="derived-list">
<div
v-for="item in getDerivedOrders(order).split"
:key="item.id"
class="derived-item"
>
<span class="derived-code">{{ item.code }}</span>
<span class="derived-meta">{{ item.quantity ?? '-' }} · {{ item.statusLabel }}</span>
</div>
</div>
</div>
</div>
<div v-if="isExpanded(order)" class="order-expand">
<div v-if="!(order.processList?.length)" class="expand-empty">暂无工序明细</div>
<div v-else class="card-flow-scroll">
@ -221,8 +167,6 @@
<!--模型抽屉 -->
<PlmModelDrawer ref="plmModelDrawerRef" />
<div class="board-pagination">
<n-pagination
v-model:page="pagination.page"
@ -407,6 +351,13 @@
</template>
</n-modal>
<!-- 工序工单拆单整单全部工序一起按数量拆 -->
<SplitProcessDialog
v-model:show="splitVisible"
:plan-id="splitPlanId"
@success="onSplitSuccess"
/>
<!-- 派工 -->
<n-modal v-model:show="dispatchmodal" title="工序派工" preset="card" style="width: 800px" :mask-closable="false">
<n-divider />
@ -664,13 +615,13 @@ import { ref, reactive, onMounted, h, computed, type Component } from 'vue'
import {
NButton, NSpace, NIcon, NTag, NSpin, NModal, NForm, NFormItem, NGrid, NGi,
NInput, NInputNumber, NDatePicker, NCheckbox, NEmpty, NPagination, NDropdown, NDescriptions,
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, useMessage, useDialog,
NDescriptionsItem, NAlert, NUpload, NText, NCard, NRadioGroup, NRadio, NDivider, useMessage, useDialog,
type UploadCustomRequestOptions, type FormRules,
} from 'naive-ui'
import {
AddOutline, RemoveOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
AddOutline, SearchOutline, RefreshOutline, CloudUploadOutline,
DownloadOutline, ChevronDownOutline, CheckmarkCircleOutline, CheckmarkCircle,
EllipseOutline, TimeOutline, PrintOutline, SyncOutline, PeopleOutline,
EllipseOutline, TimeOutline, PrintOutline, PeopleOutline,
GitBranchOutline, CloseCircleOutline, LockClosedOutline, TrashOutline, EyeSharp,
MenuOutline,
} from '@vicons/ionicons5'
@ -688,6 +639,7 @@ import {
import { useUserStore } from '@/stores/user'
import ProcessFlowStepper from './components/ProcessFlowStepper.vue'
import ProcessCard from './components/ProcessCard.vue'
import SplitProcessDialog from './components/SplitProcessDialog.vue'
import PlmModelDrawer from '@/components/PlmModelDrawer.vue'
import type { PlmModelOpenContext } from '@/api/plmModel'
import Pgitem from './pgitem.vue'
@ -706,9 +658,8 @@ const searchForm = reactive({
})
const tableData = ref<OrderProcessPlanVO[]>([])
const expandedIds = ref<Set<number>>(new Set())
const derivedExpandedIds = ref<Set<number>>(new Set())
const selectedOrderIds = ref<number[]>([])
const expandedIds = ref<Set<string>>(new Set())
const selectedOrderIds = ref<string[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 10, itemCount: 0 })
@ -716,6 +667,10 @@ const detailVisible = ref(false)
const detailLoading = ref(false)
const detailData = ref<OrderProcessPlanVO | null>(null)
/** 工序工单拆单 */
const splitVisible = ref(false)
const splitPlanId = ref<number | null>(null)
/** 订单级工序重排弹窗状态 */
const reorderVisible = ref(false)
const reorderLoading = ref(false)
@ -742,7 +697,7 @@ const dispatchLoading = ref(false)
const dispatchformRef = ref()
const dispatchform = reactive<any>({
id: '', name: '', beginTime: '', endTime: '', quantity: '', sectionId: null as number | null, workshopId: null as number | null,
list: [{ sectionId:'',deviceId:'', quantity: '' }],
list: [{ sectionId:'',deviceId:'', quantity: '', ncId:'' }],
})
const dispatchrules = {}
@ -835,7 +790,6 @@ const moreOptions = [
label: '派生操作',
key: 'group-derived',
children: [
{ label: '创建返工单', key: 'createRework', icon: dropdownIcon(SyncOutline) },
{ label: '创建工序委外', key: 'createOutsource', icon: dropdownIcon(PeopleOutline) },
{ label: '拆分工单', key: 'splitWorkOrder', icon: dropdownIcon(GitBranchOutline) },
{ label: '汇报详情', key: 'reportDetailView', icon: dropdownIcon(EyeSharp) },
@ -860,57 +814,78 @@ const moreOptions = [
},
]
/** 列表行主键:按工单号(拆单后同订单多工单分行) */
function orderRowKey(row: OrderProcessPlanVO) {
return row.orderItemId ?? `order-${row.mainCode}-${formatWorkOrderCodes(row)}`
if (row.workOrderCode) return row.workOrderCode
return `oi-${row.orderItemId ?? 'x'}`
}
/** 汇总展示订单下各道工序的主工单号 */
/** 订单级工单号展示 */
function formatWorkOrderCodes(order?: OrderProcessPlanVO | null) {
const codes = (order?.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
return codes.length ? codes.join('、') : '-'
if (order?.workOrderCode) return order.workOrderCode
const codes = (order?.processList ?? [])
.map((p) => p.workOrderCode)
.filter((c): c is string => !!c)
const unique = [...new Set(codes)]
return unique.length ? unique.join('、') : '-'
}
function sortedProcessList(list?: ProcessPlanItemVO[]) {
return [...(list ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
return [...(list ?? [])].sort((a, b) => {
const oa = a.operNumber ?? 0
const ob = b.operNumber ?? 0
if (oa !== ob) return oa - ob
return (a.planId ?? 0) - (b.planId ?? 0)
})
}
function isExpanded(order: OrderProcessPlanVO) {
return order.orderItemId != null && expandedIds.value.has(order.orderItemId)
}
function isDerivedExpanded(order: OrderProcessPlanVO) {
return order.orderItemId != null && derivedExpandedIds.value.has(order.orderItemId)
return expandedIds.value.has(orderRowKey(order))
}
function toggleExpand(order: OrderProcessPlanVO) {
if (order.orderItemId == null) return
const key = orderRowKey(order)
const next = new Set(expandedIds.value)
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
else next.add(order.orderItemId)
if (next.has(key)) next.delete(key)
else next.add(key)
expandedIds.value = next
}
function toggleDerivedExpand(order: OrderProcessPlanVO) {
if (order.orderItemId == null) return
const next = new Set(derivedExpandedIds.value)
if (next.has(order.orderItemId)) next.delete(order.orderItemId)
else next.add(order.orderItemId)
derivedExpandedIds.value = next
/** 整单拆分入口:有计划数量且未全部完工即可点开 */
function canSplitWorkOrder(order: OrderProcessPlanVO) {
const qty = order.quantity ?? 0
if (qty <= 0) return false
if (order.orderStatus === 2) return false
return (order.processList?.length ?? 0) > 0
}
/** 派生工单(返工 / 拆分),待后端字段接入后替换占位逻辑 */
function getDerivedOrders(order: OrderProcessPlanVO) {
const raw = order as OrderProcessPlanVO & {
reworkOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
splitOrders?: Array<{ id: string | number; code?: string; quantity?: number; statusLabel?: string }>
function openSplitFromOrder(order: OrderProcessPlanVO) {
if (!hasPermission('biz:orderProcessPlan:edit')) {
message.warning('无拆单权限')
return
}
return {
rework: raw.reworkOrders ?? [],
split: raw.splitOrders ?? [],
const list = sortedProcessList(order.processList)
const seed = list.find((p) => p.planId != null)
if (!seed?.planId) {
message.warning('当前工单暂无工序,无法拆单')
return
}
if (!canSplitWorkOrder(order)) {
message.warning('该工单不可拆(无数量或已完工)')
return
}
splitPlanId.value = seed.planId
splitVisible.value = true
}
function onSplitSuccess() {
loadData()
if (detailVisible.value && detailData.value?.orderItemId) {
openDetail(detailData.value)
}
}
function toggleSelect(id: number, checked: boolean) {
function toggleSelect(id: string, checked: boolean) {
if (checked) selectedOrderIds.value = [...selectedOrderIds.value, id]
else selectedOrderIds.value = selectedOrderIds.value.filter(v => v !== id)
}
@ -982,7 +957,7 @@ function handlePageSizeChange(pageSize: number) {
function onBatchSelect(key: string) {
if (key === 'expandAll') {
expandedIds.value = new Set(tableData.value.map(o => o.orderItemId!).filter(Boolean))
expandedIds.value = new Set(tableData.value.map(o => orderRowKey(o)))
} else if (key === 'collapseAll') {
expandedIds.value = new Set()
}
@ -997,11 +972,13 @@ function onMoreSelect(key: string, order: OrderProcessPlanVO) {
openReportDetail(order)
return;
}
if (key === 'splitWorkOrder') {
openSplitFromOrder(order)
return
}
const labels: Record<string, string> = {
print: '打印',
createRework: '创建返工单',
splitWorkOrder: '拆分工单',
withdraw: '撤回',
freeze: '冻结工单',
delete: '删除',
@ -1117,7 +1094,7 @@ async function openDetail(order: OrderProcessPlanVO) {
detailLoading.value = true
detailData.value = null
try {
detailData.value = await orderProcessPlanApi.detail(order.orderItemId)
detailData.value = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
} catch {
detailData.value = order
} finally {
@ -1137,12 +1114,12 @@ async function openReorder(order: OrderProcessPlanVO) {
reorderOrderMeta.value = order
reorderList.value = []
try {
const detail = await orderProcessPlanApi.detail(order.orderItemId)
const detail = await orderProcessPlanApi.detail(order.orderItemId, order.workOrderCode)
reorderOrderMeta.value = detail
// operNumber sort
// operNumber
reorderList.value = sortedProcessList(detail.processList).map((p) => ({ ...p }))
if (!reorderList.value.length) {
message.warning('当前单暂无工序计划')
message.warning('当前单暂无工序计划')
}
} catch {
//
@ -1267,11 +1244,11 @@ function disphand(process: ProcessPlanItemVO, order?: OrderProcessPlanVO) {
dispatchform.sort = entity.sort
dispatchform.sectionId = entity.sectionId ?? process.sectionId ?? null
dispatchform.workshopId = entity.workshopId ?? process.workshopId ?? order?.workshopId ?? null
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '' }]
dispatchform.list = [{ sectionId: '', deviceId: '', quantity: remainQty || '',ncId:'' }]
}
function addpgnum() {
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '' })
dispatchform.list.push({ sectionId: '', deviceId: '', quantity: '',ncId:'' })
}
function deletenum(index: number) {
@ -1393,9 +1370,8 @@ onMounted(loadData)
.board-table-head,
.order-row {
display: grid;
/* 勾选 | 展开 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
/* 勾选 | 产品工单 | 数量 | 模式 | 齐套率 | 工序轴 | 计划时间 | 生命周期 | 操作 */
grid-template-columns:
40px
40px
minmax(220px, 1.6fr)
64px
@ -1408,7 +1384,7 @@ onMounted(loadData)
align-items: center;
gap: 8px;
padding: 0 16px;
min-width: 1320px;
min-width: 1280px;
}
.board-table-head {
@ -1438,16 +1414,7 @@ onMounted(loadData)
background: rgba(32, 128, 240, 0.04);
}
.order-row.derived-expanded {
background: rgba(24, 160, 88, 0.04);
}
.order-row.expanded.derived-expanded {
background: linear-gradient(90deg, rgba(24, 160, 88, 0.04) 0%, rgba(32, 128, 240, 0.04) 100%);
}
.col-check,
.col-expand {
.col-check {
display: flex;
justify-content: center;
flex-shrink: 0;
@ -1530,74 +1497,11 @@ onMounted(loadData)
}
.order-expand {
padding: 12px 16px 16px 68px;
padding: 12px 16px 16px 56px;
background: linear-gradient(180deg, rgba(32, 128, 240, 0.03) 0%, #fff 100%);
border-top: 1px dashed #e8eaed;
}
.order-derived-expand {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
padding: 12px 16px 12px 68px;
background: linear-gradient(180deg, rgba(24, 160, 88, 0.03) 0%, #fff 100%);
border-top: 1px dashed #e8eaed;
}
.derived-section {
min-width: 0;
padding: 10px 12px;
background: #fff;
border: 1px solid #eef0f3;
border-radius: 8px;
}
.derived-section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.derived-section-title {
font-size: 13px;
font-weight: 600;
color: #1a1a1a;
}
.derived-empty {
font-size: 12px;
color: #8c8c8c;
padding: 4px 0;
}
.derived-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.derived-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 6px 8px;
background: #fafbfc;
border-radius: 6px;
font-size: 12px;
}
.derived-code {
font-weight: 500;
color: #333;
}
.derived-meta {
color: #8c8c8c;
white-space: nowrap;
}
.card-flow-scroll,
.detail-cards {
display: flex;

View File

@ -79,6 +79,13 @@
ghost
@click="emit('assign', process)"
>派工</n-button>
<n-button
v-if="canSplit"
size="tiny"
type="primary"
ghost
@click="emit('split', process)"
>拆单</n-button>
<n-button
v-if="canEdit"
size="tiny"
@ -86,7 +93,6 @@
@click="emit('edit', process)"
>编辑</n-button>
<n-button size="tiny" type="info" ghost @click="emit('viewModel', process)">模型</n-button>
<n-button size="tiny" quaternary disabled>返工</n-button>
<n-button
v-if="canOutsource"
size="tiny"
@ -135,6 +141,7 @@ const props = defineProps<{
isLast?: boolean
canEdit?: boolean
canAssign?: boolean
canSplit?: boolean
canOutsource?: boolean
canSubmitDetail?: boolean
}>()
@ -142,6 +149,7 @@ const props = defineProps<{
const emit = defineEmits<{
edit: [process: ProcessPlanItemVO]
assign: [process: ProcessPlanItemVO]
split: [process: ProcessPlanItemVO]
viewModel: [process: ProcessPlanItemVO]
outsource: [process: ProcessPlanItemVO]
submitDetail: [process: ProcessPlanItemVO]

View File

@ -39,7 +39,12 @@ const emit = defineEmits<{
}>()
const steps = computed(() => {
const list = [...(props.list ?? [])].sort((a, b) => (a.operNumber ?? 0) - (b.operNumber ?? 0))
const list = [...(props.list ?? [])].sort((a, b) => {
const oa = a.operNumber ?? 0
const ob = b.operNumber ?? 0
if (oa !== ob) return oa - ob
return (a.planId ?? 0) - (b.planId ?? 0)
})
return list.map((item, idx) => {
const label = item.processName || item.processCode || `工序${idx + 1}`
const state = item.processStatus === 4 ? 'done' : item.processStatus != null && item.processStatus >= 2 ? 'active' : 'pending'

View File

@ -0,0 +1,929 @@
<template>
<n-modal
v-model:show="visible"
preset="card"
title="拆分工单"
:style="{ width: '1120px' }"
:content-style="{ paddingTop: '8px', paddingBottom: '4px' }"
:mask-closable="false"
@after-leave="resetState"
>
<n-spin :show="loading">
<div class="split-scroll">
<n-descriptions v-if="preview" :column="3" label-placement="left" size="small" class="split-desc">
<n-descriptions-item label="原工单编号">{{ preview.workOrderCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="物料编码">{{ preview.materialCode || '-' }}</n-descriptions-item>
<n-descriptions-item label="物料/订单">{{ preview.orderItemName || '-' }}</n-descriptions-item>
<n-descriptions-item label="计划数量">{{ preview.quantity ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="已进入生产">{{ preview.investedQty ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="可拆数量">{{ preview.splittableQty ?? 0 }}</n-descriptions-item>
<n-descriptions-item label="原工序" :span="3">
{{ preview.processCount ?? 0 }}
<span v-if="preview.processNames?.length" class="process-names">
{{ preview.processNames.join('、') }}
</span>
</n-descriptions-item>
</n-descriptions>
<n-alert v-if="preview && !preview.canSplit" type="warning" :bordered="false" style="margin: 8px 0">
{{ preview.reason || '当前工单不允许拆单' }}
</n-alert>
<template v-if="preview?.canSplit">
<div class="split-section">
<div class="split-section-title">数量分配</div>
<div class="field-row">
<span class="field-label-inline">拆分方式</span>
<n-radio-group v-model:value="mode" name="splitMode" size="small">
<n-space>
<n-radio value="equal">等量拆分</n-radio>
<n-radio value="custom">指定数量</n-radio>
</n-space>
</n-radio-group>
</div>
<!-- 等量按总份数均分计划数量第1份留原单其余新建 -->
<div v-if="mode === 'equal'" class="split-body">
<div class="field-row">
<span class="field-label-inline">拆成份数</span>
<n-input-number
v-model:value="equalCount"
:min="2"
:max="Math.max(2, maxEqualParts)"
:precision="0"
size="small"
style="width: 140px"
/>
<span class="hint-inline">含原工单第1份留原单</span>
</div>
<div class="hint-inline" style="margin-top: 6px">{{ equalHint }}</div>
</div>
<!-- 指定手动设原单保留 + 各新工单数量 -->
<div v-else class="split-body">
<div class="field-row">
<span class="field-label-inline">原单保留</span>
<n-input-number
v-model:value="keepQty"
:min="unsplittableQty"
:max="Math.max(unsplittableQty, (preview.quantity ?? 0) - 1)"
:precision="0"
size="small"
style="width: 140px"
@update:value="onKeepQtyUpdate"
/>
<span class="hint-inline">不可拆 {{ unsplittableQty }}拆出 {{ splitOutQty }}</span>
</div>
<div v-for="(row, idx) in customRows" :key="idx" class="custom-row">
<n-input-number
v-model:value="customRows[idx]"
:min="1"
:precision="0"
size="small"
placeholder="新工单数量"
style="flex: 1"
/>
<n-button text type="error" :disabled="customRows.length <= 1" @click="removeCustomRow(idx)">
删除
</n-button>
</div>
<n-button dashed block size="small" style="margin-top: 6px" @click="addCustomRow">+ 添加工单</n-button>
<div class="sum-line" :class="{ error: customSum !== splitOutQty }">
拆出合计: {{ customSum }} / {{ splitOutQty }}
</div>
</div>
</div>
<div class="split-section">
<div class="split-section-title">
新工单工艺路线
<n-radio-group
v-model:value="routeMode"
name="routeMode"
size="small"
style="margin-left: 12px; font-weight: 400"
>
<n-space>
<n-radio value="kingdee">金蝶工艺路线</n-radio>
<n-radio value="assemble">工序表组装</n-radio>
</n-space>
</n-radio-group>
</div>
<div v-if="routeMode === 'kingdee'" class="split-body">
<div v-if="!preview?.materialCode" class="hint">当前订单无物料编码无法查询金蝶工艺路线</div>
<template v-else>
<div class="kingdee-layout">
<div class="kingdee-left">
<div class="field-label">路线列表{{ routeList.length }}</div>
<n-spin :show="routeLoading">
<div v-if="!routeLoading && routeList.length === 0" class="hint">未查询到工艺路线</div>
<div v-else class="route-card-list">
<div
v-for="route in routeList"
:key="route.routingNo"
class="route-card"
:class="{ active: routingNo === route.routingNo }"
@click="routingNo = route.routingNo || null"
>
<div class="route-card-head">
<span class="route-code">{{ route.routingNo }}</span>
<span class="route-step-count">{{ route.stepCount ?? route.steps?.length ?? 0 }}</span>
</div>
<div class="route-flow">
{{ (route.processNames || []).join(' → ') || '暂无工序' }}
</div>
</div>
</div>
</n-spin>
</div>
<div class="kingdee-right">
<div class="field-label">
工序明细
<span v-if="selectedRoute" class="route-detail-sub">
{{ selectedRoute.routingNo }} · {{ selectedSteps.length }}
</span>
</div>
<n-data-table
v-if="selectedRoute"
size="small"
:bordered="true"
:single-line="true"
:columns="kingdeeStepColumns"
:data="selectedSteps"
:scroll-x="900"
:max-height="260"
:pagination="false"
/>
<div v-else class="route-detail-empty">点击左侧路线查看工序</div>
</div>
</div>
</template>
</div>
<div v-else class="split-body">
<div class="assemble-toolbar">
<span class="hint-inline">按新增工艺路线方式逐行维护说明为空时默认用工序名称</span>
<n-button size="small" type="primary" secondary @click="addAssembleRow">+ 添加工序</n-button>
</div>
<n-data-table
size="small"
:bordered="true"
:single-line="false"
:columns="assembleColumns"
:data="assembleRows"
:scroll-x="1080"
:max-height="280"
:pagination="false"
/>
</div>
</div>
</template>
</div>
</n-spin>
<template #footer>
<n-space justify="end">
<n-button @click="visible = false">取消</n-button>
<n-button type="primary" :loading="submitting" :disabled="!canConfirm" @click="confirmSplit">
确认拆分
</n-button>
</n-space>
</template>
</n-modal>
</template>
<script setup lang="ts">
/**
* 拆单弹窗
* - 已进入生产数量不可拆留原单只拆可拆数量
* - 等量拆分拆成份数均分计划数量第1份留原单其余新建 30÷3 原10 + 新10 + 新10
* - 指定数量手动设原单保留 + 各新工单数量
* - 金蝶工艺路线 / 工序表组装对齐工艺路线字段
*/
import { computed, h, ref, watch } from 'vue'
import {
NAlert,
NButton,
NDataTable,
NDescriptions,
NDescriptionsItem,
NInput,
NInputNumber,
NModal,
NRadio,
NRadioGroup,
NSelect,
NSpace,
NSpin,
NSwitch,
useMessage,
type DataTableColumns,
type SelectOption,
} from 'naive-ui'
import {
orderProcessPlanApi,
type ProcessPlanSplitPreview,
type ProcessPlanSplitStep,
} from '@/api/orderProcessPlan'
import {
processRouteApi,
type KingdeeProcessRouteStep,
type ProcessRouteSelectOption,
} from '@/api/processRoute'
import { basicProcessPlanApi, type BasicProcessPlan } from '@/api/basicProcessPlan'
interface AssembleRow extends ProcessPlanSplitStep {
_key: string
}
const props = defineProps<{
show: boolean
planId: number | null
}>()
const emit = defineEmits<{
'update:show': [value: boolean]
success: []
}>()
const message = useMessage()
const loading = ref(false)
const submitting = ref(false)
const preview = ref<ProcessPlanSplitPreview | null>(null)
const mode = ref<'equal' | 'custom'>('equal')
const keepQty = ref(0)
/** 等量拆分:总份数(含原单),至少 2 = 原单 + 1 新单 */
const equalCount = ref(2)
const customRows = ref<(number | null)[]>([null])
const routeMode = ref<'kingdee' | 'assemble'>('kingdee')
const routingNo = ref<string | null>(null)
const routeList = ref<ProcessRouteSelectOption[]>([])
const routeLoading = ref(false)
const processLoading = ref(false)
const processOptions = ref<SelectOption[]>([])
const processMap = ref<Record<number, BasicProcessPlan>>({})
const assembleRows = ref<AssembleRow[]>([])
const ctrlCodeOptions: SelectOption[] = [
{ label: '汇报+免检', value: '汇报+免检' },
{ label: '汇报+质量', value: '汇报+质量' },
{ label: '委外+质量', value: '委外+质量' },
{ label: '委外+汇报', value: '委外+汇报' },
]
const activityUnitOptions = ref<SelectOption[]>([
{ label: '小时', value: '小时' },
{ label: '分钟', value: '分钟' },
{ label: '秒', value: '秒' },
])
const visible = computed({
get: () => props.show,
set: (v: boolean) => emit('update:show', v),
})
const unsplittableQty = computed(() => preview.value?.investedQty ?? 0)
const planQty = computed(() => preview.value?.quantity ?? 0)
const splitOutQty = computed(() => Math.max(planQty.value - (keepQty.value ?? 0), 0))
function onKeepQtyUpdate(v: number | null) {
const min = unsplittableQty.value
const max = Math.max(min, (preview.value?.quantity ?? 0) - 1)
if (v == null || v < min) {
keepQty.value = min
return
}
if (v > max) keepQty.value = max
}
/**
* 等量拆分把整单计划数量均分成 N 含原单
* quantities = [原单保留, 新单1, 新单2, ...]
*/
const equalQuantities = computed(() => {
const q = planQty.value
const n = equalCount.value
if (!q || !n || n < 2 || q < n) return [] as number[]
const base = Math.floor(q / n)
const rem = q % n
const list = Array.from({ length: n }, () => base)
// 0
list[n - 1] = base + rem
if (list.some((v) => v <= 0)) return []
// 1
if (list[0] < unsplittableQty.value) return []
return list
})
/** 在「第1份 ≥ 不可拆」前提下,最多能拆成几份 */
const maxEqualParts = computed(() => {
const q = planQty.value
const inv = unsplittableQty.value
if (q < 2) return 2
let max = Math.min(q, Math.max(2, q - inv)) // 1
// floor(q/n) >= inv=floor
while (max > 2 && Math.floor(q / max) < inv) {
max -= 1
}
// 1 < q
while (max > 2) {
const base = Math.floor(q / max)
if (base >= Math.max(1, inv) && base < q) break
max -= 1
}
return Math.max(2, max)
})
const equalHint = computed(() => {
const list = equalQuantities.value
if (!list.length) {
if (planQty.value < equalCount.value) return `计划数量不足以分成 ${equalCount.value}`
if (equalCount.value >= 2 && Math.floor(planQty.value / equalCount.value) < unsplittableQty.value) {
return `第1份无法覆盖不可拆数量 ${unsplittableQty.value},请减少份数`
}
return ''
}
const keep = list[0]
const news = list.slice(1)
const newCount = news.length
if (news.every((v) => v === news[0])) {
return `结果:原单保留 ${keep},新建 ${newCount} 张各 ${news[0]}(共 ${list.length} 张)`
}
return `结果:原单保留 ${keep},新建 ${newCount} 张(${news.join('、')}`
})
const customSum = computed(() =>
customRows.value.reduce((s, v) => s + (typeof v === 'number' && v > 0 ? v : 0), 0),
)
const selectedRoute = computed(() =>
routingNo.value ? routeList.value.find((r) => r.routingNo === routingNo.value) ?? null : null,
)
const selectedSteps = computed<KingdeeProcessRouteStep[]>(() => selectedRoute.value?.steps ?? [])
const kingdeeStepColumns: DataTableColumns<KingdeeProcessRouteStep> = [
{ title: '工序号', key: 'operNumber', width: 70, render: (r) => r.operNumber ?? '-' },
{ title: '工序编码', key: 'processCode', width: 110, ellipsis: { tooltip: true }, render: (r) => r.processCode || '-' },
{ title: '工序名称', key: 'processName', width: 120, ellipsis: { tooltip: true }, render: (r) => r.processName || '-' },
{ title: '工序说明', key: 'operDescription', minWidth: 140, ellipsis: { tooltip: true }, render: (r) => r.operDescription || '-' },
{ title: '工作中心', key: 'workCenterName', width: 110, ellipsis: { tooltip: true }, render: (r) => r.workCenterName || '-' },
{ title: '车间', key: 'departmentName', width: 100, ellipsis: { tooltip: true }, render: (r) => r.departmentName || '-' },
{ title: '控制码', key: 'optCtrlCodeName', width: 100, ellipsis: { tooltip: true }, render: (r) => r.optCtrlCodeName || '-' },
{
title: '标准工时',
key: 'activityQty',
width: 100,
render: (r) => (r.activityQty == null ? '-' : `${r.activityQty}${r.activityUnit ? ' ' + r.activityUnit : ''}`),
},
{ title: '质检', key: 'qualityInspection', width: 56, render: (r) => (r.qualityInspection === 1 ? '是' : '否') },
{ title: '入库', key: 'storageEntry', width: 56, render: (r) => (r.storageEntry === 1 ? '是' : '否') },
]
function createEmptyAssembleRow(operNumber = 10): AssembleRow {
return {
_key: `a-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
processId: null,
operNumber,
processCode: null,
processName: null,
operDescription: null,
sectionId: null,
workshopId: null,
activityUnit: '小时',
optCtrlCodeName: null,
activityQty: null,
qualityInspection: 0,
storageEntry: 0,
procurement: 0,
isOutsource: 0,
}
}
function renumberAssembleRows() {
assembleRows.value.forEach((row, i) => {
row.operNumber = (i + 1) * 10
})
}
function addAssembleRow() {
assembleRows.value.push(createEmptyAssembleRow((assembleRows.value.length + 1) * 10))
}
function removeAssembleRow(key: string) {
if (assembleRows.value.length <= 1) return
assembleRows.value = assembleRows.value.filter((r) => r._key !== key)
renumberAssembleRows()
}
function onAssembleProcessChange(row: AssembleRow, processId: number | null) {
row.processId = processId
if (processId == null) {
row.processCode = null
row.processName = null
row.operDescription = null
row.sectionId = null
row.workshopId = null
row.qualityInspection = 0
row.storageEntry = 0
row.procurement = 0
row.isOutsource = 0
return
}
const basic = processMap.value[processId]
if (!basic) return
row.processCode = basic.code || null
row.processName = basic.name || null
if (!row.operDescription) {
row.operDescription = basic.operDescription || basic.name || null
}
row.sectionId = basic.sectionId ?? null
row.workshopId = basic.workShopId ?? null
row.qualityInspection = basic.qualityInspection ?? 0
row.storageEntry = basic.storageEntry ?? 0
row.procurement = basic.procurement ?? 0
row.isOutsource = basic.outsource ?? 0
}
function onCtrlCodeChange(row: AssembleRow, val: string | null) {
row.optCtrlCodeName = val
if (!val) return
//
row.qualityInspection = val.includes('质量') ? 1 : 0
row.isOutsource = val.includes('委外') ? 1 : 0
}
const assembleColumns = computed<DataTableColumns<AssembleRow>>(() => [
{
title: '工序号',
key: 'operNumber',
width: 64,
render: (row) => row.operNumber ?? '-',
},
{
title: '工序',
key: 'processId',
width: 180,
render: (row) =>
h(NSelect, {
value: row.processId,
size: 'small',
filterable: true,
clearable: true,
options: processOptions.value,
loading: processLoading.value,
placeholder: '选择工序',
style: 'width: 100%',
onUpdateValue: (v: number | null) => onAssembleProcessChange(row, v),
}),
},
{
title: '工序说明',
key: 'operDescription',
width: 150,
render: (row) =>
h(NInput, {
value: row.operDescription ?? '',
size: 'small',
clearable: true,
placeholder: row.processName || '默认工序名称',
onUpdateValue: (v: string) => {
row.operDescription = v
},
}),
},
{
title: '标准工时',
key: 'activityQty',
width: 100,
render: (row) =>
h(NInputNumber, {
value: row.activityQty,
size: 'small',
min: 0,
precision: 3,
style: 'width: 90px',
placeholder: '工时',
onUpdateValue: (v: number | null) => {
row.activityQty = v
},
}),
},
{
title: '单位',
key: 'activityUnit',
width: 100,
render: (row) =>
h(NSelect, {
value: row.activityUnit,
size: 'small',
filterable: true,
tag: true,
options: activityUnitOptions.value,
placeholder: '单位',
style: 'width: 100%',
onUpdateValue: (v: string | null) => {
row.activityUnit = v
},
}),
},
{
title: '控制码',
key: 'optCtrlCodeName',
width: 140,
render: (row) =>
h(NSelect, {
value: row.optCtrlCodeName,
size: 'small',
filterable: true,
clearable: true,
options: ctrlCodeOptions,
placeholder: '选择控制码',
style: 'width: 100%',
onUpdateValue: (v: string | null) => onCtrlCodeChange(row, v),
}),
},
{
title: '质检',
key: 'qualityInspection',
width: 70,
render: (row) =>
h(NSwitch, {
value: row.qualityInspection === 1,
size: 'small',
onUpdateValue: (v: boolean) => {
row.qualityInspection = v ? 1 : 0
},
}),
},
{
title: '入库',
key: 'storageEntry',
width: 70,
render: (row) =>
h(NSwitch, {
value: row.storageEntry === 1,
size: 'small',
onUpdateValue: (v: boolean) => {
row.storageEntry = v ? 1 : 0
},
}),
},
{
title: '操作',
key: 'actions',
width: 64,
fixed: 'right',
render: (row) =>
h(
NButton,
{
text: true,
type: 'error',
size: 'small',
disabled: assembleRows.value.length <= 1,
onClick: () => removeAssembleRow(row._key),
},
{ default: () => '删除' },
),
},
])
const assembleValid = computed(() => {
if (!assembleRows.value.length) return false
return assembleRows.value.every((r) => r.processId != null || !!r.processName || !!r.processCode)
})
const canConfirm = computed(() => {
if (!preview.value?.canSplit || submitting.value) return false
if (mode.value === 'equal') {
//
if (!equalQuantities.value.length) return false
} else {
if (keepQty.value == null || keepQty.value < unsplittableQty.value) return false
if (splitOutQty.value <= 0) return false
if (customRows.value.length < 1) return false
if (customRows.value.some((v) => v == null || v <= 0)) return false
if (customSum.value !== splitOutQty.value) return false
}
if (routeMode.value === 'kingdee') return !!routingNo.value
return assembleValid.value
})
watch(
() => [props.show, props.planId] as const,
async ([show, planId]) => {
if (!show || planId == null) return
await loadPreview(planId)
await Promise.all([
loadRouteOptions(preview.value?.materialCode),
loadProcessOptions(),
])
},
)
watch(keepQty, () => {
if (mode.value === 'custom') {
customRows.value = [splitOutQty.value > 0 ? splitOutQty.value : null]
}
})
async function loadPreview(planId: number) {
loading.value = true
preview.value = null
try {
preview.value = await orderProcessPlanApi.splitPreview(planId)
const unsplittable = preview.value?.investedQty ?? 0
keepQty.value = unsplittable
// 2 + 1
equalCount.value = 2
customRows.value = [preview.value?.splittableQty ?? null]
mode.value = 'equal'
routeMode.value = 'kingdee'
routingNo.value = null
assembleRows.value = [createEmptyAssembleRow(10)]
} catch (e: any) {
message.error(e?.message || '加载拆单预览失败')
visible.value = false
} finally {
loading.value = false
}
}
async function loadRouteOptions(materialCode?: string | null) {
routeLoading.value = true
routeList.value = []
routingNo.value = null
try {
if (!materialCode) return
const list = (await processRouteApi.kingdeeSelect(materialCode)) as ProcessRouteSelectOption[]
routeList.value = (Array.isArray(list) ? list : []).filter((r) => !!r?.routingNo)
} catch (e: any) {
routeList.value = []
message.error(e?.message || '加载金蝶工艺路线失败')
} finally {
routeLoading.value = false
}
}
async function loadProcessOptions() {
processLoading.value = true
try {
const page: any = await basicProcessPlanApi.page({ page: 1, pageSize: 500 })
const rows: BasicProcessPlan[] = page?.records ?? page?.list ?? (Array.isArray(page) ? page : [])
const map: Record<number, BasicProcessPlan> = {}
processOptions.value = rows
.filter((r) => r?.id != null)
.map((r) => {
const id = Number(r.id)
map[id] = r
return {
label: `${r.code || ''} ${r.name || ''}`.trim() || String(id),
value: id,
}
})
processMap.value = map
} catch {
processOptions.value = []
processMap.value = {}
} finally {
processLoading.value = false
}
}
function addCustomRow() {
customRows.value.push(null)
}
function removeCustomRow(idx: number) {
if (customRows.value.length <= 1) return
customRows.value.splice(idx, 1)
}
function resetState() {
preview.value = null
mode.value = 'equal'
keepQty.value = 0
equalCount.value = 2
customRows.value = [null]
routeMode.value = 'kingdee'
routingNo.value = null
routeList.value = []
assembleRows.value = [createEmptyAssembleRow(10)]
}
function buildAssembleSteps(): ProcessPlanSplitStep[] {
return assembleRows.value.map((row, i) => {
const name = row.processName || processMap.value[row.processId!]?.name || null
const desc = (row.operDescription || '').trim() || name
return {
processId: row.processId,
operNumber: row.operNumber ?? (i + 1) * 10,
processCode: row.processCode,
processName: name,
operDescription: desc,
sectionId: row.sectionId,
workshopId: row.workshopId,
activityUnit: row.activityUnit,
optCtrlCodeName: row.optCtrlCodeName,
activityQty: row.activityQty,
qualityInspection: row.qualityInspection ?? 0,
storageEntry: row.storageEntry ?? 0,
procurement: row.procurement ?? 0,
isOutsource: row.isOutsource ?? 0,
}
})
}
async function confirmSplit() {
if (!props.planId || !canConfirm.value) return
// keepQty
const quantities =
mode.value === 'equal'
? equalQuantities.value
: [keepQty.value!, ...customRows.value.map((v) => Number(v))]
submitting.value = true
try {
await orderProcessPlanApi.split({
planId: props.planId,
quantities,
routeMode: routeMode.value,
routingNo: routeMode.value === 'kingdee' ? routingNo.value : null,
assembleSteps: routeMode.value === 'assemble' ? buildAssembleSteps() : undefined,
})
message.success('拆单成功')
visible.value = false
emit('success')
} catch (e: any) {
message.error(e?.message || '拆单失败')
} finally {
submitting.value = false
}
}
</script>
<style scoped>
.split-scroll {
max-height: min(72vh, 680px);
overflow-y: auto;
padding-right: 4px;
}
.split-desc {
margin-bottom: 4px;
}
.process-names {
color: #666;
font-size: 12px;
}
.split-section {
margin-top: 10px;
padding-top: 8px;
border-top: 1px dashed #e8e8e8;
}
.split-section-title {
display: flex;
align-items: center;
flex-wrap: wrap;
font-size: 13px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.field-label {
font-size: 12px;
color: #666;
margin-bottom: 6px;
}
.field-label-inline {
font-size: 13px;
color: #666;
min-width: 64px;
}
.split-body {
margin-top: 6px;
}
.field-row {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.hint,
.hint-inline {
font-size: 12px;
color: #999;
}
.hint {
margin-top: 4px;
}
.kingdee-layout {
display: grid;
grid-template-columns: 280px 1fr;
gap: 12px;
min-height: 0;
}
.kingdee-left,
.kingdee-right {
min-width: 0;
}
.route-card-list {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 280px;
overflow-y: auto;
padding: 1px;
}
.route-card {
border: 1px solid #e5e7eb;
border-radius: 6px;
padding: 8px 10px;
cursor: pointer;
background: #fafafa;
transition: border-color 0.15s, background 0.15s;
}
.route-card:hover {
border-color: #93c5fd;
background: #f8fbff;
}
.route-card.active {
border-color: #2080f0;
background: #eff6ff;
}
.route-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.route-code {
font-weight: 600;
color: #1f2937;
font-size: 12px;
}
.route-step-count {
font-size: 11px;
color: #6b7280;
white-space: nowrap;
}
.route-flow {
margin-top: 4px;
font-size: 11px;
color: #6b7280;
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.route-detail-sub {
margin-left: 6px;
font-weight: 400;
font-size: 12px;
color: #6b7280;
}
.route-detail-empty {
height: 260px;
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #e5e7eb;
border-radius: 6px;
color: #9ca3af;
font-size: 12px;
background: #fafafa;
}
.assemble-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.custom-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.sum-line {
margin-top: 6px;
font-size: 12px;
color: #666;
text-align: right;
}
.sum-line.error {
color: #d03050;
}
@media (max-width: 1100px) {
.kingdee-layout {
grid-template-columns: 1fr;
}
.route-card-list {
max-height: 140px;
}
}
</style>

View File

@ -206,6 +206,13 @@
</template>
</n-modal>
<!-- 工序工单拆单 -->
<SplitProcessDialog
v-model:show="splitVisible"
:plan-id="splitPlanId"
@success="loadData"
/>
<!--派工弹框-->
<n-modal
v-model:show="dispatchmodal"
@ -422,6 +429,7 @@ import {
import { dictDataApi } from '@/api/org'
import Pgitem from './pgitem.vue'
import SplitProcessDialog from './components/SplitProcessDialog.vue'
//import { userApi } from '@/api/system'
@ -433,6 +441,25 @@ const hasPermission = (permission: string) => userStore.hasPermission(permission
const message = useMessage()
const dialog = useDialog()
const splitVisible = ref(false)
const splitPlanId = ref<number | null>(null)
/** 列表拆单入口:是否可拆以后端预览为准,这里只做粗过滤 */
function canSplitProcess(proc: ProcessPlanItemVO) {
const qty = proc.quantity ?? 0
return qty > 0
}
function openSplit(process: ProcessPlanItemVO) {
if (!process.planId) {
message.warning('工序计划无效')
return
}
// id
splitPlanId.value = process.planId
splitVisible.value = true
}
//
const searchForm = reactive<any>({
id: '',
@ -450,6 +477,7 @@ const orderScrollX = 2090
const processScrollX = 2788
function orderRowKey(row: OrderProcessPlanVO) {
if (row.workOrderCode) return row.workOrderCode
return row.orderItemId ?? `order-${row.mainCode}-${row.orderCode}`
}
@ -490,7 +518,7 @@ function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<P
return {
title: '操作',
key: 'actions',
width: 140,
width: 200,
fixed: 'right',
align: 'center',
render(row) {
@ -511,6 +539,14 @@ function buildProcessActionColumn(order: OrderProcessPlanVO): DataTableColumns<P
onClick: () => disphand(row, order),
}, { default: () => '派工' }))
}
if (hasPermission('biz:orderProcessPlan:edit') && canSplitProcess(row)) {
buttons.push(h(NButton, {
size: 'small',
type: 'info',
ghost: true,
onClick: () => openSplit(row),
}, { default: () => '拆单' }))
}
return buttons.length > 0 ? h(NSpace, { justify: 'center' }, { default: () => buttons }) : '-'
},
}
@ -602,8 +638,12 @@ const orderColumns: DataTableColumns<OrderProcessPlanVO> = [
align: 'center',
ellipsis: { tooltip: true },
render: (row) => {
const codes = (row.processList ?? []).map((p) => p.workOrderCode).filter(Boolean)
return codes.length ? codes.join('、') : '-'
if (row.workOrderCode) return row.workOrderCode
const codes = (row.processList ?? [])
.map((p) => p.workOrderCode)
.filter((c): c is string => !!c)
const unique = [...new Set(codes)]
return unique.length ? unique.join('、') : '-'
},
},
{ title: '数量', key: 'quantity', width: 72, align: 'center' },

View File

@ -1,195 +1,211 @@
<template>
<div>
<n-grid>
<n-gi :span="10">
<n-form-item
label="指派工段"
:path="`${listname}[${index}].sectionId`"
>
<n-select
v-model:value="obj.sectionId"
filterable
placeholder="请选择工段"
:options="sectionList"
:loading="loadingRef"
disabled
remote
:clear-filter-after-select="false"
/>
</n-form-item>
</n-gi>
<n-gi :span="10">
<n-form-item
label="指派工位"
:path="`${listname}[${index}].deviceId`"
:rule="{
<div>
<n-grid>
<n-gi :span="10">
<n-form-item
label="指派工段"
:path="`${listname}[${index}].sectionId`"
>
<n-select
v-model:value="obj.sectionId"
filterable
placeholder="请选择工段"
:options="sectionList"
:loading="loadingRef"
disabled
remote
:clear-filter-after-select="false"
/>
</n-form-item>
</n-gi>
<n-gi :span="10">
<n-form-item
label="指派工位"
:path="`${listname}[${index}].deviceId`"
:rule="{
required:true,
message:`请选择工位`,
trigger: 'blur'
}"
>
<n-select
v-model:value="obj.deviceId"
filterable
placeholder="请选择工位"
:options="deviceList"
:loading="loadingRef"
clearable
remote
:clear-filter-after-select="false"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid>
<n-gi :span="10">
<n-form-item
label="数量"
:path="`${listname}[${index}].quantity`"
:rule="{
>
<n-select
v-model:value="obj.deviceId"
filterable
placeholder="请选择工位"
:options="deviceList"
:loading="loadingRef"
clearable
remote
:clear-filter-after-select="false"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid>
<n-gi :span="10">
<n-form-item
label="数量"
:path="`${listname}[${index}].quantity`"
:rule="{
required: true,
message: `请输入数量`,
trigger: 'blur',
}"
>
<n-input v-model:value="obj.quantity" />
</n-form-item>
</n-gi>
<n-gi :span="4" style="padding-left:10px;">
<n-icon
v-if="index == 0"
size="25"
style="padding-top: 5px;cursor: pointer;" color="#1060c9"
@click="addpgnum"
>
<AddOutline />
</n-icon>
>
<n-input v-model:value="obj.quantity"/>
</n-form-item>
</n-gi>
<n-gi :span="10">
<n-form-item
label="NC下发"
:path="`${listname}[${index}].ncId`"
>
<n-select
v-model:value="obj.ncId"
filterable
placeholder="请选择下发NC"
:options="ncList"
clearable
remote
:clear-filter-after-select="false"
/>
</n-form-item>
</n-gi>
<n-gi :span="4" style="padding-left:10px;">
<n-icon
v-if="index == 0"
size="25"
style="padding-top: 5px;cursor: pointer;" color="#1060c9"
@click="addpgnum"
>
<AddOutline/>
</n-icon>
<n-icon
v-else
size="18"
style="padding:7px 0 0 2px;cursor: pointer;" color="#d03050"
@click="deletenum(index)"
>
<TrashOutline />
</n-icon>
</n-gi>
<n-icon
v-else
size="18"
style="padding:7px 0 0 2px;cursor: pointer;" color="#d03050"
@click="deletenum(index)"
>
<TrashOutline/>
</n-icon>
</n-gi>
</n-grid>
</div>
</div>
</template>
<script setup lang="ts">
import { ref,reactive } from 'vue'
import { userApi } from '@/api/system'
import {sectionApi, type Section} from '@/api/section'
import {
AddOutline,
TrashOutline,
} from '@vicons/ionicons5'
import {ref, reactive} from 'vue'
import {userApi} from '@/api/system'
import {sectionApi, type Section} from '@/api/section'
import {
AddOutline,
TrashOutline,
} from '@vicons/ionicons5'
import {loadAllNcCode} from '@/api/nccode.ts'
const props = withDefaults(defineProps<{
listname:any
obj:any,
index:any
sectionId?: number | string | null
workshopId?: number | string | null
}>(), {
obj:{}
})
const emit = defineEmits<{
addhand:[],
delehand:[index:any],
update: [index:any,obj:any]
}>()
const props = withDefaults(defineProps<{
listname: any
obj: any,
index: any
sectionId?: number | string | null
workshopId?: number | string | null
}>(), {
obj: {}
})
const emit = defineEmits<{
addhand: [],
delehand: [index: any],
update: [index: any, obj: any]
}>()
// function updatehand() {
// emit('update')
// }
// function updatehand() {
// emit('update')
// }
function addpgnum() {
emit('addhand')
}
function addpgnum() {
emit('addhand')
}
function deletenum(index:any){
emit('delehand',index)
}
function deletenum(index: any) {
emit('delehand', index)
}
let sectionList = reactive<{label:string,value:any}[]>([])
let yglist = ref<any>([])
let sectionList = reactive<{ label: string, value: any }[]>([])
let yglist = ref<any>([])
let loadingRef = ref(false)
let loadingRef = ref(false)
let deviceList = ref<any>([])
let deviceList = ref<any>([])
let ncList = ref<any>([])
function slehand(v?:any) {
loadingRef.value = true
yglist.value.splice(0)
// dispatchform.userlist = [{
// id:'',
// num:''
// }]
userApi.page({
page: 1,
pageSize: 100,
username: v,
}).then((rps:any) =>{
if(rps.list && rps.list.length > 0){
yglist.value = rps.list.map((n:any) => {
return {
label:n.username,
value:n.id+''
}
})
}
console.log(yglist.value,'12')
loadingRef.value = false
}).catch(() => {
loadingRef.value = false
})
function slehand(v?: any) {
loadingRef.value = true
yglist.value.splice(0)
// dispatchform.userlist = [{
// id:'',
// num:''
// }]
userApi.page({
page: 1,
pageSize: 100,
username: v,
}).then((rps: any) => {
if (rps.list && rps.list.length > 0) {
yglist.value = rps.list.map((n: any) => {
return {
label: n.username,
value: n.id + ''
}
})
}
console.log(yglist.value, '12')
loadingRef.value = false
}).catch(() => {
loadingRef.value = false
})
}
// sectionId
async function loadSection() {
if (props.sectionId == null || props.sectionId === '') return
const res = await sectionApi.list({sectionId: props.sectionId})
const mesSection = res?.MesSection;
const mesDevice = res?.MesDevice || []
if (!mesSection) return
// sectionId
async function loadSection() {
if (props.sectionId == null || props.sectionId === '') return
const res = await sectionApi.list({sectionId: props.sectionId})
const mesSection = res?.MesSection;
const mesDevice = res?.MesDevice || []
if (!mesSection) return
sectionList.push({
label:mesSection.deptName,
value:mesSection.id
})
props.obj.sectionId = mesSection.id
sectionList.push({
label: mesSection.deptName,
value: mesSection.id
})
props.obj.sectionId = mesSection.id
deviceList.value = mesDevice.map((n:any)=>{
return {
label:n.deviceName,
value:n.id+''
}
})
}
deviceList.value = mesDevice.map((n: any) => {
return {
label: n.deviceName,
value: n.id + ''
}
})
}
// function searchSection(sectionId?:number){
// if(!sectionId) return
// props.obj.deviceId = ''
// deviceList.value = []
// deviceApi.list(sectionId).then(res=>{
// deviceList.value = res.map((n:any)=>{
// return {
// label:n.name,
// value:n.id+''
// }
// })
// })
// }
//
async function loadNc() {
const res = await loadAllNcCode()
ncList.value = res.map((n: any) => {
return {
label: n.ncName,
value: n.id
}
})
console.log(ncList.value)
}
loadSection()
slehand()
loadNc()
loadSection()
slehand()
// handleDevice()
</script>

View File

@ -0,0 +1,452 @@
<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="mapper">
<n-select v-model:value="searchForm.mapperKey" placeholder="请选择mapper" clearable style="width: 300px" :options="mapperOptions"
@update:value="mapperSearchUpdate" />
</n-form-item>
<n-form-item label="控制方法">
<n-select v-model:value="searchForm.controlMethod" placeholder="请选择控制方法" clearable style="width: 200px" :options="methodSearchOptions" />
</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="table-toolbar">
<n-space>
<n-button type="primary" @click="handleAdd">
<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>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="pagination"
:row-key="(row) => row.id"
:scroll-x="1200"
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
@update:checked-row-keys="handleCheck"
/>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 800px" :mask-closable=false>
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid :cols="2" :x-gap="24">
<n-form-item-gi label="mapper" path="mapperKey">
<n-select v-model:value="formData.mapperKey" placeholder="请选择mapper" :options="mapperOptions" @update:value="mapperUpdate" />
</n-form-item-gi>
<n-form-item-gi label="控制方法" path="controlMethod">
<n-select v-model:value="formData.controlMethod" multiple placeholder="请选择控制方法" :options="methodOptions" />
</n-form-item-gi>
</n-grid>
<n-grid :cols="1" :x-gap="24">
<n-form-item-gi label="权限控制方案" path="permissionControl">
<n-dynamic-input
v-model:value="permissionControl"
key-field="sole"
preset="pair"
:min="1"
:max="5"
@create="handleCreate"
>
<template #default="{ value,index }">
<n-space>
<n-select
v-model:value="value.key"
:options="roleOptions"
placeholder="请选择角色"
style="width: 150px"
/>
<n-input
v-model:value="value.value"
placeholder="请输入权限内容"
readonly
style="width: 400px"
@click="clickPermission(index)"
/>
</n-space>
</template>
</n-dynamic-input>
</n-form-item-gi>
</n-grid>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<n-modal v-model:show="updatePermissionModal" :draggable="true" :on-after-leave="closePermission">
<n-card style="width: 600px" :bordered="false" size="huge" role="dialog" aria-modal="true" >
<n-input v-model:value="updatePermissionContent" placeholder="请输入权限内容" type="textarea" />
</n-card>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline } from '@vicons/ionicons5'
import { permissionConfigApi, type PermissionConfig } from '@/api/permissionConfig'
import { roleApi, SysRole } from '@/api/system'
const message = useMessage()
const dialog = useDialog()
//
const searchForm = reactive({
mapperKey: '',
controlMethod: '',
})
const permissionControl = ref([
{
sole:Date.now(),
key: '',
value: '123'
}
])
//
const roleOptions = ref<Array<{ label: string; value: number; disabled:false }>>([])
const handleCreate = () => {
return {
sole: Date.now() + Math.random(), // keyfield使
key:"",
value: null // selectkey
}
}
//
const tableData = ref<PermissionConfig[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
//
const modalVisible = ref(false)
const modalTitle = ref('')
const importModalVisible = ref(false)
const formRef = ref()
const defaultFormData: PermissionConfig = {
mapperKey: '',
controlMethod: '',
permissionControl: '',
}
const formData = reactive<PermissionConfig>({ ...defaultFormData })
// //使
//
const formRules = {
}
//
const columns: DataTableColumns<PermissionConfig> = [
{ type: 'selection' },
{ title: 'mapper', key: 'mapperKey' },
{ title: '控制方法', key: 'controlMethod' },
{ title: '权限控制方案', key: 'permissionControl' },
{
title: '操作',
key: 'actions',
width: 140,
fixed: 'right',
render(row) {
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) }), ' 删除']
})
])
}
}
]
const updatePermissionIndex = ref<number>(-1)
const updatePermissionContent = ref<string>('')
const updatePermissionModal = ref(false)
function clickPermission(index:number){
updatePermissionIndex.value = index
updatePermissionContent.value = permissionControl.value[index].value;
updatePermissionModal.value = true
}
function closePermission(){
permissionControl.value[updatePermissionIndex.value].value = updatePermissionContent.value;
}
//
async function loadData() {
loading.value = true
try {
const res = await permissionConfigApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
mapperKey: searchForm.mapperKey || undefined,
controlMethod: searchForm.controlMethod || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
//
function handleSearch() {
pagination.page = 1
loadData()
}
//
function handleReset() {
searchForm.mapperKey = ''
searchForm.controlMethod = ''
handleSearch()
}
//
function handlePageChange(page: number) {
pagination.page = page
loadData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadData()
}
//
function handleCheck(keys: Array<string | number>) {
selectedIds.value = keys as number[]
}
//
function handleAdd() {
modalTitle.value = '新增数据权限配置'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
//
function handleEdit(row: PermissionConfig) {
modalTitle.value = '编辑数据权限配置'
Object.assign(formData, row)
modalVisible.value = true
}
//
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = { ...formData } as PermissionConfig
if (submitData.id) {
await permissionConfigApi.update(submitData)
message.success('修改成功')
} else {
await permissionConfigApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
//
}
}
//
function handleDelete(row: PermissionConfig) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await permissionConfigApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
//
}
}
})
}
//
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await permissionConfigApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
} catch (error) {
//
}
}
})
}
//
async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.mapperKey) params.mapperKey = searchForm.mapperKey
if (searchForm.controlMethod) params.controlMethod = searchForm.controlMethod
const blob = await permissionConfigApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '数据权限配置数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
//
}
}
//
async function handleDownloadTemplate() {
try {
const blob = await permissionConfigApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '数据权限配置导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
//
}
}
//
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await permissionConfigApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
content: `成功: ${result.success} 条,失败: ${result.fail}\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
positiveText: '确定'
})
} else {
message.success(`导入成功,共 ${result.success} 条数据`)
importModalVisible.value = false
}
loadData()
} catch (error) {
//
}
}
const mapperOptions = ref<{ label: string; value: any }[]>([])
const methodSearchOptions = ref<{ label: string; value: any }[]>([])
const methodOptions = ref<{ label: string; value: any }[]>([])
function mapperUpdate(v:any,opt:any) {
formData.controlMethod=''
methodOptions.value = opt.childList;
}
function mapperSearchUpdate(v:any,opt:any) {
searchForm.controlMethod=''
methodSearchOptions.value = opt.childList;
}
//
async function loadDictOptions() {
try {
mapperOptions.value = await permissionConfigApi.options();
}catch{}
try {
const roles = await roleApi.list()
roleOptions.value = roles.map((role: SysRole) => ({
label: role.name,
value: role.id!,
disabled:false
}))
} catch (error) {
//
}
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
</style>

View File

@ -217,7 +217,6 @@ const columns: DataTableColumns<Device> = [
{ type: 'selection' },
{ title: '工位编码', key: 'deviceCode' },
{ title: '工位名称', key: 'deviceName' },
{ title: '工位类型', key: 'deviceType' },
{ title: '所属工段', key: 'sectionName' },
{ title: '状态', key: 'status',
render:(row) =>{

View File

@ -606,7 +606,6 @@ async function doLogin() {
if (rememberMeEnabled.value) {
loginData.rememberMe = formData.rememberMe
}
await userStore.login(loginData)
message.success('登录成功')
const redirect = route.query.redirect as string

View File

@ -22,6 +22,7 @@
:max-height="1000"
size="small"
style="margin-bottom:15px"
@update-expanded-row-keys="handleExpandChange"
/>
</n-scrollbar>
@ -105,7 +106,7 @@ import {
import { mytasks,type AssingWork } from '@/api/production';
import {
NButton, NSpace, NTag, NDataTable, DataTableColumns, NDescriptions, NDescriptionsItem
NButton, NSpace, NTag, NDataTable, DataTableColumns, NDescriptions, NDescriptionsItem, NPagination
} from 'naive-ui'
import { dictDataApi } from '@/api/org'
import { assingWorkDetailApi,AssingWorkDetail } from '@/api/AssingWorkDetail'
@ -360,6 +361,7 @@ const AssingWorkDetailColums:DataTableColumns<AssingWorkDetail> = [
render(row:any) {
const val = row.status
const opt = qcAssingWorkDetailStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
@ -449,48 +451,112 @@ function AssingWorkToggleExpandAll() {
}
// key=idvalue=
const expandDataMap = new Map<number, any[]>()
interface DetailPageState {
list: AssingWorkDetail[]
page: number
pageSize: number
total: number
loading: boolean
}
// keyid
const expandDetailMap = new Map<number, DetailPageState>()
function renderAssingWorkExpand(row: AssingWork) {
// 1.
if (!expandDataMap.has(row.id)) {
expandDataMap.set(row.id, [])
loadingRowIds.value.add(row.id)
const rowId = row.id
//
getAssingWorkDetail(row.id).then((detailList) => {
expandDataMap.set(row.id, detailList)
loadingRowIds.value.delete(row.id)
})
if (!expandDetailMap.has(rowId)) {
expandDetailMap.set(rowId, reactive({
list: [],
page: 1,
pageSize: 10,
total: 0,
loading: true
}))
loadDetail(rowId)
}
// 2.
if (loadingRowIds.value.has(row.id)) {
return h('div', { style: 'padding:12px' }, '加载中...')
}
const state = expandDetailMap.get(rowId)!
console.log(state)
console.log(state.list)
//
// if (state.loading) {
// return h('div', {style: 'padding:12px'}, '...')
// }
const tableData = expandDataMap.get(row.id)!
return h('div', { style: 'padding: 0 16px 12px' }, [
h(NDataTable, {
columns: AssingWorkDetailColums,
data: tableData,
data: state.list,
rowKey: DetailRowKey,
size: 'small',
bordered: true,
striped: true,
scrollX: 1360
})
}),
// + npagination + prefix
h('div', {
class: 'pagination-container',
style: 'display: flex; justify-content: flex-end; margin-top: 12px'
}, [
h(NPagination, {
page: state.page,
pageSize: state.pageSize,
itemCount: state.total,
onUpdatePage: (p: number) => handleDetailPageChange(rowId, p),
onUpdatePageSize: (sz: number) => handleDetailPageSizeChange(rowId, sz)
}, {
// #prefix
prefix: () => h('span', `${state.total}`)
})
])
])
}
// /
async function handleDetailPageChange(rowId: number,page:number) {
const state = expandDetailMap.get(rowId)
if (!state) return
state.page = page
loadDetail(rowId)
}
async function handleDetailPageSizeChange(rowId: number,pageSize: number) {
const state = expandDetailMap.get(rowId)
if (!state) return
state.pageSize = pageSize
state.page = 1
loadDetail(rowId)
}
function handleExpandChange(openKeys: number[]){
for (const [rowId] of expandDetailMap) {
if (!openKeys.includes(rowId)) {
expandDetailMap.delete(rowId)
}
}
}
async function getAssingWorkDetail(assingWorkId:number) {
const res = await assingWorkDetailApi.selectAssingWorkDetailListByAssingWorkId(assingWorkId)
return res
}
const loadDetail = async (rowId: number)=>{
const state = expandDetailMap.get(rowId)
if (!state) return
state.loading = true
try {
const res = await assingWorkDetailApi.selectAssingWorkDetailListByAssingWorkId({
assingWorkId: rowId,
page: state.page,
pageSize: state.pageSize
})
state.list = res.list
state.total = res.total
}finally {
state.loading = false
}
}
//
async function loadDictOptions() {

View File

@ -31,12 +31,27 @@
remote
:scroll-x="600"
/>
<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="[2,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>
<script setup lang="ts">
import { NCard, NDataTable, NGi,NTag, NGrid,DataTableColumns} from "naive-ui";
import {onMounted, ref,h} from "vue";
import {onMounted, ref, h, reactive} from "vue";
import { dictDataApi } from '@/api/org'
import {QualityTesting,qualityTestingApi} from "@/api/qualityTesting.ts";
@ -47,6 +62,12 @@ const props =defineProps(
baseInfo:Object
}
)
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0
})
//
const qcSourceTypeOptions = ref<{ label: string; value: any;class:any }[]>([])
@ -93,8 +114,24 @@ async function loadDictOptions() {
const qualityTestingList = ref<QualityTesting[]>([])
async function loadQualityTestingList() {
const res = await qualityTestingApi.loadQualityTestingList(props.assingId)
qualityTestingList.value = res
const res = await qualityTestingApi.loadQualityTestingList({
assingId:props.assingId,
page: pagination.page,
pageSize: pagination.pageSize
})
qualityTestingList.value = res.list
pagination.itemCount = res.total
}
function handlePageChange(page: number) {
pagination.page = page
loadQualityTestingList()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadQualityTestingList()
}
onMounted(()=>{

View File

@ -32,11 +32,27 @@
:scroll-x="600"
/>
<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="[2,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>
<script setup lang="ts">
import { NCard, NDataTable, NGi,NTag, NGrid,DataTableColumns} from "naive-ui";
import {onMounted, ref,h} from "vue";
import {onMounted, ref, h, reactive} from "vue";
import { dictDataApi } from '@/api/org'
import { SubmitLog,submitLogApi} from '@/api/submitLog'
@ -51,6 +67,12 @@ const props =defineProps(
const submitStatusOptions = ref<{ label: string; value: any ;class:any}[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0
})
//
const reportRecordsColums:DataTableColumns<SubmitLog> = [
@ -79,8 +101,24 @@ async function loadDictOptions() {
//
const submitLogList = ref<SubmitLog[]>([])
async function load() {
const res = await submitLogApi.loadSumbitLog(props.assingId)
submitLogList.value = res
const res = await submitLogApi.loadSumbitLog({
assingId:props.assingId,
page: pagination.page,
pageSize: pagination.pageSize
})
submitLogList.value = res.list
pagination.itemCount = res.total
}
function handlePageChange(page: number) {
pagination.page = page
load()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
load()
}
onMounted(()=>{

View File

@ -16,16 +16,16 @@ export default defineConfig({
emptyOutDir: true
},
server: {
// host:'192.168.12.11', //true,
host:'192.168.12.4', //true,
port: 3000,
proxy: {
'/api': {
//target:'http://192.168.12.230:8888/', //'http://192.168.5.230:8888',
target:'http://192.168.12.4:8888/',
//target:'http://192.168.12.4:8888/',
// target:'http://192.168.12.4:8888/',
//target:'http://192.168.12.230:8888',
//target:'http://192.168.5.230:8888',
//target:'http://192.168.5.232:8888/',
//target:'http://localhost:8888',
target:'http://localhost:8888',
changeOrigin: true
},
'/druid': {