diff --git a/apps/web-antd/src/api/iot/alert/config/index.ts b/apps/web-antd/src/api/iot/alert/config/index.ts index d419a5563..a1d3bdfcc 100644 --- a/apps/web-antd/src/api/iot/alert/config/index.ts +++ b/apps/web-antd/src/api/iot/alert/config/index.ts @@ -14,6 +14,9 @@ export namespace AlertConfigApi { receiveUserIds?: number[]; receiveUserNames?: string[]; receiveTypes?: number[]; + smsTemplateCode?: string; + mailTemplateCode?: string; + notifyTemplateCode?: string; createTime?: Date; } } diff --git a/apps/web-antd/src/api/member/user/index.ts b/apps/web-antd/src/api/member/user/index.ts index e02f332ed..35229ad98 100644 --- a/apps/web-antd/src/api/member/user/index.ts +++ b/apps/web-antd/src/api/member/user/index.ts @@ -13,6 +13,7 @@ export namespace MemberUserApi { loginIp: string; mark: string; mobile: string; + email?: string; name?: string; nickname?: string; registerIp: string; diff --git a/apps/web-antd/src/api/mes/pro/task/issue/index.ts b/apps/web-antd/src/api/mes/pro/task/issue/index.ts new file mode 100644 index 000000000..b3ccf956b --- /dev/null +++ b/apps/web-antd/src/api/mes/pro/task/issue/index.ts @@ -0,0 +1,73 @@ +import type { PageParam, PageResult } from '@vben/request'; + +import { requestClient } from '#/api/request'; + +export namespace MesProTaskIssueApi { + /** MES 生产任务投料 */ + export interface TaskIssue { + id?: number; // 编号 + taskId?: number; // 生产任务编号 + workOrderId?: number; // 生产工单编号 + workstationId?: number; // 工作站编号 + sourceDocType?: string; // 来源单据类型 + sourceDocId?: number; // 来源单据编号 + sourceLineId?: number; // 来源单据行编号 + sourceDocCode?: string; // 来源单据编码 + batchCode?: string; // 投料批次 + itemId?: number; // 产品物料编号 + itemName?: string; // 产品名称 + itemCode?: string; // 产品编码 + itemSpecification?: string; // 规格型号 + unitMeasureId?: number; // 单位编号 + unitMeasureName?: string; // 单位名称 + issuedQuantity?: number; // 总投料数量 + availableQuantity?: number; // 当前可用数量 + usedQuantity?: number; // 当前使用数量 + remark?: string; // 备注 + } + + /** MES 生产任务投料分页查询参数 */ + export interface PageParams extends PageParam { + taskId?: number; // 生产任务编号 + workOrderId?: number; // 生产工单编号 + workstationId?: number; // 工作站编号 + itemId?: number; // 产品物料编号 + } +} + +/** 查询生产任务投料分页 */ +export function getTaskIssuePage(params: MesProTaskIssueApi.PageParams) { + return requestClient.get>( + '/mes/pro/task-issue/page', + { params }, + ); +} + +/** 查询生产任务投料详情 */ +export function getTaskIssue(id: number) { + return requestClient.get( + `/mes/pro/task-issue/get?id=${id}`, + ); +} + +/** 新增生产任务投料 */ +export function createTaskIssue(data: MesProTaskIssueApi.TaskIssue) { + return requestClient.post('/mes/pro/task-issue/create', data); +} + +/** 修改生产任务投料 */ +export function updateTaskIssue(data: MesProTaskIssueApi.TaskIssue) { + return requestClient.put('/mes/pro/task-issue/update', data); +} + +/** 删除生产任务投料 */ +export function deleteTaskIssue(id: number) { + return requestClient.delete(`/mes/pro/task-issue/delete?id=${id}`); +} + +/** 按生产任务查询投料列表 */ +export function getTaskIssueListByTask(taskId: number) { + return requestClient.get( + `/mes/pro/task-issue/list-by-task?taskId=${taskId}`, + ); +} diff --git a/apps/web-antd/src/api/mes/wm/barcode/index.ts b/apps/web-antd/src/api/mes/wm/barcode/index.ts index 7e8154141..39673a3ae 100644 --- a/apps/web-antd/src/api/mes/wm/barcode/index.ts +++ b/apps/web-antd/src/api/mes/wm/barcode/index.ts @@ -5,17 +5,17 @@ import { requestClient } from '#/api/request'; export namespace MesWmBarcodeApi { /** MES 条码清单 */ export interface Barcode { - id?: number; - configId?: number; - format?: number; - bizType?: number; - content?: string; - bizId?: number; - bizCode?: string; - bizName?: string; - status?: number; - remark?: string; - createTime?: Date; + id?: number; // 条码编号 + configId?: number; // 条码配置编号 + format?: number; // 条码格式 + bizType?: number; // 业务类型 + content?: string; // 条码内容 + bizId?: number; // 业务对象编号 + bizCode?: string; // 业务对象编码 + bizName?: string; // 业务对象名称 + status?: number; // 状态 + remark?: string; // 备注 + createTime?: Date; // 创建时间 } } diff --git a/apps/web-antd/src/api/mes/wm/batch/index.ts b/apps/web-antd/src/api/mes/wm/batch/index.ts index 7ce0a76f9..14d5fa1bd 100644 --- a/apps/web-antd/src/api/mes/wm/batch/index.ts +++ b/apps/web-antd/src/api/mes/wm/batch/index.ts @@ -39,22 +39,22 @@ export namespace MesWmBatchApi { /** MES 批次分页查询参数 */ export interface PageParams extends PageParam { - code?: string; - itemId?: number; - vendorId?: number; - clientId?: number; - workOrderId?: number; - taskId?: number; - workstationId?: number; - toolId?: number; - moldId?: number; - salesOrderCode?: string; - purchaseOrderCode?: string; - lotNumber?: string; - qualityStatus?: number; - produceDate?: string[]; - expireDate?: string[]; - receiptDate?: string[]; + code?: string; // 批次号 + itemId?: number; // 物料编号 + vendorId?: number; // 供应商编号 + clientId?: number; // 客户编号 + workOrderId?: number; // 工单编号 + taskId?: number; // 生产任务编号 + workstationId?: number; // 工作站编号 + toolId?: number; // 工具编号 + moldId?: number; // 模具编号 + salesOrderCode?: string; // 销售订单号 + purchaseOrderCode?: string; // 采购订单号 + lotNumber?: string; // 批号 + qualityStatus?: number; // 质量状态 + produceDate?: string[]; // 生产日期 + expireDate?: string[]; // 过期日期 + receiptDate?: string[]; // 入库日期 } } diff --git a/apps/web-antd/src/api/mes/wm/sn/index.ts b/apps/web-antd/src/api/mes/wm/sn/index.ts index 5a733fa39..6970a796a 100644 --- a/apps/web-antd/src/api/mes/wm/sn/index.ts +++ b/apps/web-antd/src/api/mes/wm/sn/index.ts @@ -27,11 +27,11 @@ export namespace MesWmSnApi { /** MES SN 码分组分页查询参数 */ export interface PageParams extends PageParam { - uuid?: string; - code?: string; - itemId?: number; - batchCode?: string; - createTime?: string[]; + uuid?: string; // 分组 UUID + code?: string; // SN 码 + itemId?: number; // 物料编号 + batchCode?: string; // 批次号 + createTime?: string[]; // 创建时间 } } diff --git a/apps/web-antd/src/api/system/mail/template/index.ts b/apps/web-antd/src/api/system/mail/template/index.ts index 57f722cf5..fffd5d62b 100644 --- a/apps/web-antd/src/api/system/mail/template/index.ts +++ b/apps/web-antd/src/api/system/mail/template/index.ts @@ -17,6 +17,13 @@ export namespace SystemMailTemplateApi { createTime: Date; } + /** 邮件模版精简信息 */ + export interface MailTemplateSimple { + id: number; + name: string; + code: string; + } + /** 邮件发送信息 */ export interface MailSendReqVO { toMails: string[]; @@ -35,6 +42,13 @@ export function getMailTemplatePage(params: PageParam) { ); } +/** 查询邮件模版精简列表 */ +export function getSimpleMailTemplateList() { + return requestClient.get( + '/system/mail-template/simple-list', + ); +} + /** 查询邮件模版详情 */ export function getMailTemplate(id: number) { return requestClient.get( diff --git a/apps/web-antd/src/api/system/notify/template/index.ts b/apps/web-antd/src/api/system/notify/template/index.ts index dd19f4b8f..92c7d7bcc 100644 --- a/apps/web-antd/src/api/system/notify/template/index.ts +++ b/apps/web-antd/src/api/system/notify/template/index.ts @@ -16,6 +16,13 @@ export namespace SystemNotifyTemplateApi { remark: string; } + /** 站内信模板精简信息 */ + export interface NotifyTemplateSimple { + id: number; + name: string; + code: string; + } + /** 发送站内信请求 */ export interface NotifySendReqVO { userId: number; @@ -33,6 +40,13 @@ export function getNotifyTemplatePage(params: PageParam) { ); } +/** 查询站内信模板精简列表 */ +export function getSimpleNotifyTemplateList() { + return requestClient.get( + '/system/notify-template/simple-list', + ); +} + /** 查询站内信模板详情 */ export function getNotifyTemplate(id: number) { return requestClient.get( diff --git a/apps/web-antd/src/api/system/sms/template/index.ts b/apps/web-antd/src/api/system/sms/template/index.ts index eccfb911e..5cfc5ca9b 100644 --- a/apps/web-antd/src/api/system/sms/template/index.ts +++ b/apps/web-antd/src/api/system/sms/template/index.ts @@ -19,6 +19,13 @@ export namespace SystemSmsTemplateApi { createTime?: Date; } + /** 短信模板精简信息 */ + export interface SmsTemplateSimple { + id: number; + name: string; + code: string; + } + /** 发送短信请求 */ export interface SmsSendReqVO { mobile: string; @@ -35,6 +42,13 @@ export function getSmsTemplatePage(params: PageParam) { ); } +/** 查询短信模板精简列表 */ +export function getSimpleSmsTemplateList() { + return requestClient.get( + '/system/sms-template/simple-list', + ); +} + /** 查询短信模板详情 */ export function getSmsTemplate(id: number) { return requestClient.get( diff --git a/apps/web-antd/src/components/form-create/rules/use-area-select-rule.ts b/apps/web-antd/src/components/form-create/rules/use-area-select-rule.ts index b118a1799..75c13a540 100644 --- a/apps/web-antd/src/components/form-create/rules/use-area-select-rule.ts +++ b/apps/web-antd/src/components/form-create/rules/use-area-select-rule.ts @@ -21,7 +21,7 @@ export function useAreaSelectRule() { title: label, info: '', $required: false, - modelField: 'value', // 特殊:ele 里是 model-value,antd 里是 value + modelField: 'value', // Ant Design Vue 组件使用 value;web-ele 自定义组件使用默认 modelValue }; }, props(_: any, { t }: any) { diff --git a/apps/web-antd/src/components/form-create/rules/use-dict-select.ts b/apps/web-antd/src/components/form-create/rules/use-dict-select.ts index 08171a24b..3b77092b7 100644 --- a/apps/web-antd/src/components/form-create/rules/use-dict-select.ts +++ b/apps/web-antd/src/components/form-create/rules/use-dict-select.ts @@ -39,7 +39,7 @@ export function useDictSelectRule() { title: label, info: '', $required: false, - modelField: 'value', // 特殊:ele 里是 model-value,antd 里是 value + modelField: 'value', // Ant Design Vue 组件使用 value;web-ele 自定义组件使用默认 modelValue }; }, props(_: any, { t }: any) { diff --git a/apps/web-antd/src/components/form-create/rules/use-iframe-rule.ts b/apps/web-antd/src/components/form-create/rules/use-iframe-rule.ts index 39d26d766..cb70c0720 100644 --- a/apps/web-antd/src/components/form-create/rules/use-iframe-rule.ts +++ b/apps/web-antd/src/components/form-create/rules/use-iframe-rule.ts @@ -21,7 +21,7 @@ export function useIframeRule() { title: label, info: '', $required: false, - modelField: 'value', // 特殊:ele 里是 model-value,antd 里是 value + modelField: 'value', // Ant Design Vue 组件使用 value;web-ele 自定义组件使用默认 modelValue }; }, props(_: any, { t }: any) { diff --git a/apps/web-antd/src/views/bpm/processInstance/detail/modules/process-print.vue b/apps/web-antd/src/views/bpm/processInstance/detail/modules/process-print.vue index 3f4dec119..3d487af22 100644 --- a/apps/web-antd/src/views/bpm/processInstance/detail/modules/process-print.vue +++ b/apps/web-antd/src/views/bpm/processInstance/detail/modules/process-print.vue @@ -90,6 +90,7 @@ const [Modal, modalApi] = useVbenModal({ /** 获取打印数据 */ async function fetchPrintData(id: string) { printData.value = await getProcessInstancePrintData(id); + printTime.value = formatDate(new Date(), 'YYYY-MM-DD HH:mm'); initPrintDataMap(); await parseFormFields(); } @@ -154,7 +155,7 @@ function tryFormatDate(value: unknown) { return ''; } const formatted = formatDate(value as Date | number | string); - return formatted === 'Invalid Date' ? String(value) : formatted; + return formatted === 'Invalid Date' ? escapeHtml(value) : formatted; } function formatDateValue(value: unknown) { @@ -164,6 +165,15 @@ function formatDateValue(value: unknown) { return tryFormatDate(value); } +function escapeHtml(value: unknown) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + function formatPrimitiveValue(value: unknown): string { if (isEmptyValue(value)) { return ''; @@ -185,9 +195,9 @@ function formatPrimitiveValue(value: unknown): string { getRecordValue(record, 'url') ?? getRecordValue(record, 'value') ?? JSON.stringify(value); - return String(displayValue); + return escapeHtml(displayValue); } - return String(value); + return escapeHtml(value); } function createImageHtml(url: string) { @@ -248,7 +258,7 @@ function mapValuesWithOptions( (option) => option?.value === item || String(option?.value ?? '') === String(item), ); - return matched?.label ?? String(item); + return escapeHtml(matched?.label ?? String(item)); }) .filter((s) => isNotEmptyString(s)); return labels.join(', '); @@ -276,9 +286,11 @@ function mapValueWithLabelMap( ) { const values = toValueArray(value); const labels = values - .map((item) => labelMap.get(String(item)) ?? String(item)) + .map((item) => escapeHtml(labelMap.get(String(item)) ?? String(item))) .filter((s) => isNotEmptyString(s)); - return labels.length > 0 ? labels.join(separator) : formatPrimitiveValue(values); + return labels.length > 0 + ? labels.join(escapeHtml(separator)) + : formatPrimitiveValue(values); } /** @@ -380,7 +392,12 @@ function formatPrintField( const options = getDictOptions(dictType, valueType); return mapValuesWithOptions(value, options); } - case 'FileUpload': { + case 'Editor': + case 'Tinymce': { + return isEmptyValue(value) ? '' : String(value); + } + case 'FileUpload': + case 'UploadFile': { return renderFileListHtml(value); } case 'IframeComponent': { @@ -394,21 +411,20 @@ function formatPrintField( } case 'ImagesUpload': case 'ImageUpload': - case 'UploadImg': { + case 'UploadImg': + case 'UploadImgs': { return renderImageListHtml(value); } case 'switch': { if (isEmptyValue(value)) return '否'; - const checkedVal = getRuleProp(rule, 'checkedValue'); + const checkedVal = + getRuleProp(rule, 'checkedValue') ?? getRuleProp(rule, 'activeValue'); const isChecked = checkedVal !== undefined && checkedVal !== null ? value === checkedVal : Boolean(value); return isChecked ? '是' : '否'; } - case 'Tinymce': { - return isEmptyValue(value) ? '' : String(value); - } case 'UserSelect': { if (String(getRuleProp(rule, 'returnType')) === 'name') { return formatPrimitiveValue(value); @@ -481,7 +497,7 @@ function getPrintTemplateHTML() { const headTd = document.createElement('td'); headTd.setAttribute('colspan', '2'); headTd.setAttribute('class', 'border border-black p-1.5 text-center'); - headTd.innerHTML = '流程记录'; + headTd.textContent = '流程记录'; headTr.append(headTd); processRecordTable.append(headTr); @@ -489,10 +505,10 @@ function getPrintTemplateHTML() { const tr = document.createElement('tr'); const td1 = document.createElement('td'); td1.setAttribute('class', 'border border-black p-1.5'); - td1.innerHTML = item.name; + td1.textContent = item.name; const td2 = document.createElement('td'); td2.setAttribute('class', 'border border-black p-1.5'); - td2.innerHTML = item.description; + td2.textContent = item.description; tr.append(td1); tr.append(td2); processRecordTable.append(tr); diff --git a/apps/web-antd/src/views/crm/backlog/modules/clue-follow-list.vue b/apps/web-antd/src/views/crm/backlog/modules/clue-follow-list.vue index 64776fd3f..b9823605b 100644 --- a/apps/web-antd/src/views/crm/backlog/modules/clue-follow-list.vue +++ b/apps/web-antd/src/views/crm/backlog/modules/clue-follow-list.vue @@ -45,6 +45,7 @@ const [Grid] = useVbenVxeGrid({ return await getCluePage({ pageNo: page.currentPage, pageSize: page.pageSize, + sceneType: 1, transformStatus: false, ...formValues, }); diff --git a/apps/web-antd/src/views/iot/alert/config/data.ts b/apps/web-antd/src/views/iot/alert/config/data.ts index 063de59b8..86dab96cc 100644 --- a/apps/web-antd/src/views/iot/alert/config/data.ts +++ b/apps/web-antd/src/views/iot/alert/config/data.ts @@ -2,12 +2,25 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { AlertConfigApi } from '#/api/iot/alert/config'; -import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; +import { markRaw } from 'vue'; + +import { + CommonStatusEnum, + DICT_TYPE, + IotAlertReceiveTypeEnum, +} from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { getSimpleRuleSceneList } from '#/api/iot/rule/scene'; import { getSimpleUserList } from '#/api/system/user'; import { getRangePickerDefaultProps } from '#/utils'; +import { MailTemplateSelect } from '#/views/system/mail/template/components'; +import { NotifyTemplateSelect } from '#/views/system/notify/template/components'; +import { SmsTemplateSelect } from '#/views/system/sms/template/components'; + +function hasReceiveType(values: Partial>, type: number) { + return Array.isArray(values.receiveTypes) && values.receiveTypes.includes(type); +} /** 新增/修改告警配置的表单 */ export function useFormSchema(): VbenFormSchema[] { @@ -100,6 +113,60 @@ export function useFormSchema(): VbenFormSchema[] { defaultValue: [], rules: 'required', }, + { + fieldName: 'smsTemplateCode', + label: '短信模板', + component: markRaw(SmsTemplateSelect), + dependencies: { + triggerFields: ['receiveTypes'], + show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.SMS), + trigger: async (values, formApi) => { + if ( + !hasReceiveType(values, IotAlertReceiveTypeEnum.SMS) && + values.smsTemplateCode + ) { + await formApi.setFieldValue('smsTemplateCode', undefined); + } + }, + }, + rules: 'selectRequired', + }, + { + fieldName: 'mailTemplateCode', + label: '邮件模板', + component: markRaw(MailTemplateSelect), + dependencies: { + triggerFields: ['receiveTypes'], + show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL), + trigger: async (values, formApi) => { + if ( + !hasReceiveType(values, IotAlertReceiveTypeEnum.MAIL) && + values.mailTemplateCode + ) { + await formApi.setFieldValue('mailTemplateCode', undefined); + } + }, + }, + rules: 'selectRequired', + }, + { + fieldName: 'notifyTemplateCode', + label: '站内信模板', + component: markRaw(NotifyTemplateSelect), + dependencies: { + triggerFields: ['receiveTypes'], + show: (values) => hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY), + trigger: async (values, formApi) => { + if ( + !hasReceiveType(values, IotAlertReceiveTypeEnum.NOTIFY) && + values.notifyTemplateCode + ) { + await formApi.setFieldValue('notifyTemplateCode', undefined); + } + }, + }, + rules: 'selectRequired', + }, ]; } diff --git a/apps/web-antd/src/views/iot/rule/scene/modules/form.vue b/apps/web-antd/src/views/iot/rule/scene/modules/form.vue index 191e6f4e3..cccd24183 100644 --- a/apps/web-antd/src/views/iot/rule/scene/modules/form.vue +++ b/apps/web-antd/src/views/iot/rule/scene/modules/form.vue @@ -4,15 +4,7 @@ import type { RuleSceneApi } from '#/api/iot/rule/scene'; import { computed, nextTick, reactive, ref } from 'vue'; import { useVbenDrawer } from '@vben/common-ui'; -import { - CommonStatusEnum, - IotRuleSceneActionTypeEnum, - IotRuleSceneTriggerConditionTypeEnum, - IotRuleSceneTriggerTimeOperatorEnum, - IotRuleSceneTriggerTypeEnum, - isDeviceTrigger, -} from '@vben/constants'; -import { CronUtils } from '@vben/utils'; +import { CommonStatusEnum, IotRuleSceneTriggerTypeEnum } from '@vben/constants'; import { Form, message } from 'ant-design-vue'; @@ -22,6 +14,10 @@ import { updateSceneRule, } from '#/api/iot/rule/scene'; import { $t } from '#/locales'; +import { + validateSceneRuleActions, + validateSceneRuleTriggers, +} from '#/views/iot/utils/scene-rule'; import ActionSection from '../form/sections/action-section.vue'; import BasicInfoSection from '../form/sections/basic-info-section.vue'; @@ -47,6 +43,16 @@ const [Drawer, drawerApi] = useVbenDrawer({ } catch { return; } + const triggerError = validateSceneRuleTriggers(formData.value.triggers); + if (triggerError) { + message.error(triggerError); + return; + } + const actionError = validateSceneRuleActions(formData.value.actions); + if (actionError) { + message.error(actionError); + return; + } drawerApi.lock(); try { const data = { ...formData.value } as RuleSceneApi.SceneRule; @@ -117,194 +123,21 @@ function normalizeFormData(result: any): RuleSceneApi.SceneRule { /** 触发器校验 */ function validateTriggers(_rule: any, value: any, callback: any) { - if (!value || !Array.isArray(value) || value.length === 0) { - callback(new Error('至少需要一个触发器')); + const error = validateSceneRuleTriggers(value); + if (error) { + callback(new Error(error)); return; } - for (const [i, trigger] of value.entries()) { - if (!trigger.type) { - callback(new Error(`触发器 ${i + 1}:触发器类型不能为空`)); - return; - } - if (isDeviceTrigger(trigger.type)) { - if (!trigger.productId) { - callback(new Error(`触发器 ${i + 1}:产品不能为空`)); - return; - } - // deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES),是合法值;仅 undefined / null 视为未选 - if (trigger.deviceId === undefined || trigger.deviceId === null) { - callback(new Error(`触发器 ${i + 1}:设备不能为空`)); - return; - } - const isStateUpdate = - trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE; - if (!isStateUpdate && !trigger.identifier) { - callback(new Error(`触发器 ${i + 1}:物模型标识符不能为空`)); - return; - } - // 事件上报 / 服务调用:operator 由前端自动设为 '=',参数值留空表示「事件 / 调用发生即匹配」 - const isEventOrService = - trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST || - trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE; - if (!isEventOrService) { - if (!trigger.operator) { - callback(new Error(`触发器 ${i + 1}:操作符不能为空`)); - return; - } - if ( - trigger.value === undefined || - trigger.value === null || - trigger.value === '' - ) { - callback(new Error(`触发器 ${i + 1}:参数值不能为空`)); - return; - } - } - } - if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) { - if (!trigger.cronExpression) { - callback(new Error(`触发器 ${i + 1}:CRON 表达式不能为空`)); - return; - } - if (!CronUtils.validate(trigger.cronExpression)) { - callback(new Error(`触发器 ${i + 1}:CRON 表达式格式不正确`)); - return; - } - } - // 递归校验 conditionGroups(嵌套条件组) - if (trigger.conditionGroups?.length) { - for (const [gi, group] of trigger.conditionGroups.entries()) { - if (!Array.isArray(group) || group.length === 0) { - callback( - new Error(`触发器 ${i + 1}:条件组 ${gi + 1} 不能为空`), - ); - return; - } - for (const [ci, condition] of group.entries()) { - const prefix = `触发器 ${i + 1} 条件组 ${gi + 1} 条件 ${ci + 1}`; - if (!condition.type) { - callback(new Error(`${prefix}:条件类型不能为空`)); - return; - } - const isDeviceStatus = - condition.type === - IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS; - const isDeviceProperty = - condition.type === - IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY; - const isCurrentTime = - condition.type === - IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME; - if (isDeviceStatus || isDeviceProperty) { - if (!condition.productId) { - callback(new Error(`${prefix}:产品不能为空`)); - return; - } - // deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES),是合法值 - if ( - condition.deviceId === undefined || - condition.deviceId === null - ) { - callback(new Error(`${prefix}:设备不能为空`)); - return; - } - if (isDeviceProperty && !condition.identifier) { - callback(new Error(`${prefix}:物模型标识符不能为空`)); - return; - } - } - if (!condition.operator) { - callback(new Error(`${prefix}:操作符不能为空`)); - return; - } - // 设备状态:param 是状态值(必填);设备属性:param 是比较值(必填) - if ( - (isDeviceStatus || isDeviceProperty) && - (condition.param === undefined || - condition.param === null || - condition.param === '') - ) { - callback( - new Error( - `${prefix}:${isDeviceStatus ? '设备状态' : '比较值'}不能为空`, - ), - ); - return; - } - // 当前时间:TODAY 不需要 param;BETWEEN_TIME 需要双段「v1,v2」;其它需要单段 - if (isCurrentTime) { - const op = condition.operator; - if (op === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value) { - // TODAY 无需 param - } else if ( - op === IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value - ) { - const parts = condition.param - ? String(condition.param).split(',') - : []; - if (parts.length < 2 || !parts[0] || !parts[1]) { - callback(new Error(`${prefix}:起止时间不能为空`)); - return; - } - } else if (!condition.param) { - callback(new Error(`${prefix}:时间值不能为空`)); - return; - } - } - } - } - } - } callback(); } /** 执行器校验 */ function validateActions(_rule: any, value: any, callback: any) { - if (!value || !Array.isArray(value) || value.length === 0) { - callback(new Error('至少需要一个执行器')); + const error = validateSceneRuleActions(value); + if (error) { + callback(new Error(error)); return; } - for (const [i, action] of value.entries()) { - if (!action.type) { - callback(new Error(`执行器 ${i + 1}:执行器类型不能为空`)); - return; - } - if ( - action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET || - action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE - ) { - if (!action.productId) { - callback(new Error(`执行器 ${i + 1}:产品不能为空`)); - return; - } - // deviceId = 0 表示「全部设备」(DEVICE_SELECTOR_OPTIONS.ALL_DEVICES); - // 后端 IotDevicePropertySetSceneRuleAction / IotDeviceServiceInvokeSceneRuleAction - // 均支持广播执行,因此 0 是合法值,仅 undefined / null 视为未选 - if (action.deviceId === undefined || action.deviceId === null) { - callback(new Error(`执行器 ${i + 1}:设备不能为空`)); - return; - } - if ( - action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE && - !action.identifier - ) { - callback(new Error(`执行器 ${i + 1}:服务不能为空`)); - return; - } - if (!action.params || Object.keys(action.params).length === 0) { - callback(new Error(`执行器 ${i + 1}:参数配置不能为空`)); - return; - } - } - // 仅恢复告警动作需要选择已有告警配置;触发告警动作不需要预选 alertConfigId - if ( - action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER && - !action.alertConfigId - ) { - callback(new Error(`执行器 ${i + 1}:告警配置不能为空`)); - return; - } - } callback(); } diff --git a/apps/web-antd/src/views/iot/utils/scene-rule.ts b/apps/web-antd/src/views/iot/utils/scene-rule.ts new file mode 100644 index 000000000..9b038806f --- /dev/null +++ b/apps/web-antd/src/views/iot/utils/scene-rule.ts @@ -0,0 +1,401 @@ +import type { RuleSceneApi } from '#/api/iot/rule/scene'; + +import { + IotRuleSceneActionTypeEnum, + IotRuleSceneTriggerConditionTypeEnum, + IotRuleSceneTriggerTimeOperatorEnum, + IotRuleSceneTriggerTypeEnum, + isDeviceTrigger, +} from '@vben/constants'; +import { CronUtils, isEmptyVal, isObject } from '@vben/utils'; + +/** + * 判断普通 ID 选择值是否缺失。 + * + * 产品、告警配置等普通业务 ID 应为正数;`0` 不代表有效业务数据。 + * + * @param value 普通业务 ID + * @returns 是否缺失 + */ +function isRequiredIdMissing(value: unknown): boolean { + return !value; +} + +/** + * 判断设备 ID 选择值是否缺失。 + * + * 场景联动的设备选择器中,`0` 表示「全部设备」,是合法值;因此这里只能把 + * `undefined`、`null`、空字符串视为未选择,不能使用普通 falsy 判断。 + * + * @param value 设备 ID + * @returns 是否缺失 + */ +function isDeviceIdMissing(value: unknown): boolean { + return isEmptyVal(value); +} + +/** + * 判断执行器参数是否为空。 + * + * 参数配置当前以 JSON 字符串为主,同时兼容历史对象值: + * - 空字符串、空白字符串视为空; + * - 空对象 `{}` 视为空; + * - 非空 JSON 对象视为已配置; + * - 非法 JSON 不在这里判空,交给 JSON 格式校验返回更准确的错误。 + * + * @param params 执行器参数 + * @returns 是否为空 + */ +export function isActionParamsEmpty(params?: unknown): boolean { + if (isEmptyVal(params)) { + return true; + } + + if (typeof params === 'string') { + if (!params.trim()) { + return true; + } + try { + const parsed = JSON.parse(params); + if (isObject(parsed) && !Array.isArray(parsed)) { + return Object.keys(parsed).length === 0; + } + } catch { + return false; + } + return false; + } + + if (isObject(params) && !Array.isArray(params)) { + return Object.keys(params).length === 0; + } + + return false; +} + +/** + * 判断执行器参数是否为合法 JSON。 + * + * 参数编辑组件正常会保存 JSON 字符串;编辑旧数据时可能仍然是对象,对象可直接视为合法。 + * + * @param params 执行器参数 + * @returns 是否合法 + */ +function isActionParamsJsonValid(params?: unknown): boolean { + if (isObject(params)) { + return true; + } + try { + JSON.parse(String(params)); + return true; + } catch { + return false; + } +} + +/** + * 校验单个附加子条件。 + * + * 该方法用于提交前兜底校验,错误提示会带上 path,方便定位到第几个触发器、 + * 第几个条件组和第几个条件。 + * + * @param condition 子条件配置 + * @param path 错误提示前缀 + * @returns 错误信息,通过则返回 null + */ +export function validateTriggerCondition( + condition: RuleSceneApi.TriggerCondition, + path: string, +): null | string { + if (!condition.type) { + return `${path}:条件类型不能为空`; + } + + const isDeviceStatus = + condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_STATUS; + const isDeviceProperty = + condition.type === IotRuleSceneTriggerConditionTypeEnum.DEVICE_PROPERTY; + + // 设备状态和设备属性都必须先选择产品、设备;deviceId = 0 表示全部设备。 + if (isDeviceStatus || isDeviceProperty) { + if (isRequiredIdMissing(condition.productId)) { + return `${path}:产品不能为空`; + } + if (isDeviceIdMissing(condition.deviceId)) { + return `${path}:设备不能为空`; + } + } + + // 设备状态只校验操作符和状态枚举值。 + if (isDeviceStatus) { + if (!condition.operator) { + return `${path}:操作符不能为空`; + } + if (isEmptyVal(condition.param)) { + return `${path}:设备状态不能为空`; + } + return null; + } + + // 设备属性需要校验物模型标识符、操作符和比较值。 + if (isDeviceProperty) { + if (!condition.identifier) { + return `${path}:监控项不能为空`; + } + if (!condition.operator) { + return `${path}:操作符不能为空`; + } + if (isEmptyVal(condition.param)) { + return `${path}:比较值不能为空`; + } + return null; + } + + // 当前时间按操作符动态判断 param 是否需要填写。 + if (condition.type === IotRuleSceneTriggerConditionTypeEnum.CURRENT_TIME) { + if (!condition.operator) { + return `${path}:时间条件不能为空`; + } + + if ( + condition.operator === IotRuleSceneTriggerTimeOperatorEnum.TODAY.value + ) { + return null; + } + + if (isEmptyVal(condition.param)) { + return `${path}:时间值不能为空`; + } + + if ( + condition.operator === + IotRuleSceneTriggerTimeOperatorEnum.BETWEEN_TIME.value + ) { + const parts = String(condition.param).split(','); + if (!parts[0]?.trim() || !parts[1]?.trim()) { + return `${path}:开始和结束时间不能为空`; + } + } + } + + return null; +} + +/** + * 校验触发器的附加条件组。 + * + * 条件组结构是二维数组:外层是 OR 关系的条件组,内层是 AND 关系的条件列表。 + * 这里逐组、逐条件返回第一条错误,避免一次性弹出过多提示。 + * + * @param groups 附加条件组 + * @param triggerIndex 触发器在列表中的下标,用于生成可定位的错误提示 + * @returns 错误信息,通过则返回 null + */ +export function validateTriggerConditionGroups( + groups: RuleSceneApi.TriggerCondition[][] | undefined, + triggerIndex: number, +): null | string { + if (!groups?.length) { + return null; + } + + for (const [groupIndex, group] of groups.entries()) { + // 空条件组没有实际过滤条件,提交后语义不明确,需要拦截。 + if (!Array.isArray(group) || group.length === 0) { + return `触发器 ${triggerIndex + 1}:条件组 ${groupIndex + 1} 不能为空`; + } + + for (const [conditionIndex, condition] of group.entries()) { + const error = validateTriggerCondition( + condition, + `触发器 ${triggerIndex + 1} 条件组 ${groupIndex + 1} 条件 ${ + conditionIndex + 1 + }`, + ); + if (error) { + return error; + } + } + } + + return null; +} + +/** + * 校验单个触发器配置。 + * + * 该方法用于场景联动表单提交前兜底校验,避免触发器配置没有独立表单项 prop 时漏掉必填项。 + * 校验逻辑需要和触发器主条件 UI 保持一致,同时继续校验附加条件组。 + * + * @param trigger 触发器配置 + * @param index 触发器在列表中的下标,用于生成可定位的错误提示 + * @returns 错误信息,通过则返回 null + */ +export function validateTriggerItem( + trigger: RuleSceneApi.Trigger, + index: number, +): null | string { + const prefix = `触发器 ${index + 1}`; + + if (!trigger.type) { + return `${prefix}:触发器类型不能为空`; + } + + // 设备类触发器都有产品、设备两个基础字段;deviceId = 0 表示全部设备。 + if (isDeviceTrigger(trigger.type)) { + if (isRequiredIdMissing(trigger.productId)) { + return `${prefix}:产品不能为空`; + } + if (isDeviceIdMissing(trigger.deviceId)) { + return `${prefix}:设备不能为空`; + } + + // 设备状态变化不依赖物模型标识符,只校验操作符和状态值。 + if (trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_STATE_UPDATE) { + if (!trigger.operator) { + return `${prefix}:操作符不能为空`; + } + if (isEmptyVal(trigger.value)) { + return `${prefix}:设备状态不能为空`; + } + } else { + if (!trigger.identifier) { + return `${prefix}:监控项不能为空`; + } + + // 事件上报和服务调用只监听是否发生,不需要额外的操作符和比较值。 + const isEventOrService = + trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_EVENT_POST || + trigger.type === IotRuleSceneTriggerTypeEnum.DEVICE_SERVICE_INVOKE; + if (!isEventOrService) { + if (!trigger.operator) { + return `${prefix}:操作符不能为空`; + } + if (isEmptyVal(trigger.value)) { + return `${prefix}:参数值不能为空`; + } + } + } + } + + // 定时触发器需要 CRON 表达式,并继续校验 CRON 格式。 + if (trigger.type === IotRuleSceneTriggerTypeEnum.TIMER) { + if (!trigger.cronExpression) { + return `${prefix}:CRON 表达式不能为空`; + } + if (!CronUtils.validate(trigger.cronExpression)) { + return `${prefix}:CRON 表达式格式不正确`; + } + } + + return validateTriggerConditionGroups(trigger.conditionGroups, index); +} + +/** + * 校验触发器列表。 + * + * 场景联动至少需要一个触发器;列表内逐条返回第一条错误,避免一次提交出现多条提示。 + * + * @param triggers 触发器列表 + * @returns 错误信息,通过则返回 null + */ +export function validateSceneRuleTriggers( + triggers?: RuleSceneApi.Trigger[], +): null | string { + if (!triggers?.length) { + return '至少需要一个触发器'; + } + + for (const [index, trigger] of triggers.entries()) { + const error = validateTriggerItem(trigger, index); + if (error) { + return error; + } + } + + return null; +} + +/** + * 校验单个执行器配置。 + * + * 该方法用于场景联动表单提交前兜底校验,避免执行器配置没有独立表单项 prop 时漏掉必填项。 + * 校验逻辑需要和执行器 UI 保持一致。 + * + * @param action 执行器配置 + * @param index 执行器在列表中的下标,用于生成可定位的错误提示 + * @returns 错误信息,通过则返回 null + */ +export function validateActionItem( + action: RuleSceneApi.Action, + index: number, +): null | string { + const prefix = `执行器 ${index + 1}`; + + if (!action.type) { + return `${prefix}:执行器类型不能为空`; + } + + // 设备属性设置和设备服务调用都需要指定设备,并填写物模型参数。 + if ( + action.type === IotRuleSceneActionTypeEnum.DEVICE_PROPERTY_SET || + action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE + ) { + if (isRequiredIdMissing(action.productId)) { + return `${prefix}:产品不能为空`; + } + if (isDeviceIdMissing(action.deviceId)) { + return `${prefix}:设备不能为空`; + } + if ( + action.type === IotRuleSceneActionTypeEnum.DEVICE_SERVICE_INVOKE && + !action.identifier + ) { + return `${prefix}:服务不能为空`; + } + + if (isActionParamsEmpty(action.params)) { + return `${prefix}:参数配置不能为空`; + } + if (!isActionParamsJsonValid(action.params)) { + return `${prefix}:参数格式须为合法 JSON`; + } + + return null; + } + + // 告警恢复执行器需要绑定具体告警配置;触发告警不需要预选告警配置。 + if ( + action.type === IotRuleSceneActionTypeEnum.ALERT_RECOVER && + !action.alertConfigId + ) { + return `${prefix}:告警配置不能为空`; + } + + return null; +} + +/** + * 校验执行器列表。 + * + * 场景联动至少需要一个执行器;列表内逐条返回第一条错误,避免一次提交出现多条提示。 + * + * @param actions 执行器列表 + * @returns 错误信息,通过则返回 null + */ +export function validateSceneRuleActions( + actions?: RuleSceneApi.Action[], +): null | string { + if (!actions?.length) { + return '至少需要一个执行器'; + } + + for (const [index, action] of actions.entries()) { + const error = validateActionItem(action, index); + if (error) { + return error; + } + } + + return null; +} diff --git a/apps/web-antd/src/views/member/user/data.ts b/apps/web-antd/src/views/member/user/data.ts index d9a37cd45..414227f46 100644 --- a/apps/web-antd/src/views/member/user/data.ts +++ b/apps/web-antd/src/views/member/user/data.ts @@ -36,6 +36,17 @@ export function useFormSchema(): VbenFormSchema[] { }, rules: 'required', }, + { + fieldName: 'email', + label: '邮箱', + component: 'Input', + componentProps: { + allowClear: true, + maxlength: 50, + placeholder: '请输入邮箱', + }, + rules: z.string().email('邮箱格式不正确').or(z.literal('')).optional(), + }, { fieldName: 'status', label: '状态', @@ -153,6 +164,15 @@ export function useGridFormSchema(): VbenFormSchema[] { allowClear: true, }, }, + { + fieldName: 'email', + label: '邮箱', + component: 'Input', + componentProps: { + placeholder: '请输入邮箱', + allowClear: true, + }, + }, { fieldName: 'loginDate', label: '登录时间', @@ -236,6 +256,11 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { title: '手机号', minWidth: 120, }, + { + field: 'email', + title: '邮箱', + minWidth: 180, + }, { field: 'nickname', title: '昵称', diff --git a/apps/web-antd/src/views/member/user/detail/modules/basic-info.vue b/apps/web-antd/src/views/member/user/detail/modules/basic-info.vue index c3a8b4294..115ad683f 100644 --- a/apps/web-antd/src/views/member/user/detail/modules/basic-info.vue +++ b/apps/web-antd/src/views/member/user/detail/modules/basic-info.vue @@ -34,6 +34,10 @@ const [Descriptions] = useDescription({ field: 'mobile', label: '手机号', }, + { + field: 'email', + label: '邮箱', + }, { field: 'sex', label: '性别', diff --git a/apps/web-antd/src/views/mes/cal/plan/data.ts b/apps/web-antd/src/views/mes/cal/plan/data.ts index c0c957956..2fcb12099 100644 --- a/apps/web-antd/src/views/mes/cal/plan/data.ts +++ b/apps/web-antd/src/views/mes/cal/plan/data.ts @@ -16,7 +16,10 @@ import { getRangePickerDefaultProps } from '#/utils'; export type FormType = 'create' | 'detail' | 'update'; /** 新增/修改排班计划的表单 */ -export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { +export function useFormSchema( + formType: FormType, + formApi?: VbenFormApi, +): VbenFormSchema[] { return [ { fieldName: 'id', @@ -43,18 +46,21 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { placeholder: '请输入计划编码', }, rules: 'required', - suffix: () => - h( - Button, - { - type: 'default', - onClick: async () => { - const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_PLAN_CODE); - await formApi?.setFieldValue('code', code); - }, - }, - { default: () => '生成' }, - ), + suffix: + formType === 'detail' + ? undefined + : () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_PLAN_CODE); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ), }, { fieldName: 'name', diff --git a/apps/web-antd/src/views/mes/cal/plan/modules/form.vue b/apps/web-antd/src/views/mes/cal/plan/modules/form.vue index c2402d002..3ecbda54e 100644 --- a/apps/web-antd/src/views/mes/cal/plan/modules/form.vue +++ b/apps/web-antd/src/views/mes/cal/plan/modules/form.vue @@ -103,11 +103,11 @@ const [Modal, modalApi] = useVbenModal({ formData.value = undefined; return; } - formApi.setState({ schema: useFormSchema(formApi) }); subTabsName.value = 'shift'; // 加载数据 const data = modalApi.getData<{ formType: FormType; id?: number }>(); formType.value = data.formType; + formApi.setState({ schema: useFormSchema(data.formType, formApi) }); formApi.setDisabled(formType.value === 'detail'); modalApi.setState({ showConfirmButton: formType.value !== 'detail' }); if (!data?.id) { diff --git a/apps/web-antd/src/views/mes/cal/team/data.ts b/apps/web-antd/src/views/mes/cal/team/data.ts index fac2487aa..fb28dd230 100644 --- a/apps/web-antd/src/views/mes/cal/team/data.ts +++ b/apps/web-antd/src/views/mes/cal/team/data.ts @@ -16,7 +16,10 @@ import { generateAutoCode } from '#/api/mes/md/autocode/record'; export type FormType = 'create' | 'detail' | 'update'; /** 新增/修改班组的表单 */ -export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { +export function useFormSchema( + formType: FormType, + formApi?: VbenFormApi, +): VbenFormSchema[] { return [ { fieldName: 'id', @@ -35,18 +38,21 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { placeholder: '请输入班组编码', }, rules: z.string().min(1, '班组编码不能为空').max(64), - suffix: () => - h( - Button, - { - type: 'default', - onClick: async () => { - const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_TEAM_CODE); - await formApi?.setFieldValue('code', code); - }, - }, - { default: () => '生成' }, - ), + suffix: + formType === 'detail' + ? undefined + : () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode(MesAutoCodeRuleCode.CAL_TEAM_CODE); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ), }, { fieldName: 'name', diff --git a/apps/web-antd/src/views/mes/cal/team/modules/form.vue b/apps/web-antd/src/views/mes/cal/team/modules/form.vue index 388db173d..2406bcd43 100644 --- a/apps/web-antd/src/views/mes/cal/team/modules/form.vue +++ b/apps/web-antd/src/views/mes/cal/team/modules/form.vue @@ -79,11 +79,11 @@ const [Modal, modalApi] = useVbenModal({ formData.value = undefined; return; } - formApi.setState({ schema: useFormSchema(formApi) }); subTabsName.value = 'member'; // 加载数据 const data = modalApi.getData<{ formType: FormType; id?: number }>(); formType.value = data.formType; + formApi.setState({ schema: useFormSchema(data.formType, formApi) }); formApi.setDisabled(formType.value === 'detail'); modalApi.setState({ showConfirmButton: formType.value !== 'detail' }); if (!data?.id) { diff --git a/apps/web-antd/src/views/mes/dv/checkplan/components/index.ts b/apps/web-antd/src/views/mes/dv/checkplan/components/index.ts index ff080b299..87010bda0 100644 --- a/apps/web-antd/src/views/mes/dv/checkplan/components/index.ts +++ b/apps/web-antd/src/views/mes/dv/checkplan/components/index.ts @@ -1 +1,2 @@ +export { default as DvCheckPlanSelectDialog } from './select-dialog.vue'; export { default as DvCheckPlanSelect } from './select.vue'; diff --git a/apps/web-antd/src/views/mes/dv/checkplan/components/select-dialog.vue b/apps/web-antd/src/views/mes/dv/checkplan/components/select-dialog.vue new file mode 100644 index 000000000..2a25af0a6 --- /dev/null +++ b/apps/web-antd/src/views/mes/dv/checkplan/components/select-dialog.vue @@ -0,0 +1,215 @@ + + + diff --git a/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue b/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue index 1c3ab50e8..07e71de90 100644 --- a/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/checkplan/components/select.vue @@ -1,13 +1,19 @@ diff --git a/apps/web-antd/src/views/mes/dv/checkplan/data.ts b/apps/web-antd/src/views/mes/dv/checkplan/data.ts index 6c42eb7b3..ef7e5d640 100644 --- a/apps/web-antd/src/views/mes/dv/checkplan/data.ts +++ b/apps/web-antd/src/views/mes/dv/checkplan/data.ts @@ -17,7 +17,10 @@ import { getRangePickerDefaultProps } from '#/utils'; export type FormType = 'create' | 'detail' | 'update'; /** 新增/修改点检保养方案的表单 */ -export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { +export function useFormSchema( + formType: FormType, + formApi?: VbenFormApi, +): VbenFormSchema[] { return [ { fieldName: 'id', @@ -44,18 +47,21 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { placeholder: '请输入方案编码', }, rules: 'required', - suffix: () => - h( - Button, - { - type: 'default', - onClick: async () => { - const code = await generateAutoCode(MesAutoCodeRuleCode.DV_CHECK_PLAN_CODE); - await formApi?.setFieldValue('code', code); - }, - }, - { default: () => '生成' }, - ), + suffix: + formType === 'detail' + ? undefined + : () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode(MesAutoCodeRuleCode.DV_CHECK_PLAN_CODE); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ), }, { fieldName: 'name', @@ -239,3 +245,68 @@ export function useGridColumns(): VxeTableGridOptions['columns'] { + return [ + { type: multiple ? 'checkbox' : 'radio', width: 50 }, + { field: 'code', title: '计划编码', minWidth: 180 }, + { field: 'name', title: '计划名称', minWidth: 150 }, + { + field: 'type', + title: '计划类型', + width: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_DV_SUBJECT_TYPE }, + }, + }, + { field: 'startDate', title: '开始日期', width: 120, formatter: 'formatDate' }, + { field: 'endDate', title: '结束日期', width: 120, formatter: 'formatDate' }, + { field: 'cycleCount', title: '频率', width: 100 }, + { + field: 'cycleType', + title: '周期类型', + width: 120, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_DV_CYCLE_TYPE }, + }, + }, + { + field: 'status', + title: '状态', + width: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_DV_CHECK_PLAN_STATUS }, + }, + }, + ]; +} diff --git a/apps/web-antd/src/views/mes/dv/checkplan/modules/form.vue b/apps/web-antd/src/views/mes/dv/checkplan/modules/form.vue index 94b3f064d..391ab971d 100644 --- a/apps/web-antd/src/views/mes/dv/checkplan/modules/form.vue +++ b/apps/web-antd/src/views/mes/dv/checkplan/modules/form.vue @@ -78,11 +78,11 @@ const [Modal, modalApi] = useVbenModal({ formData.value = undefined; return; } - formApi.setState({ schema: useFormSchema(formApi) }); subTabsName.value = 'machinery'; // 加载数据 const data = modalApi.getData<{ formType: FormType; id?: number }>(); formType.value = data.formType; + formApi.setState({ schema: useFormSchema(data.formType, formApi) }); formApi.setDisabled(formType.value === 'detail'); modalApi.setState({ showConfirmButton: formType.value !== 'detail' }); if (!data?.id) { diff --git a/apps/web-antd/src/views/mes/dv/checkrecord/data.ts b/apps/web-antd/src/views/mes/dv/checkrecord/data.ts index 665e3c988..abb9ac626 100644 --- a/apps/web-antd/src/views/mes/dv/checkrecord/data.ts +++ b/apps/web-antd/src/views/mes/dv/checkrecord/data.ts @@ -53,7 +53,6 @@ export function useFormSchema(): VbenFormSchema[] { type: MesDvSubjectTypeEnum.CHECK, placeholder: '请选择计划', }, - rules: 'selectRequired', }, { fieldName: 'userId', @@ -66,7 +65,6 @@ export function useFormSchema(): VbenFormSchema[] { placeholder: '请选择点检人', valueField: 'id', }, - rules: 'selectRequired', }, { fieldName: 'checkTime', diff --git a/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue b/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue index 9debff5fd..3b77b79fd 100644 --- a/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue +++ b/apps/web-antd/src/views/mes/dv/checkrecord/modules/line-list.vue @@ -2,9 +2,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvCheckRecordLineApi } from '#/api/mes/dv/checkrecord/line'; -import { computed, MesDvCheckResultEnum, MesDvSubjectTypeEnum, ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; -import { DICT_TYPE } from '@vben/constants'; +import { DICT_TYPE, MesDvCheckResultEnum, MesDvSubjectTypeEnum } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { message, Modal } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/dv/machinery/components/index.ts b/apps/web-antd/src/views/mes/dv/machinery/components/index.ts index fee5e15af..b3d061fac 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/components/index.ts +++ b/apps/web-antd/src/views/mes/dv/machinery/components/index.ts @@ -1 +1,2 @@ +export { default as DvMachinerySelectDialog } from './select-dialog.vue'; export { default as DvMachinerySelect } from './select.vue'; diff --git a/apps/web-antd/src/views/mes/dv/machinery/components/select-dialog.vue b/apps/web-antd/src/views/mes/dv/machinery/components/select-dialog.vue new file mode 100644 index 000000000..86d0f34a2 --- /dev/null +++ b/apps/web-antd/src/views/mes/dv/machinery/components/select-dialog.vue @@ -0,0 +1,225 @@ + + + diff --git a/apps/web-antd/src/views/mes/dv/machinery/components/select.vue b/apps/web-antd/src/views/mes/dv/machinery/components/select.vue index ea9b543c8..5104e9771 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/components/select.vue +++ b/apps/web-antd/src/views/mes/dv/machinery/components/select.vue @@ -1,58 +1,138 @@ diff --git a/apps/web-antd/src/views/mes/dv/machinery/data.ts b/apps/web-antd/src/views/mes/dv/machinery/data.ts index b32f87ad9..a0e08b633 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/data.ts +++ b/apps/web-antd/src/views/mes/dv/machinery/data.ts @@ -41,18 +41,21 @@ export function useFormSchema(formType: FormType, formApi?: VbenFormApi): VbenFo componentProps: (values) => ({ disabled: !!values.id }), }, rules: 'required', - suffix: () => - h( - Button, - { - type: 'default', - onClick: async () => { - const code = await generateAutoCode(MesAutoCodeRuleCode.DV_MACHINERY_CODE); - await formApi?.setFieldValue('code', code); - }, - }, - { default: () => '生成' }, - ), + suffix: + formType === 'detail' + ? undefined + : () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode(MesAutoCodeRuleCode.DV_MACHINERY_CODE); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ), }, { fieldName: 'name', @@ -273,3 +276,65 @@ export function useImportFormSchema(): VbenFormSchema[] { }, ]; } + +/** 设备选择弹窗的搜索表单 */ +export function useMachinerySelectGridFormSchema(): VbenFormSchema[] { + return [ + { + fieldName: 'code', + label: '设备编码', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入设备编码', + }, + }, + { + fieldName: 'name', + label: '设备名称', + component: 'Input', + componentProps: { + allowClear: true, + placeholder: '请输入设备名称', + }, + }, + { + fieldName: 'workshopId', + label: '所属车间', + component: markRaw(MdWorkshopSelect), + componentProps: { + allowClear: true, + placeholder: '请选择所属车间', + }, + }, + ]; +} + +/** 设备选择弹窗的字段 */ +export function useMachinerySelectGridColumns( + multiple = false, +): VxeTableGridOptions['columns'] { + return [ + { type: multiple ? 'checkbox' : 'radio', width: 50 }, + { field: 'code', title: '设备编码', width: 120 }, + { field: 'name', title: '设备名称', minWidth: 120 }, + { field: 'brand', title: '品牌', minWidth: 120 }, + { field: 'specification', title: '规格型号', minWidth: 120 }, + { field: 'workshopName', title: '所属车间', width: 120 }, + { + field: 'status', + title: '设备状态', + width: 100, + cellRender: { + name: 'CellDict', + props: { type: DICT_TYPE.MES_DV_MACHINERY_STATUS }, + }, + }, + { + field: 'createTime', + title: '创建时间', + width: 160, + formatter: 'formatDateTime', + }, + ]; +} diff --git a/apps/web-antd/src/views/mes/dv/machinery/type/data.ts b/apps/web-antd/src/views/mes/dv/machinery/type/data.ts index e817fcea1..5bdf07bf6 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/type/data.ts +++ b/apps/web-antd/src/views/mes/dv/machinery/type/data.ts @@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvMachineryTypeApi } from '#/api/mes/dv/machinery/type'; -import { DICT_TYPE, h } from 'vue'; +import { h } from 'vue'; -import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants'; +import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { handleTree } from '@vben/utils'; @@ -14,8 +14,14 @@ import { z } from '#/adapter/form'; import { getMachineryTypeList } from '#/api/mes/dv/machinery/type'; import { generateAutoCode } from '#/api/mes/md/autocode/record'; +/** 表单类型 */ +export type FormType = 'create' | 'detail' | 'update'; + /** 新增/修改设备类型的表单 */ -export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { +export function useFormSchema( + formType: FormType, + formApi?: VbenFormApi, +): VbenFormSchema[] { return [ { fieldName: 'id', @@ -52,18 +58,23 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { placeholder: '请输入类型编码', }, rules: z.string().min(1, '类型编码不能为空').max(64), - suffix: () => - h( - Button, - { - type: 'default', - onClick: async () => { - const code = await generateAutoCode(MesAutoCodeRuleCode.DV_MACHINERY_TYPE_CODE); - await formApi?.setFieldValue('code', code); - }, - }, - { default: () => '生成' }, - ), + suffix: + formType === 'detail' + ? undefined + : () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode( + MesAutoCodeRuleCode.DV_MACHINERY_TYPE_CODE, + ); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ), }, { fieldName: 'name', diff --git a/apps/web-antd/src/views/mes/dv/machinery/type/modules/form.vue b/apps/web-antd/src/views/mes/dv/machinery/type/modules/form.vue index 4a0269d17..f88cb4c0f 100644 --- a/apps/web-antd/src/views/mes/dv/machinery/type/modules/form.vue +++ b/apps/web-antd/src/views/mes/dv/machinery/type/modules/form.vue @@ -64,9 +64,11 @@ const [Modal, modalApi] = useVbenModal({ formData.value = undefined; return; } - formApi.setState({ schema: useFormSchema(formApi) }); // 加载数据 const data = modalApi.getData(); + formApi.setState({ + schema: useFormSchema(data?.id ? 'update' : 'create', formApi), + }); if (!data || !data.id) { formData.value = data || undefined; if (data) { diff --git a/apps/web-antd/src/views/mes/dv/maintenrecord/data.ts b/apps/web-antd/src/views/mes/dv/maintenrecord/data.ts index badcf2162..4a9fbeb42 100644 --- a/apps/web-antd/src/views/mes/dv/maintenrecord/data.ts +++ b/apps/web-antd/src/views/mes/dv/maintenrecord/data.ts @@ -53,7 +53,6 @@ export function useFormSchema(): VbenFormSchema[] { type: MesDvSubjectTypeEnum.MAINTENANCE, placeholder: '请选择计划', }, - rules: 'selectRequired', }, { fieldName: 'userId', @@ -66,7 +65,6 @@ export function useFormSchema(): VbenFormSchema[] { placeholder: '请选择保养人', valueField: 'id', }, - rules: 'selectRequired', }, { fieldName: 'maintenTime', diff --git a/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue b/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue index 1b03fa032..9c16b9c74 100644 --- a/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue +++ b/apps/web-antd/src/views/mes/dv/maintenrecord/modules/line-list.vue @@ -2,9 +2,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { MesDvMaintenRecordLineApi } from '#/api/mes/dv/maintenrecord/line'; -import { computed, MesDvMaintenStatusEnum, MesDvSubjectTypeEnum, ref, watch } from 'vue'; +import { computed, ref, watch } from 'vue'; -import { DICT_TYPE } from '@vben/constants'; +import { DICT_TYPE, MesDvMaintenStatusEnum, MesDvSubjectTypeEnum } from '@vben/constants'; import { getDictOptions } from '@vben/hooks'; import { message, Modal } from 'ant-design-vue'; diff --git a/apps/web-antd/src/views/mes/dv/repair/data.ts b/apps/web-antd/src/views/mes/dv/repair/data.ts index 524f41e87..a2eaaae1f 100644 --- a/apps/web-antd/src/views/mes/dv/repair/data.ts +++ b/apps/web-antd/src/views/mes/dv/repair/data.ts @@ -16,8 +16,17 @@ import { DvMachinerySelect } from '#/views/mes/dv/machinery/components'; /** 表单类型 */ export type FormType = 'confirm' | 'create' | 'detail' | 'finish' | 'update'; +/** 表头是否只读(完成维修、验收、详情态;finishDate 在 confirm 单独放开) */ +function isHeaderReadonly(formType: FormType): boolean { + return ['confirm', 'detail', 'finish'].includes(formType); +} + /** 新增/修改维修工单的表单 */ -export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { +export function useFormSchema( + formType: FormType, + formApi?: VbenFormApi, +): VbenFormSchema[] { + const headerReadonly = isHeaderReadonly(formType); return [ { fieldName: 'id', @@ -45,27 +54,30 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { }, dependencies: { triggerFields: ['id'], - componentProps: (values) => ({ disabled: !!values.id }), + componentProps: (values) => ({ disabled: headerReadonly || !!values.id }), }, rules: 'required', - suffix: () => - h( - Button, - { - type: 'default', - onClick: async () => { - const code = await generateAutoCode(MesAutoCodeRuleCode.DV_REPAIR_CODE); - await formApi?.setFieldValue('code', code); - }, - }, - { default: () => '生成' }, - ), + suffix: headerReadonly + ? undefined + : () => + h( + Button, + { + type: 'default', + onClick: async () => { + const code = await generateAutoCode(MesAutoCodeRuleCode.DV_REPAIR_CODE); + await formApi?.setFieldValue('code', code); + }, + }, + { default: () => '生成' }, + ), }, { fieldName: 'name', label: '维修单名称', component: 'Input', componentProps: { + disabled: headerReadonly, placeholder: '请输入维修单名称', }, rules: 'required', @@ -75,6 +87,7 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { label: '设备', component: markRaw(DvMachinerySelect), componentProps: { + disabled: headerReadonly, placeholder: '请选择设备', }, rules: 'selectRequired', @@ -84,6 +97,7 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { label: '报修日期', component: 'DatePicker', componentProps: { + disabled: headerReadonly, format: 'YYYY-MM-DD HH:mm:ss', placeholder: '请选择报修日期', showTime: true, @@ -98,10 +112,18 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { componentProps: { allowClear: true, api: getSimpleUserList, + disabled: true, labelField: 'nickname', placeholder: '请选择维修人', valueField: 'id', }, + // 维修人为待验收(≥APPROVING)态自动产生的只读回显字段 + dependencies: { + triggerFields: ['status'], + if: (values) => + values.status != null && + values.status >= MesDvRepairStatusEnum.APPROVING, + }, }, { fieldName: 'finishDate', @@ -113,6 +135,15 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { showTime: true, valueFormat: 'x', }, + // 维修中(≥CONFIRMED)态展示;仅"完成维修"弹窗可编辑并必填,其余态只读回显 + dependencies: { + triggerFields: ['status'], + if: (values) => + values.status != null && + values.status >= MesDvRepairStatusEnum.CONFIRMED, + disabled: formType !== 'confirm', + rules: () => (formType === 'confirm' ? 'required' : null), + }, }, { fieldName: 'confirmUserId', @@ -121,21 +152,37 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { componentProps: { allowClear: true, api: getSimpleUserList, + disabled: true, labelField: 'nickname', placeholder: '请选择验收人', valueField: 'id', }, + // 验收信息为已确认(≥FINISHED)态自动产生的只读回显字段 + dependencies: { + triggerFields: ['status'], + if: (values) => + values.status != null && + values.status >= MesDvRepairStatusEnum.FINISHED, + }, }, { fieldName: 'confirmDate', label: '验收日期', component: 'DatePicker', componentProps: { + disabled: true, format: 'YYYY-MM-DD HH:mm:ss', placeholder: '请选择验收日期', showTime: true, valueFormat: 'x', }, + // 验收信息为已确认(≥FINISHED)态自动产生的只读回显字段 + dependencies: { + triggerFields: ['status'], + if: (values) => + values.status != null && + values.status >= MesDvRepairStatusEnum.FINISHED, + }, }, { fieldName: 'result', @@ -143,9 +190,17 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { component: 'RadioGroup', componentProps: { buttonStyle: 'solid', + disabled: true, optionType: 'button', options: getDictOptions(DICT_TYPE.MES_DV_REPAIR_RESULT, 'number'), }, + // 验收信息为已确认(≥FINISHED)态自动产生的只读回显字段 + dependencies: { + triggerFields: ['status'], + if: (values) => + values.status != null && + values.status >= MesDvRepairStatusEnum.FINISHED, + }, }, { fieldName: 'remark', @@ -153,6 +208,7 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] { component: 'Textarea', formItemClass: 'col-span-3', componentProps: { + disabled: headerReadonly, placeholder: '请输入备注', rows: 3, }, diff --git a/apps/web-antd/src/views/mes/dv/repair/modules/form.vue b/apps/web-antd/src/views/mes/dv/repair/modules/form.vue index 1ae2e612b..efbe2058b 100644 --- a/apps/web-antd/src/views/mes/dv/repair/modules/form.vue +++ b/apps/web-antd/src/views/mes/dv/repair/modules/form.vue @@ -28,20 +28,29 @@ const emit = defineEmits(['success']); const formType = ref('create'); const formData = ref(); const isDetail = computed(() => formType.value === 'detail'); -const isReadonly = computed(() => ['confirm', 'detail', 'finish'].includes(formType.value)); +const isLineReadonly = computed(() => + // 明细只读:完成维修/验收/详情态明细均不可编辑 + ['confirm', 'detail', 'finish'].includes(formType.value), +); +const isFormDisabled = computed(() => + // 整表禁用:仅详情/验收态;完成维修态需放开 finishDate,故单头只读改由 schema 逐字段控制 + ['detail', 'finish'].includes(formType.value), +); const canSubmit = computed( () => formType.value === 'update' && formData.value?.status === MesDvRepairStatusEnum.PREPARE, ); -const getTitle = computed( - () => - ({ - create: '新增维修工单', - update: '修改维修工单', - confirm: '完成维修', - finish: '验收维修', - detail: '查看维修工单', - })[formType.value], -); +const getTitle = computed(() => { + if (formType.value === 'detail') { + return '查看维修工单'; + } + if (formType.value === 'confirm') { + return '完成维修'; + } + if (formType.value === 'finish') { + return '验收维修'; + } + return formType.value === 'update' ? '修改维修工单' : '新增维修工单'; +}); const [Form, formApi] = useVbenForm({ commonConfig: { @@ -161,11 +170,11 @@ const [Modal, modalApi] = useVbenModal({ formData.value = undefined; return; } - formApi.setState({ schema: useFormSchema(formApi) }); // 加载数据 const data = modalApi.getData<{ formType: FormType; id?: number }>(); formType.value = data.formType; - formApi.setDisabled(isReadonly.value); + formApi.setState({ schema: useFormSchema(data.formType, formApi) }); + formApi.setDisabled(isFormDisabled.value); modalApi.setState({ showConfirmButton: ['create', 'update'].includes(formType.value) }); if (!data?.id) { return; @@ -184,7 +193,7 @@ const [Modal, modalApi] = useVbenModal({