<script lang="ts" setup>
|
import type { VbenFormSchema } from '#/adapter/form';
|
import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
|
import type { MesProWorkOrderParamRecordApi } from '#/api/mes/pro/workorderparamrecord';
|
|
import { nextTick, ref, watch } from 'vue';
|
|
import { IconifyIcon } from '@vben/icons';
|
|
import { Button, Divider, message, Popconfirm, Tag } from 'ant-design-vue';
|
|
import { useVbenForm, z } from '#/adapter/form';
|
import { getTemplate } from '#/api/mes/pro/processdesign/template';
|
import {
|
createWorkOrderParamRecord,
|
deleteWorkOrderParamRecord,
|
getWorkOrderParamRecord,
|
getWorkOrderParamRecordPage,
|
updateWorkOrderParamRecord,
|
} from '#/api/mes/pro/workorderparamrecord';
|
|
import TemplateSelectDialog from '../TemplateSelectDialog.vue';
|
|
defineOptions({ name: 'WorkbenchRecordParamTab' });
|
|
const props = defineProps<{
|
/** 当前工单编号 */
|
workOrderId?: number;
|
/** 当前工作站编号 */
|
workstationId?: number;
|
}>();
|
|
// --- 模式 ---
|
type Mode = 'list' | 'create' | 'edit';
|
const mode = ref<Mode>('list');
|
const editingRecordId = ref<number>();
|
|
// --- 模板状态 ---
|
const selectedTemplate = ref<MesPdTemplateApi.Template>();
|
const templateDetails = ref<MesPdTemplateApi.TemplateDetail[]>([]);
|
const tplSelectDialogRef = ref<InstanceType<typeof TemplateSelectDialog>>();
|
|
function openTemplateSelect() {
|
tplSelectDialogRef.value?.open();
|
}
|
|
/** 选择模板后加载详情 */
|
async function handleTemplateSelect(tpl: MesPdTemplateApi.Template) {
|
if (!tpl.id) return;
|
try {
|
const detail = await getTemplate(tpl.id);
|
selectedTemplate.value = detail;
|
templateDetails.value = detail.details ?? [];
|
await buildParamForm(templateDetails.value);
|
} catch {
|
message.error('加载模板详情失败');
|
}
|
}
|
|
// --- 动态参数表单 ---
|
const [ParamForm, paramFormApi] = useVbenForm({
|
commonConfig: {
|
componentProps: { class: 'w-full' },
|
formItemClass: 'col-span-1',
|
labelWidth: 160,
|
},
|
layout: 'horizontal',
|
schema: [],
|
showDefaultActions: false,
|
wrapperClass: 'grid-cols-2',
|
});
|
|
/** 根据模板明细构建参数表单 */
|
async function buildParamForm(
|
details: MesPdTemplateApi.TemplateDetail[],
|
existingValues?: Map<number, string>,
|
) {
|
// 先构建基础字段(recordNo, remark)
|
const baseSchemas: VbenFormSchema[] = [
|
{
|
fieldName: '_recordNo',
|
label: '记录序号',
|
component: 'Input',
|
componentProps: { placeholder: '请输入记录序号/批次号(可选)' },
|
},
|
{
|
fieldName: '_remark',
|
label: '备注',
|
component: 'Textarea',
|
formItemClass: 'col-span-2',
|
componentProps: { placeholder: '请输入备注(可选)', rows: 2 },
|
},
|
];
|
|
if (!details || details.length === 0) {
|
await paramFormApi.setState({ schema: baseSchemas });
|
return;
|
}
|
|
const paramSchemas: VbenFormSchema[] = details
|
.filter((d) => d != null)
|
.map((d) => {
|
const paramType = d.paramType ?? 1;
|
const isRequired = d.required ?? false;
|
const unitSuffix = d.unitMeasureName ? ` (${d.unitMeasureName})` : '';
|
const paramName = d.paramName ?? '';
|
const templateDetailId = d.id;
|
|
let component = 'Input';
|
const componentProps: Record<string, any> = {
|
placeholder: `请输入${paramName}${unitSuffix}`,
|
maxLength: 100,
|
};
|
|
switch (paramType) {
|
case 1:
|
case 2:
|
component = 'InputNumber';
|
componentProps.controls = false;
|
componentProps.style = 'width: 100%';
|
break;
|
case 3:
|
component = 'Select';
|
componentProps.allowClear = true;
|
componentProps.options = [];
|
break;
|
case 4:
|
component = 'TimePicker';
|
componentProps.format = 'HH:mm:ss';
|
componentProps.valueFormat = 'HH:mm:ss';
|
break;
|
case 5:
|
component = 'DatePicker';
|
componentProps.format = 'YYYY-MM-DD';
|
componentProps.valueFormat = 'YYYY-MM-DD';
|
break;
|
}
|
|
let rules: any;
|
if (isRequired) {
|
if (paramType === 1 || paramType === 2) {
|
rules = z.number({ message: `${paramName}不能为空` });
|
} else {
|
rules = z.string({ message: `${paramName}不能为空` }).min(1, `${paramName}不能为空`);
|
}
|
}
|
|
return {
|
fieldName: `param_${templateDetailId}`,
|
label: `${paramName}${unitSuffix}`,
|
component,
|
componentProps,
|
rules,
|
defaultValue: existingValues?.get(templateDetailId!),
|
} as VbenFormSchema;
|
});
|
|
await paramFormApi.resetForm();
|
await nextTick();
|
await paramFormApi.setState({ schema: [...baseSchemas, ...paramSchemas] });
|
}
|
|
/** 新增参数记录 */
|
async function handleCreate() {
|
mode.value = 'create';
|
editingRecordId.value = undefined;
|
selectedTemplate.value = undefined;
|
templateDetails.value = [];
|
await paramFormApi.setState({ schema: [] });
|
await paramFormApi.resetForm();
|
}
|
|
/** 打开模板选择 */
|
function handleSelectTemplate() {
|
tplSelectDialogRef.value?.open();
|
}
|
|
/** 取消新增/编辑 */
|
function handleCancelForm() {
|
mode.value = 'list';
|
editingRecordId.value = undefined;
|
selectedTemplate.value = undefined;
|
templateDetails.value = [];
|
paramFormApi.resetForm();
|
}
|
|
/** 保存参数记录 */
|
async function handleSave() {
|
if (!selectedTemplate.value || !props.workOrderId) return;
|
const { valid } = await paramFormApi.validate();
|
if (!valid) {
|
message.warning('请检查必填参数');
|
return;
|
}
|
const values = await paramFormApi.getValues();
|
|
// 构建 details 数组
|
const details = templateDetails.value
|
.filter((d) => d != null && d.id != null)
|
.map((d) => ({
|
templateDetailId: d.id!,
|
paramValue: values[`param_${d.id}`] != null ? String(values[`param_${d.id}`]) : undefined,
|
}));
|
|
// 获取表单中的 recordNo 和 remark
|
const recordNo = values._recordNo as string | undefined;
|
const remark = values._remark as string | undefined;
|
|
try {
|
if (mode.value === 'edit' && editingRecordId.value) {
|
await updateWorkOrderParamRecord({
|
id: editingRecordId.value,
|
templateId: selectedTemplate.value.id,
|
recordNo,
|
remark,
|
details,
|
});
|
message.success('更新成功');
|
} else {
|
await createWorkOrderParamRecord(
|
{ templateId: selectedTemplate.value.id, recordNo, remark, details },
|
props.workOrderId,
|
);
|
message.success('创建成功');
|
}
|
handleCancelForm();
|
refreshRecordList();
|
} catch {
|
message.error('保存失败');
|
}
|
}
|
|
/** 编辑已有记录 */
|
async function handleEditRecord(recordId: number) {
|
try {
|
const record = await getWorkOrderParamRecord(recordId);
|
if (!record.templateId) return;
|
mode.value = 'edit';
|
editingRecordId.value = record.id;
|
|
// 加载模板
|
const tpl = await getTemplate(record.templateId);
|
selectedTemplate.value = tpl;
|
templateDetails.value = tpl.details ?? [];
|
|
// 构建已有值映射
|
const valueMap = new Map<number, string>();
|
for (const d of record.details ?? []) {
|
if (d.templateDetailId != null && d.paramValue != null) {
|
valueMap.set(d.templateDetailId, d.paramValue);
|
}
|
}
|
|
await buildParamForm(templateDetails.value, valueMap);
|
|
// 设置 recordNo 和 remark
|
await paramFormApi.setFieldValue('_recordNo', record.recordNo);
|
await paramFormApi.setFieldValue('_remark', record.remark);
|
} catch {
|
message.error('加载记录详情失败');
|
}
|
}
|
|
/** 删除记录 */
|
async function handleDeleteRecord(recordId: number) {
|
try {
|
await deleteWorkOrderParamRecord(recordId);
|
message.success('删除成功');
|
refreshRecordList();
|
} catch {
|
message.error('删除失败');
|
}
|
}
|
|
// --- 记录列表 ---
|
const recordList = ref<MesProWorkOrderParamRecordApi.WorkOrderParamRecord[]>([]);
|
const recordLoading = ref(false);
|
|
async function refreshRecordList() {
|
if (!props.workOrderId) {
|
recordList.value = [];
|
return;
|
}
|
recordLoading.value = true;
|
try {
|
const result = await getWorkOrderParamRecordPage({
|
workOrderId: props.workOrderId,
|
workstationId: props.workstationId,
|
pageNo: 1,
|
pageSize: 100,
|
});
|
recordList.value = result.list ?? [];
|
} finally {
|
recordLoading.value = false;
|
}
|
}
|
|
// 工单或工作站变更时加载记录
|
watch(
|
() => [props.workOrderId, props.workstationId],
|
() => {
|
handleCancelForm();
|
refreshRecordList();
|
},
|
{ immediate: true },
|
);
|
</script>
|
|
<template>
|
<div class="record-param-tab">
|
<!-- 顶部操作栏 -->
|
<div class="record-param-tab__toolbar">
|
<div class="record-param-tab__toolbar-left">
|
<IconifyIcon icon="lucide:file-cog" class="record-param-tab__toolbar-icon" />
|
<span class="record-param-tab__toolbar-title">生产记录参数</span>
|
</div>
|
<Button
|
v-if="mode === 'list'"
|
type="primary"
|
size="small"
|
:disabled="!workOrderId"
|
@click="handleCreate"
|
>
|
<template #icon><IconifyIcon icon="lucide:plus" /></template>
|
新增参数记录
|
</Button>
|
<div v-else class="record-param-tab__toolbar-actions">
|
<Tag :color="mode === 'create' ? 'blue' : 'orange'">
|
{{ mode === 'create' ? '新建' : '编辑' }}
|
</Tag>
|
<Button type="primary" size="small" @click="handleSave">
|
<template #icon><IconifyIcon icon="lucide:save" /></template>
|
保存
|
</Button>
|
<Button size="small" @click="handleCancelForm">取消</Button>
|
</div>
|
</div>
|
|
<!-- 表单区域(新增/编辑模式) -->
|
<div v-if="mode !== 'list'" class="record-param-tab__form-card">
|
<!-- 模板选择 -->
|
<div class="record-param-tab__template-row">
|
<span class="record-param-tab__template-label">参数模板:</span>
|
<template v-if="selectedTemplate">
|
<Tag color="green">{{ selectedTemplate.templateName }}</Tag>
|
<span class="record-param-tab__template-code">{{ selectedTemplate.templateCode }}</span>
|
<Button
|
v-if="mode === 'create'"
|
size="small"
|
type="link"
|
@click="handleSelectTemplate"
|
>
|
更换模板
|
</Button>
|
</template>
|
<Button v-else type="dashed" size="small" @click="handleSelectTemplate">
|
<template #icon><IconifyIcon icon="lucide:folder-open" /></template>
|
选择模板
|
</Button>
|
</div>
|
|
<!-- 记录编号 + 备注 -->
|
<div class="record-param-tab__meta-row">
|
<ParamForm />
|
</div>
|
|
<!-- 无模板提示 -->
|
<div v-if="!selectedTemplate" class="record-param-tab__no-template">
|
<IconifyIcon icon="lucide:arrow-up-left" class="record-param-tab__no-template-icon" />
|
<span>请先选择一个参数模板</span>
|
</div>
|
|
<!-- 参数表单已加载 -->
|
<div
|
v-else-if="templateDetails.length > 0"
|
class="record-param-tab__param-info"
|
>
|
<span class="record-param-tab__param-count">
|
共 {{ templateDetails.length }} 项参数,
|
{{ templateDetails.filter((d) => d.required).length }} 项必填
|
</span>
|
</div>
|
<div v-else class="record-param-tab__no-template">
|
<span>该模板暂无参数明细</span>
|
</div>
|
</div>
|
|
<Divider style="margin: 12px 0" />
|
|
<!-- 记录列表 -->
|
<div class="record-param-tab__list">
|
<div class="record-param-tab__list-header">
|
<span class="record-param-tab__list-title">
|
参数记录
|
<span class="record-param-tab__list-count">({{ recordList.length }})</span>
|
</span>
|
<Button
|
v-if="workOrderId"
|
size="small"
|
:loading="recordLoading"
|
@click="refreshRecordList"
|
>
|
<template #icon><IconifyIcon icon="lucide:refresh-cw" /></template>
|
刷新
|
</Button>
|
</div>
|
|
<!-- 无工单提示 -->
|
<div v-if="!workOrderId" class="record-param-tab__empty">
|
<IconifyIcon icon="lucide:alert-circle" class="record-param-tab__empty-icon" />
|
<p>请先选择工作站和工单任务</p>
|
</div>
|
|
<!-- 空列表 -->
|
<div v-else-if="recordList.length === 0" class="record-param-tab__empty">
|
<IconifyIcon icon="lucide:inbox" class="record-param-tab__empty-icon" />
|
<p>暂无参数记录,点击上方"新增参数记录"开始</p>
|
</div>
|
|
<!-- 记录卡片列表 -->
|
<div v-else class="record-param-tab__cards">
|
<div
|
v-for="record in recordList"
|
:key="record.id"
|
class="record-param-tab__card"
|
>
|
<div class="record-param-tab__card-head">
|
<div class="record-param-tab__card-meta">
|
<Tag color="green">{{ record.templateName }}</Tag>
|
<span v-if="record.recordNo" class="record-param-tab__card-no">
|
{{ record.recordNo }}
|
</span>
|
<span class="record-param-tab__card-time">{{ record.createTime }}</span>
|
</div>
|
<div class="record-param-tab__card-actions">
|
<Button type="link" size="small" @click="handleEditRecord(record.id!)">
|
编辑
|
</Button>
|
<Popconfirm
|
title="确认删除该参数记录?删除后不可恢复"
|
@confirm="handleDeleteRecord(record.id!)"
|
>
|
<Button type="link" size="small" danger>删除</Button>
|
</Popconfirm>
|
</div>
|
</div>
|
<div v-if="record.remark" class="record-param-tab__card-remark">
|
备注:{{ record.remark }}
|
</div>
|
<div class="record-param-tab__card-params">
|
<div
|
v-for="d in record.details"
|
:key="d.id"
|
class="record-param-tab__param-row"
|
>
|
<span class="record-param-tab__param-name">{{ d.paramName }}</span>
|
<span class="record-param-tab__param-value">
|
{{ d.paramValue ?? '-' }}<span v-if="d.unitMeasureName" class="record-param-tab__param-unit"> {{ d.unitMeasureName }}</span>
|
</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<!-- 模板选择弹窗 -->
|
<TemplateSelectDialog
|
ref="tplSelectDialogRef"
|
@select="handleTemplateSelect"
|
/>
|
</div>
|
</template>
|
|
<style lang="scss" scoped>
|
.record-param-tab {
|
padding: 20px 24px;
|
height: 100%;
|
display: flex;
|
flex-direction: column;
|
overflow: auto;
|
}
|
|
// --- 顶部工具栏 ---
|
.record-param-tab__toolbar {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 12px;
|
flex-shrink: 0;
|
}
|
|
.record-param-tab__toolbar-left {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
.record-param-tab__toolbar-icon {
|
font-size: 22px;
|
color: #22c55e;
|
}
|
|
.record-param-tab__toolbar-title {
|
font-size: 17px;
|
font-weight: 600;
|
color: #1a1a1a;
|
}
|
|
.record-param-tab__toolbar-actions {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
}
|
|
// --- 表单卡片 ---
|
.record-param-tab__form-card {
|
margin-top: 16px;
|
padding: 16px 20px;
|
background: #fafbfc;
|
border-radius: 10px;
|
border: 1px solid #e8e8e8;
|
flex-shrink: 0;
|
}
|
|
.record-param-tab__template-row {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
margin-bottom: 12px;
|
}
|
|
.record-param-tab__template-label {
|
font-size: 14px;
|
font-weight: 500;
|
color: #595959;
|
flex-shrink: 0;
|
}
|
|
.record-param-tab__template-code {
|
font-size: 12px;
|
color: #8c8c8c;
|
}
|
|
.record-param-tab__meta-row {
|
margin-top: 8px;
|
}
|
|
.record-param-tab__no-template {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
padding: 20px 0;
|
color: #8c8c8c;
|
font-size: 14px;
|
justify-content: center;
|
}
|
|
.record-param-tab__no-template-icon {
|
font-size: 18px;
|
opacity: 0.6;
|
}
|
|
.record-param-tab__param-info {
|
margin-top: 8px;
|
}
|
|
.record-param-tab__param-count {
|
font-size: 13px;
|
color: #8c8c8c;
|
}
|
|
// --- 记录列表 ---
|
.record-param-tab__list {
|
flex: 1;
|
min-height: 0;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.record-param-tab__list-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 12px;
|
flex-shrink: 0;
|
}
|
|
.record-param-tab__list-title {
|
font-size: 15px;
|
font-weight: 600;
|
color: #1a1a1a;
|
}
|
|
.record-param-tab__list-count {
|
font-size: 13px;
|
font-weight: 400;
|
color: #8c8c8c;
|
margin-left: 6px;
|
}
|
|
// --- 空状态 ---
|
.record-param-tab__empty {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
gap: 8px;
|
color: #8c8c8c;
|
|
p {
|
margin: 0;
|
font-size: 14px;
|
}
|
}
|
|
.record-param-tab__empty-icon {
|
font-size: 36px;
|
opacity: 0.4;
|
}
|
|
// --- 记录卡片 ---
|
.record-param-tab__cards {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
gap: 10px;
|
overflow: auto;
|
}
|
|
.record-param-tab__card {
|
padding: 12px 16px;
|
background: #fff;
|
border: 1px solid #f0f0f0;
|
border-radius: 8px;
|
transition: box-shadow 0.2s;
|
|
&:hover {
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
}
|
}
|
|
.record-param-tab__card-head {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: 8px;
|
}
|
|
.record-param-tab__card-meta {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
flex-wrap: wrap;
|
}
|
|
.record-param-tab__card-no {
|
font-size: 13px;
|
font-weight: 500;
|
color: #595959;
|
padding: 2px 8px;
|
background: #f5f5f5;
|
border-radius: 4px;
|
}
|
|
.record-param-tab__card-time {
|
font-size: 12px;
|
color: #8c8c8c;
|
}
|
|
.record-param-tab__card-actions {
|
display: flex;
|
gap: 4px;
|
flex-shrink: 0;
|
}
|
|
.record-param-tab__card-remark {
|
margin-top: 6px;
|
font-size: 13px;
|
color: #8c8c8c;
|
}
|
|
.record-param-tab__card-params {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 4px 16px;
|
margin-top: 8px;
|
}
|
|
.record-param-tab__param-row {
|
display: flex;
|
align-items: baseline;
|
gap: 8px;
|
font-size: 13px;
|
line-height: 1.6;
|
padding: 2px 0;
|
}
|
|
.record-param-tab__param-name {
|
flex-shrink: 0;
|
color: #8c8c8c;
|
min-width: 100px;
|
|
&::after {
|
content: ':';
|
}
|
}
|
|
.record-param-tab__param-value {
|
color: #1a1a1a;
|
font-weight: 500;
|
}
|
|
.record-param-tab__param-unit {
|
color: #8c8c8c;
|
font-size: 12px;
|
}
|
</style>
|