!352 MES 所有功能的迁移

Merge pull request !352 from 芋道源码/migration
pull/353/MERGE
芋道源码 2026-05-31 11:33:31 +00:00 committed by Gitee
commit e943f4fcfd
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
249 changed files with 8572 additions and 2498 deletions

View File

@ -14,6 +14,9 @@ export namespace AlertConfigApi {
receiveUserIds?: number[];
receiveUserNames?: string[];
receiveTypes?: number[];
smsTemplateCode?: string;
mailTemplateCode?: string;
notifyTemplateCode?: string;
createTime?: Date;
}
}

View File

@ -13,6 +13,7 @@ export namespace MemberUserApi {
loginIp: string;
mark: string;
mobile: string;
email?: string;
name?: string;
nickname?: string;
registerIp: string;

View File

@ -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<PageResult<MesProTaskIssueApi.TaskIssue>>(
'/mes/pro/task-issue/page',
{ params },
);
}
/** 查询生产任务投料详情 */
export function getTaskIssue(id: number) {
return requestClient.get<MesProTaskIssueApi.TaskIssue>(
`/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<MesProTaskIssueApi.TaskIssue[]>(
`/mes/pro/task-issue/list-by-task?taskId=${taskId}`,
);
}

View File

@ -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; // 创建时间
}
}

View File

@ -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[]; // 入库日期
}
}

View File

@ -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[]; // 创建时间
}
}

View File

@ -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<SystemMailTemplateApi.MailTemplateSimple[]>(
'/system/mail-template/simple-list',
);
}
/** 查询邮件模版详情 */
export function getMailTemplate(id: number) {
return requestClient.get<SystemMailTemplateApi.MailTemplate>(

View File

@ -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<SystemNotifyTemplateApi.NotifyTemplateSimple[]>(
'/system/notify-template/simple-list',
);
}
/** 查询站内信模板详情 */
export function getNotifyTemplate(id: number) {
return requestClient.get<SystemNotifyTemplateApi.NotifyTemplate>(

View File

@ -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<SystemSmsTemplateApi.SmsTemplateSimple[]>(
'/system/sms-template/simple-list',
);
}
/** 查询短信模板详情 */
export function getSmsTemplate(id: number) {
return requestClient.get<SystemSmsTemplateApi.SmsTemplate>(

View File

@ -21,7 +21,7 @@ export function useAreaSelectRule() {
title: label,
info: '',
$required: false,
modelField: 'value', // 特殊ele 里是 model-valueantd 里是 value
modelField: 'value', // Ant Design Vue 组件使用 valueweb-ele 自定义组件使用默认 modelValue
};
},
props(_: any, { t }: any) {

View File

@ -39,7 +39,7 @@ export function useDictSelectRule() {
title: label,
info: '',
$required: false,
modelField: 'value', // 特殊ele 里是 model-valueantd 里是 value
modelField: 'value', // Ant Design Vue 组件使用 valueweb-ele 自定义组件使用默认 modelValue
};
},
props(_: any, { t }: any) {

View File

@ -21,7 +21,7 @@ export function useIframeRule() {
title: label,
info: '',
$required: false,
modelField: 'value', // 特殊ele 里是 model-valueantd 里是 value
modelField: 'value', // Ant Design Vue 组件使用 valueweb-ele 自定义组件使用默认 modelValue
};
},
props(_: any, { t }: any) {

View File

@ -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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
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);

View File

@ -45,6 +45,7 @@ const [Grid] = useVbenVxeGrid({
return await getCluePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
sceneType: 1,
transformStatus: false,
...formValues,
});

View File

@ -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<Record<string, any>>, 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',
},
];
}

View File

@ -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 paramBETWEEN_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();
}

View File

@ -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;
}

View File

@ -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: '昵称',

View File

@ -34,6 +34,10 @@ const [Descriptions] = useDescription({
field: 'mobile',
label: '手机号',
},
{
field: 'email',
label: '邮箱',
},
{
field: 'sex',
label: '性别',

View File

@ -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',

View File

@ -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) {

View File

@ -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',

View File

@ -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) {

View File

@ -1 +1,2 @@
export { default as DvCheckPlanSelectDialog } from './select-dialog.vue';
export { default as DvCheckPlanSelect } from './select.vue';

View File

@ -0,0 +1,215 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvCheckPlanApi } from '#/api/mes/dv/checkplan';
import { nextTick, ref } from 'vue';
import { Button, message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getCheckPlanPage } from '#/api/mes/dv/checkplan';
import {
useCheckPlanSelectGridColumns,
useCheckPlanSelectGridFormSchema,
} from '../data';
defineOptions({ name: 'DvCheckPlanSelectDialog' });
const emit = defineEmits<{
selected: [rows: MesDvCheckPlanApi.CheckPlan[]];
}>();
const open = ref(false); //
const multiple = ref(false); //
const fixedType = ref<number>(); //
const fixedStatus = ref<number>(); //
const selectedRows = ref<MesDvCheckPlanApi.CheckPlan[]>([]); //
const preSelectedIds = ref<number[]>([]); //
/** 获取当前表格数据 */
function getTableRows() {
return gridApi.grid.getTableData().fullData as MesDvCheckPlanApi.CheckPlan[];
}
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesDvCheckPlanApi.CheckPlan>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesDvCheckPlanApi.CheckPlan[];
records.forEach((row) => {
const rowId = row.id;
if (rowId != null) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理单选变化 */
function handleRadioChange(row: MesDvCheckPlanApi.CheckPlan) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesDvCheckPlanApi.CheckPlan) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 处理行双击 */
async function handleCellDblclick({ row }: { row: MesDvCheckPlanApi.CheckPlan }) {
if (multiple.value) {
await toggleMultipleRow(row);
return;
}
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 回显预选方案 */
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = getTableRows();
for (const row of rows) {
if (row.id == null || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useCheckPlanSelectGridFormSchema(),
},
gridOptions: {
columns: useCheckPlanSelectGridColumns(false),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getCheckPlanPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
status: fixedStatus.value,
type: fixedType.value,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesDvCheckPlanApi.CheckPlan>,
gridEvents: {
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesDvCheckPlanApi.CheckPlan }) => {
handleRadioChange(row);
},
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
/** 打开方案选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean; status?: number; type?: number },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
fixedType.value = options?.type;
fixedStatus.value = options?.status;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useCheckPlanSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
await applyPreSelection();
}
/** 关闭方案选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择方案 */
function handleConfirm() {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="点检方案选择"
width="70%"
:destroy-on-close="true"
@ok="handleConfirm"
@cancel="closeModal"
>
<Grid table-title="" />
<template #footer>
<Button @click="closeModal"> </Button>
<Button type="primary" @click="handleConfirm"> </Button>
</template>
</Modal>
</template>

View File

@ -1,13 +1,19 @@
<script lang="ts" setup>
import type { SelectValue } from 'ant-design-vue/es/select';
import type { MesDvCheckPlanApi } from '#/api/mes/dv/checkplan';
import { onMounted, ref, watch } from 'vue';
import { computed, ref, useAttrs, watch } from 'vue';
import { Select } from 'ant-design-vue';
import { DICT_TYPE } from '@vben/constants';
import { IconifyIcon } from '@vben/icons';
import { getCheckPlanPage } from '#/api/mes/dv/checkplan';
import { Input, Tooltip } from 'ant-design-vue';
import { getCheckPlan } from '#/api/mes/dv/checkplan';
import { DictTag } from '#/components/dict-tag';
import DvCheckPlanSelectDialog from './select-dialog.vue';
defineOptions({ name: 'DvCheckPlanSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
@ -22,53 +28,125 @@ const props = withDefaults(
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择计划',
placeholder: '请选择保养方案',
status: undefined,
type: undefined,
},
);
const emit = defineEmits<{
change: [row?: MesDvCheckPlanApi.CheckPlan];
'update:modelValue': [value?: number];
change: [item: MesDvCheckPlanApi.CheckPlan | undefined];
'update:modelValue': [value: number | undefined];
}>();
const list = ref<MesDvCheckPlanApi.CheckPlan[]>([]); //
const attrs = useAttrs(); //
const dialogRef = ref<InstanceType<typeof DvCheckPlanSelectDialog>>(); //
const hovering = ref(false); //
const selectedItem = ref<MesDvCheckPlanApi.CheckPlan>(); //
/** 加载点检计划列表 */
async function getList() {
const data = await getCheckPlanPage({
pageNo: 1,
pageSize: 100,
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据方案编号回显选择器 */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getCheckPlan(id);
}
watch(
() => props.modelValue,
(value) => {
resolveItemById(value);
},
{ immediate: true },
);
/** 清空已选方案 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
/** 打开方案选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, {
multiple: false,
status: props.status,
type: props.type,
});
list.value = data.list || [];
}
/** 处理点检计划选择变化 */
function handleChange(value: SelectValue) {
const planId = typeof value === 'number' ? value : undefined;
emit('update:modelValue', planId);
emit(
'change',
list.value.find((item) => item.id === planId),
);
/** 回填选中的方案 */
function handleSelected(rows: MesDvCheckPlanApi.CheckPlan[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
watch(() => [props.status, props.type], getList);
onMounted(getList);
</script>
<template>
<Select
:allow-clear="allowClear"
:disabled="disabled"
:field-names="{ label: 'name', value: 'id' }"
:options="list"
:placeholder="placeholder"
:value="modelValue"
<div
v-bind="attrs"
class="w-full"
option-filter-prop="name"
show-search
@change="handleChange"
/>
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>编码{{ selectedItem.code || '-' }}</div>
<div>名称{{ selectedItem.name || '-' }}</div>
<div class="flex items-center">
频度{{ selectedItem.cycleCount ?? '-' }}
<DictTag
class="ml-1"
:type="DICT_TYPE.MES_DV_CYCLE_TYPE"
:value="selectedItem.cycleType"
/>
</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<DvCheckPlanSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@ -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<MesDvCheckPlanApi.CheckPla
},
];
}
/** 点检方案选择弹窗的搜索表单 */
export function useCheckPlanSelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '计划编号',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入计划编号',
},
},
{
fieldName: 'name',
label: '计划名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入计划名称',
},
},
];
}
/** 点检方案选择弹窗的字段 */
export function useCheckPlanSelectGridColumns(
multiple = false,
): VxeTableGridOptions<MesDvCheckPlanApi.CheckPlan>['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 },
},
},
];
}

View File

@ -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) {

View File

@ -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',

View File

@ -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';

View File

@ -1 +1,2 @@
export { default as DvMachinerySelectDialog } from './select-dialog.vue';
export { default as DvMachinerySelect } from './select.vue';

View File

@ -0,0 +1,225 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvMachineryApi } from '#/api/mes/dv/machinery';
import type { MesDvMachineryTypeApi } from '#/api/mes/dv/machinery/type';
import { nextTick, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getMachineryPage } from '#/api/mes/dv/machinery';
import { MachineryTypeTree } from '#/views/mes/dv/machinery/type/components';
import {
useMachinerySelectGridColumns,
useMachinerySelectGridFormSchema,
} from '../data';
defineOptions({ name: 'DvMachinerySelectDialog' });
const emit = defineEmits<{
selected: [rows: MesDvMachineryApi.Machinery[]];
}>();
const open = ref(false); //
const multiple = ref(false); //
const selectedRows = ref<MesDvMachineryApi.Machinery[]>([]); //
const selectedMachineryTypeId = ref<number>(); //
const preSelectedIds = ref<number[]>([]); //
const typeTreeRef = ref<InstanceType<typeof MachineryTypeTree>>(); //
/** 获取当前表格数据 */
function getTableRows() {
return gridApi.grid.getTableData().fullData as MesDvMachineryApi.Machinery[];
}
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesDvMachineryApi.Machinery>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesDvMachineryApi.Machinery[];
records.forEach((row) => {
const rowId = row.id;
if (rowId != null) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理单选变化 */
function handleRadioChange(row: MesDvMachineryApi.Machinery) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesDvMachineryApi.Machinery) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 处理行双击 */
async function handleCellDblclick({ row }: { row: MesDvMachineryApi.Machinery }) {
if (multiple.value) {
await toggleMultipleRow(row);
return;
}
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 按设备类型筛选 */
function handleTypeNodeClick(row: MesDvMachineryTypeApi.MachineryType | undefined) {
selectedMachineryTypeId.value = row?.id;
gridApi.query();
}
/** 回显预选设备 */
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = getTableRows();
for (const row of rows) {
if (row.id == null || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useMachinerySelectGridFormSchema(),
},
gridOptions: {
columns: useMachinerySelectGridColumns(false),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getMachineryPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
machineryTypeId: selectedMachineryTypeId.value,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesDvMachineryApi.Machinery>,
gridEvents: {
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesDvMachineryApi.Machinery }) => {
handleRadioChange(row);
},
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedMachineryTypeId.value = undefined;
selectedRows.value = [];
typeTreeRef.value?.reset();
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
/** 打开设备选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useMachinerySelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
await applyPreSelection();
}
/** 关闭设备选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择设备 */
function handleConfirm() {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="设备选择"
width="80%"
:destroy-on-close="true"
@ok="handleConfirm"
@cancel="closeModal"
>
<div class="flex h-full w-full">
<div class="mr-4 h-full w-1/5">
<MachineryTypeTree ref="typeTreeRef" @node-click="handleTypeNodeClick" />
</div>
<div class="w-4/5">
<Grid table-title="" />
</div>
</div>
</Modal>
</template>

View File

@ -1,58 +1,138 @@
<script lang="ts" setup>
import type { SelectValue } from 'ant-design-vue/es/select';
import type { MesDvMachineryApi } from '#/api/mes/dv/machinery';
import { onMounted, ref } from 'vue';
import { computed, ref, useAttrs, watch } from 'vue';
import { Select } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import { getMachinerySimpleList } from '#/api/mes/dv/machinery';
import { Input, Tooltip } from 'ant-design-vue';
withDefaults(
import { getMachinery } from '#/api/mes/dv/machinery';
import DvMachinerySelectDialog from './select-dialog.vue';
defineOptions({ name: 'DvMachinerySelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
}>(),
{ allowClear: true, disabled: false, modelValue: undefined, placeholder: '请选择设备' },
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择设备',
},
);
const emit = defineEmits<{
change: [row?: MesDvMachineryApi.Machinery];
'update:modelValue': [value?: number];
change: [item: MesDvMachineryApi.Machinery | undefined];
'update:modelValue': [value: number | undefined];
}>();
const list = ref<MesDvMachineryApi.Machinery[]>([]); //
const attrs = useAttrs(); //
const dialogRef = ref<InstanceType<typeof DvMachinerySelectDialog>>(); //
const hovering = ref(false); //
const selectedItem = ref<MesDvMachineryApi.Machinery>(); //
/** 加载设备列表 */
async function getList() {
list.value = await getMachinerySimpleList();
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据设备编号回显选择器 */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getMachinery(id);
}
/** 处理设备选择变化 */
function handleChange(value: SelectValue) {
const machineryId = typeof value === 'number' ? value : undefined;
emit('update:modelValue', machineryId);
emit(
'change',
list.value.find((item) => item.id === machineryId),
);
watch(
() => props.modelValue,
(value) => {
resolveItemById(value);
},
{ immediate: true },
);
/** 清空已选设备 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
onMounted(getList);
/** 打开设备选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, { multiple: false });
}
/** 回填选中的设备 */
function handleSelected(rows: MesDvMachineryApi.Machinery[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<Select
:allow-clear="allowClear"
:disabled="disabled"
:field-names="{ label: 'name', value: 'id' }"
:options="list"
:placeholder="placeholder"
:value="modelValue"
<div
v-bind="attrs"
class="w-full"
option-filter-prop="name"
show-search
@change="handleChange"
/>
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>设备编码{{ selectedItem.code || '-' }}</div>
<div>设备名称{{ selectedItem.name || '-' }}</div>
<div v-if="selectedItem.brand">{{ selectedItem.brand }}</div>
<div v-if="selectedItem.specification">
规格型号{{ selectedItem.specification }}
</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<DvMachinerySelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@ -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<MesDvMachineryApi.Machinery>['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',
},
];
}

View File

@ -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',

View File

@ -64,9 +64,11 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
//
const data = modalApi.getData<MesDvMachineryTypeApi.MachineryType>();
formApi.setState({
schema: useFormSchema(data?.id ? 'update' : 'create', formApi),
});
if (!data || !data.id) {
formData.value = data || undefined;
if (data) {

View File

@ -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',

View File

@ -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';

View File

@ -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,
},

View File

@ -28,20 +28,29 @@ const emit = defineEmits(['success']);
const formType = ref<FormType>('create');
const formData = ref<MesDvRepairApi.Repair>();
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({
<template>
<Modal :title="getTitle" class="w-4/5">
<Form class="mx-4" />
<LineList v-if="formData?.id" :disabled="isReadonly" :repair-id="formData.id" />
<LineList v-if="formData?.id" :disabled="isLineReadonly" :repair-id="formData.id" />
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
<Popconfirm

View File

@ -5,10 +5,14 @@ import type { MesDvSubjectApi } from '#/api/mes/dv/subject';
import { computed, onMounted, ref } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
import { Select } from 'ant-design-vue';
import { getSubjectSimpleList } from '#/api/mes/dv/subject';
defineOptions({ name: 'DvSubjectSelect' });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
@ -30,9 +34,26 @@ const emit = defineEmits<{
'update:modelValue': [value?: number];
}>();
const list = ref<MesDvSubjectApi.Subject[]>([]); //
const filteredList = computed( //
() => list.value.filter((item) => !props.type || item.type === props.type),
);
const filteredList = computed(() => {
//
const result: Array<MesDvSubjectApi.Subject & { disabled?: boolean }> =
list.value.filter(
(item) =>
item.status === CommonStatusEnum.ENABLE &&
(!props.type || item.type === props.type),
);
//
if (
props.modelValue != null &&
!result.some((item) => item.id === props.modelValue)
) {
const current = list.value.find((item) => item.id === props.modelValue);
if (current) {
result.push({ ...current, disabled: true });
}
}
return result;
});
/** 加载项目列表 */
async function getList() {
@ -43,10 +64,7 @@ async function getList() {
function handleChange(value: SelectValue) {
const subjectId = typeof value === 'number' ? value : undefined;
emit('update:modelValue', subjectId);
emit(
'change',
list.value.find((item) => item.id === subjectId),
);
emit('change', list.value.find((item) => item.id === subjectId));
}
onMounted(getList);

View File

@ -2,9 +2,14 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesDvSubjectApi } from '#/api/mes/dv/subject';
import { DICT_TYPE, h } from 'vue';
import { h } from 'vue';
import { CommonStatusEnum, MesAutoCodeRuleCode, MesDvSubjectTypeEnum } from '@vben/constants';
import {
CommonStatusEnum,
DICT_TYPE,
MesAutoCodeRuleCode,
MesDvSubjectTypeEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@ -16,7 +21,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',
@ -34,18 +42,21 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
placeholder: '请输入项目编码',
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(MesAutoCodeRuleCode.DV_SUBJECT_CODE);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(MesAutoCodeRuleCode.DV_SUBJECT_CODE);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',

View File

@ -66,10 +66,10 @@ const [Modal, modalApi] = useVbenModal({
if (!isOpen) {
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
//
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) {

View File

@ -448,10 +448,7 @@ export function usePartGridColumns(): VxeTableGridOptions<MesMdAutoCodePartApi.A
title: '循环方式',
width: 120,
align: 'center',
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_MD_AUTO_CODE_CYCLE_METHOD },
},
slots: { default: 'cycleMethod' },
},
{
field: 'remark',

View File

@ -5,6 +5,7 @@ import type { MesMdAutoCodePartApi } from '#/api/mes/md/autocode/part';
import { ref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { message } from 'ant-design-vue';
@ -13,6 +14,7 @@ import {
deleteAutoCodePart,
getAutoCodePartListByRuleId,
} from '#/api/mes/md/autocode/part';
import { DictTag } from '#/components/dict-tag';
import { $t } from '#/locales';
import { usePartGridColumns } from '../data';
@ -107,6 +109,13 @@ watch(
/>
</div>
<Grid class="w-full" table-title="">
<template #cycleMethod="{ row }">
<DictTag
v-if="row.cycleFlag"
:type="DICT_TYPE.MES_MD_AUTO_CODE_CYCLE_METHOD"
:value="row.cycleMethod"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[

View File

@ -16,6 +16,8 @@ import {
useClientSelectGridFormSchema,
} from '../data';
defineOptions({ name: 'MdClientSelectDialog' });
const emit = defineEmits<{
selected: [rows: MesMdClientApi.Client[]];
}>();
@ -24,43 +26,12 @@ const open = ref(false); // 弹窗是否打开
const multiple = ref(true); //
const selectedRows = ref<MesMdClientApi.Client[]>([]); //
const preSelectedIds = ref<number[]>([]); //
const latestQueryRows = ref<MesMdClientApi.Client[]>([]); //
const queryFinished = ref(false); //
// TODO @
const MAX_TABLE_READY_FRAMES = 60;
/** 等待下一帧 */
function waitNextFrame(): Promise<void> {
return new Promise((resolve) => {
requestAnimationFrame(() => resolve());
});
}
/** 获取当前表格数据 */
function getTableRows() {
return gridApi.grid.getTableData().fullData as MesMdClientApi.Client[];
}
/** 等待 VXE 将当前查询结果写入表格数据后再回显选中 */
async function waitTableReady(): Promise<void> {
if (preSelectedIds.value.length === 0) {
return;
}
for (let index = 0; index < MAX_TABLE_READY_FRAMES; index += 1) {
if (queryFinished.value) {
const rows = getTableRows();
if (latestQueryRows.value.length === 0 && rows.length === 0) {
return;
}
if (latestQueryRows.value.length > 0 && rows.length > 0) {
return;
}
}
await waitNextFrame();
}
}
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesMdClientApi.Client>();
@ -70,7 +41,7 @@ function getMultipleSelectedRows() {
] as MesMdClientApi.Client[];
records.forEach((row) => {
const rowId = row.id;
if (rowId !== null && rowId !== undefined) {
if (rowId != null) {
selectedMap.set(rowId, row);
}
});
@ -110,10 +81,9 @@ async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
// proxy fullData
const rows = getTableRows();
for (const row of rows) {
if (row.id === null || !preSelectedIds.value.includes(row.id as number)) {
if (row.id == null || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
@ -149,15 +119,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
const data = await getClientPage({
return await getClientPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
status: CommonStatusEnum.ENABLE,
});
latestQueryRows.value = data.list || [];
queryFinished.value = true;
return data;
},
},
},
@ -197,8 +164,6 @@ async function openModal(
open.value = true;
multiple.value = options?.multiple ?? true;
preSelectedIds.value = selectedIds || [];
latestQueryRows.value = [];
queryFinished.value = false;
await nextTick();
gridApi.setGridOptions({
columns: useClientSelectGridColumns(multiple.value),
@ -206,13 +171,13 @@ async function openModal(
await resetQueryState();
await gridApi.query();
await nextTick();
await waitTableReady();
await applyPreSelection();
}
/** 关闭客户选择弹窗 */
function closeModal() {
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择客户 */

View File

@ -38,28 +38,23 @@ const selectedItem = ref<MesMdClientApi.Client>(); // 当前选中客户
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
//
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue !== null,
props.modelValue != null,
);
/** 根据客户编号回显选择器 */
async function resolveItemById(id: number | undefined) {
if (id === null) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getClient(id as number);
} catch (error) {
console.error('[MdClientSelect] resolveItemById failed:', error);
}
selectedItem.value = await getClient(id);
}
watch(
@ -88,8 +83,8 @@ function handleClick(event: MouseEvent) {
clearSelected();
return;
}
const selectedIds = props.modelValue === null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds as number[], { multiple: false });
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, { multiple: false });
}
/** 回填选中的客户 */

View File

@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdClientApi } from '#/api/mes/md/client';
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 { Button } from 'ant-design-vue';
@ -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',
@ -33,27 +36,24 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
componentProps: {
placeholder: '请输入客户编码',
},
dependencies: {
triggerFields: ['id'],
componentProps: (values) => ({
disabled: !!values.id,
}),
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_CLIENT_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '自动生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_CLIENT_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '自动生成' },
),
},
{
fieldName: 'name',

View File

@ -72,11 +72,11 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
subTabsName.value = 'productSalesLine';
//
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) {

View File

@ -33,11 +33,31 @@ const [Grid] = useVbenVxeGrid({
width: 140,
slots: { default: 'itemCode' },
},
{ field: 'itemName', title: '物料名称', minWidth: 150 },
{ field: 'specification', title: '规格型号', minWidth: 140 },
{ field: 'unitMeasureName', title: '单位', width: 100 },
{ field: 'quantity', title: '出库数量', width: 120 },
{ field: 'batchCode', title: '批次号', minWidth: 140 },
{
field: 'itemName',
title: '物料名称',
minWidth: 150,
},
{
field: 'specification',
title: '规格型号',
minWidth: 140,
},
{
field: 'unitMeasureName',
title: '单位',
width: 100,
},
{
field: 'quantity',
title: '出库数量',
width: 120,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 140,
},
],
height: 320,
keepSource: true,

View File

@ -2,21 +2,50 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
import { useVbenModal } from '@vben/common-ui';
import { DICT_TYPE } from '@vben/constants';
import { Button } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getProductSalesPage } from '#/api/mes/wm/productsales';
import ProductSalesForm from '#/views/mes/wm/productsales/modules/form.vue';
const props = defineProps<{
clientId: number;
}>();
const [DetailModal, detailModalApi] = useVbenModal({
connectedComponent: ProductSalesForm,
destroyOnClose: true,
});
/** 查看销售出库单详情 */
function handleViewSales(row: MesWmProductSalesApi.ProductSales) {
if (row.id) {
detailModalApi.setData({ formType: 'detail', id: row.id }).open();
}
}
const [Grid] = useVbenVxeGrid({
gridOptions: {
columns: [
{ field: 'code', title: '出库单编号', minWidth: 160 },
{ field: 'name', title: '出库单名称', minWidth: 150 },
{ field: 'salesOrderCode', title: '销售订单编号', minWidth: 140 },
{
field: 'code',
title: '出库单编号',
minWidth: 160,
slots: { default: 'code' },
},
{
field: 'name',
title: '出库单名称',
minWidth: 150,
},
{
field: 'salesOrderCode',
title: '销售订单编号',
minWidth: 140,
},
{
field: 'salesDate',
title: '出库日期',
@ -58,5 +87,12 @@ const [Grid] = useVbenVxeGrid({
</script>
<template>
<Grid table-title="" />
<DetailModal />
<Grid table-title="">
<template #code="{ row }">
<Button type="link" @click="handleViewSales(row)">
{{ row.code }}
</Button>
</template>
</Grid>
</template>

View File

@ -68,7 +68,7 @@ async function openModal(itemId: number, selectedBomItemId?: number) {
list.value = await getProductBomListByItemId(itemId);
gridApi.setGridOptions({ data: list.value });
await nextTick();
if (selectedBomItemId !== null) {
if (selectedBomItemId != null) {
const match = list.value.find(
(row) => row.bomItemId === selectedBomItemId,
);

View File

@ -40,29 +40,24 @@ const selectedBom = ref<MesMdProductBomApi.ProductBom>(); // 当前选中的 BOM
const displayLabel = computed(() => selectedBom.value?.bomItemName ?? ''); //
const showClear = computed(
//
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue !== null,
props.modelValue != null,
);
/** 根据 BOM 子物料编号回显选择器 */
async function resolveBomById(bomItemId: number | undefined) {
if (bomItemId === null || props.itemId === null) {
if (bomItemId == null || props.itemId == null) {
selectedBom.value = undefined;
return;
}
if (selectedBom.value?.bomItemId === bomItemId) {
return;
}
try {
const list = await getProductBomListByItemId(props.itemId as number);
selectedBom.value = list.find((item) => item.bomItemId === bomItemId);
} catch (error) {
console.error('[MdProductBomSelect] resolveBomById failed:', error);
}
const list = await getProductBomListByItemId(props.itemId);
selectedBom.value = list.find((item) => item.bomItemId === bomItemId);
}
watch(
@ -91,7 +86,7 @@ function clearSelected() {
/** 打开 BOM 物料选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled || props.itemId === null) {
if (props.disabled || props.itemId == null) {
return;
}
const target = event.target as HTMLElement;
@ -100,7 +95,7 @@ function handleClick(event: MouseEvent) {
clearSelected();
return;
}
dialogRef.value?.open(props.itemId as number, props.modelValue);
dialogRef.value?.open(props.itemId, props.modelValue);
}
/** 回填选中的 BOM 物料 */

View File

@ -23,52 +23,58 @@ const emit = defineEmits<{
const open = ref(false); //
const multiple = ref(true); //
const syncingSingleSelection = ref(false); //
const selectedRows = ref<MesMdItemApi.Item[]>([]); //
const selectedItemTypeId = ref<number>(); //
const preSelectedIds = ref<number[]>([]); //
const typeTreeRef = ref<InstanceType<typeof MdItemTypeTree>>(); //
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
async function syncSingleSelection(row?: MesMdItemApi.Item) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
/** 获取当前表格数据 */
function getTableRows() {
return gridApi.grid.getTableData().fullData as MesMdItemApi.Item[];
}
/** 处理勾选变化,单选模式只保留最后一条 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesMdItemApi.Item[];
row?: MesMdItemApi.Item;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesMdItemApi.Item>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesMdItemApi.Item[];
records.forEach((row) => {
const rowId = row.id;
if (rowId != null) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理全选变化 */
function handleCheckboxAll({ records }: { records: MesMdItemApi.Item[] }) {
if (syncingSingleSelection.value) {
/** 处理勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理单选变化 */
function handleRadioChange(row: MesMdItemApi.Item) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesMdItemApi.Item) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 处理行双击 */
async function handleCellDblclick({ row }: { row: MesMdItemApi.Item }) {
if (multiple.value) {
await toggleMultipleRow(row);
return;
}
selectedRows.value = records;
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 按分类筛选物料 */
@ -78,18 +84,25 @@ function handleItemTypeNodeClick(row: MesMdItemTypeApi.ItemType | undefined) {
}
/** 回显预选物料 */
function applyPreSelection() {
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesMdItemApi.Item[];
const rows = getTableRows();
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
if (row.id == null || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
@ -98,7 +111,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useItemSelectGridFormSchema(),
},
gridOptions: {
columns: useItemSelectGridColumns(),
columns: useItemSelectGridColumns(true),
height: 560,
keepSource: true,
checkboxConfig: {
@ -106,6 +119,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
@ -129,8 +146,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
} as VxeTableGridOptions<MesMdItemApi.Item>,
gridEvents: {
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesMdItemApi.Item }) => {
handleRadioChange(row);
},
},
});
@ -140,6 +161,8 @@ async function resetQueryState() {
selectedRows.value = [];
typeTreeRef.value?.reset();
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
@ -152,10 +175,13 @@ async function openModal(
multiple.value = options?.multiple ?? true;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useItemSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
await applyPreSelection();
}
/** 关闭物料选择弹窗 */
@ -166,14 +192,12 @@ async function closeModal() {
/** 确认选择物料 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}

View File

@ -38,28 +38,23 @@ const selectedItem = ref<MesMdItemApi.Item>(); // 当前选中物料
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
//
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue !== null,
props.modelValue != null,
);
/** 根据物料编号回显选择器 */
async function resolveItemById(id: number | undefined) {
if (id === null) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getItem(id as number);
} catch (error) {
console.error('[MdItemSelect] resolveItemById failed:', error);
}
selectedItem.value = await getItem(id);
}
watch(
@ -88,8 +83,8 @@ function handleClick(event: MouseEvent) {
clearSelected();
return;
}
const selectedIds = props.modelValue === null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds as number[], { multiple: false });
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, { multiple: false });
}
/** 回填选中的物料 */

View File

@ -3,9 +3,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdItemApi } from '#/api/mes/md/item';
import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom';
import { DICT_TYPE, h, markRaw } from 'vue';
import { h, markRaw } from 'vue';
import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@ -19,7 +19,10 @@ import { MdUnitMeasureSelect } from '#/views/mes/md/unitmeasure/components';
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改物料产品的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
@ -38,20 +41,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.MD_ITEM_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_ITEM_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
@ -356,9 +362,11 @@ export function useItemSelectGridFormSchema(): VbenFormSchema[] {
}
/** 物料选择弹窗列表字段 */
export function useItemSelectGridColumns(): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
export function useItemSelectGridColumns(
multiple = true,
): VxeTableGridOptions<MesMdItemApi.Item>['columns'] {
return [
{ type: 'checkbox', width: 50 },
{ type: multiple ? 'checkbox' : 'radio', width: 50 },
{
field: 'code',
title: '物料编码',

View File

@ -100,11 +100,11 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
subTabsName.value = 'bom';
//
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) {

View File

@ -68,38 +68,54 @@ const [Form, formApi] = useVbenForm({
fieldName: 'bomItemCode',
label: 'BOM 物料编码',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
},
{
fieldName: 'bomItemName',
label: 'BOM 物料名称',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
},
{
fieldName: 'bomItemSpecification',
label: '规格型号',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
},
{
fieldName: 'unitMeasureName',
label: '单位',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
},
{
fieldName: 'quantity',
label: '用量比例',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 4, step: 0.1 },
componentProps: {
class: '!w-full',
min: 0,
precision: 4,
step: 0.1,
},
rules: z.number().default(1),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: { placeholder: '请输入备注', rows: 3 },
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
],
showDefaultActions: false,

View File

@ -79,21 +79,30 @@ const [Form, formApi] = useVbenForm({
fieldName: 'title',
label: '标题',
component: 'Input',
componentProps: { placeholder: '请输入标题' },
componentProps: {
placeholder: '请输入标题',
},
rules: 'required',
},
{
fieldName: 'sort',
label: '展示顺序',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 0 },
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
rules: z.number().default(0),
},
{
fieldName: 'description',
label: '内容说明',
component: 'Textarea',
componentProps: { placeholder: '请输入详细描述', rows: 3 },
componentProps: {
placeholder: '请输入详细描述',
rows: 3,
},
},
{
fieldName: 'processId',
@ -104,13 +113,19 @@ const [Form, formApi] = useVbenForm({
fieldName: 'url',
label: '图片',
component: markRaw(ImageUpload),
componentProps: { maxNumber: 1, showDescription: false },
componentProps: {
maxNumber: 1,
showDescription: false,
},
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: { placeholder: '请输入备注', rows: 3 },
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
],
showDefaultActions: false,

View File

@ -2,9 +2,14 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdItemTypeApi } from '#/api/mes/md/item/type';
import { DICT_TYPE, h } from 'vue';
import { h } from 'vue';
import { CommonStatusEnum, MesAutoCodeRuleCode, MesItemOrProductEnum } from '@vben/constants';
import {
CommonStatusEnum,
DICT_TYPE,
MesAutoCodeRuleCode,
MesItemOrProductEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { handleTree } from '@vben/utils';
@ -14,8 +19,14 @@ import { z } from '#/adapter/form';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getItemTypeList } from '#/api/mes/md/item/type';
/** 表单类型 */
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改物料分类的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
@ -59,20 +70,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.MD_ITEM_TYPE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '自动生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_ITEM_TYPE_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '自动生成' },
),
},
{
fieldName: 'name',

View File

@ -62,9 +62,11 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
//
const data = modalApi.getData<MesMdItemTypeApi.ItemType>();
formApi.setState({
schema: useFormSchema(data?.id ? 'update' : 'create', formApi),
});
if (!data || !data.id) {
formData.value = data || undefined;
if (data) {

View File

@ -16,71 +16,86 @@ import {
useVendorSelectGridFormSchema,
} from '../data';
defineOptions({ name: 'MdVendorSelectDialog' });
const emit = defineEmits<{
selected: [rows: MesMdVendorApi.Vendor[]];
}>();
const open = ref(false); //
const multiple = ref(true); //
const syncingSingleSelection = ref(false); //
const selectedRows = ref<MesMdVendorApi.Vendor[]>([]); //
const preSelectedIds = ref<number[]>([]); //
/** 单选模式下同步 VXE 勾选状态,避免跨页残留多选 */
async function syncSingleSelection(row?: MesMdVendorApi.Vendor) {
syncingSingleSelection.value = true;
await nextTick();
await gridApi.grid.clearCheckboxRow();
if (row) {
await gridApi.grid.setCheckboxRow(row, true);
}
await nextTick();
syncingSingleSelection.value = false;
/** 获取当前表格数据 */
function getTableRows() {
return gridApi.grid.getTableData().fullData as MesMdVendorApi.Vendor[];
}
/** 处理勾选变化,单选模式只保留最后一条 */
async function handleCheckboxChange({
checked,
records,
row,
}: {
checked: boolean;
records: MesMdVendorApi.Vendor[];
row?: MesMdVendorApi.Vendor;
}) {
if (syncingSingleSelection.value) {
return;
}
if (!multiple.value) {
const selected = checked && row ? [row] : [];
selectedRows.value = selected;
await syncSingleSelection(selected[0]);
return;
}
selectedRows.value = records;
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesMdVendorApi.Vendor>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesMdVendorApi.Vendor[];
records.forEach((row) => {
const rowId = row.id;
if (rowId != null) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理全选变化 */
function handleCheckboxAll({ records }: { records: MesMdVendorApi.Vendor[] }) {
if (syncingSingleSelection.value) {
/** 处理勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理单选变化 */
function handleRadioChange(row: MesMdVendorApi.Vendor) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesMdVendorApi.Vendor) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 处理行双击 */
async function handleCellDblclick({ row }: { row: MesMdVendorApi.Vendor }) {
if (multiple.value) {
await toggleMultipleRow(row);
return;
}
selectedRows.value = records;
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 回显预选供应商 */
function applyPreSelection() {
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = gridApi.grid.getData() as MesMdVendorApi.Vendor[];
const rows = getTableRows();
for (const row of rows) {
if (row.id && preSelectedIds.value.includes(row.id)) {
gridApi.grid.setCheckboxRow(row, true);
if (!multiple.value) {
selectedRows.value = [row];
}
if (row.id == null || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
@ -89,7 +104,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useVendorSelectGridFormSchema(),
},
gridOptions: {
columns: useVendorSelectGridColumns(),
columns: useVendorSelectGridColumns(true),
height: 520,
keepSource: true,
checkboxConfig: {
@ -97,6 +112,10 @@ const [Grid, gridApi] = useVbenVxeGrid({
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
@ -119,8 +138,12 @@ const [Grid, gridApi] = useVbenVxeGrid({
},
} as VxeTableGridOptions<MesMdVendorApi.Vendor>,
gridEvents: {
checkboxAll: handleCheckboxAll,
checkboxChange: handleCheckboxChange,
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesMdVendorApi.Vendor }) => {
handleRadioChange(row);
},
},
});
@ -128,6 +151,8 @@ const [Grid, gridApi] = useVbenVxeGrid({
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
@ -140,10 +165,13 @@ async function openModal(
multiple.value = options?.multiple ?? true;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useVendorSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
applyPreSelection();
await applyPreSelection();
}
/** 关闭供应商选择弹窗 */
@ -154,14 +182,12 @@ async function closeModal() {
/** 确认选择供应商 */
function handleConfirm() {
if (selectedRows.value.length === 0) {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit(
'selected',
multiple.value ? selectedRows.value : [selectedRows.value[0]!],
);
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}

View File

@ -38,28 +38,23 @@ const selectedItem = ref<MesMdVendorApi.Vendor>(); // 当前选中供应商
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
//
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue !== null,
props.modelValue != null,
);
/** 根据供应商编号回显选择器 */
async function resolveItemById(id: number | undefined) {
if (id === null) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getVendor(id as number);
} catch (error) {
console.error('[MdVendorSelect] resolveItemById failed:', error);
}
selectedItem.value = await getVendor(id);
}
watch(
@ -88,8 +83,8 @@ function handleClick(event: MouseEvent) {
clearSelected();
return;
}
const selectedIds = props.modelValue === null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds as number[], { multiple: false });
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, { multiple: false });
}
/** 回填选中的供应商 */

View File

@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdVendorApi } from '#/api/mes/md/vendor';
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 { Button } from 'ant-design-vue';
@ -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',
@ -33,27 +36,24 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
componentProps: {
placeholder: '请输入供应商编码',
},
dependencies: {
triggerFields: ['id'],
componentProps: (values) => ({
disabled: !!values.id,
}),
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_VENDOR_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '自动生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_VENDOR_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '自动生成' },
),
},
{
fieldName: 'name',
@ -416,9 +416,11 @@ export function useVendorSelectGridFormSchema(): VbenFormSchema[] {
}
/** 供应商选择弹窗的字段 */
export function useVendorSelectGridColumns(): VxeTableGridOptions<MesMdVendorApi.Vendor>['columns'] {
export function useVendorSelectGridColumns(
multiple = true,
): VxeTableGridOptions<MesMdVendorApi.Vendor>['columns'] {
return [
{ type: 'checkbox', width: 50 },
{ type: multiple ? 'checkbox' : 'radio', width: 50 },
{
field: 'code',
title: '供应商编码',

View File

@ -74,11 +74,11 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
subTabsName.value = 'itemReceiptLine';
//
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) {

View File

@ -31,12 +31,36 @@ function formatDate(value: Date | number | string | undefined) {
const [Grid, gridApi] = useVbenVxeGrid({
gridOptions: {
columns: [
{ field: 'itemCode', title: '物料编码', width: 140 },
{ field: 'itemName', title: '物料名称', minWidth: 150 },
{ field: 'specification', title: '规格型号', minWidth: 140 },
{ field: 'unitMeasureName', title: '单位', width: 100 },
{ field: 'receivedQuantity', title: '入库数量', width: 120 },
{ field: 'batchCode', title: '批次号', minWidth: 140 },
{
field: 'itemCode',
title: '物料编码',
width: 140,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 150,
},
{
field: 'specification',
title: '规格型号',
minWidth: 140,
},
{
field: 'unitMeasureName',
title: '单位',
width: 100,
},
{
field: 'receivedQuantity',
title: '入库数量',
width: 120,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 140,
},
],
height: 280,
keepSource: true,

View File

@ -33,11 +33,31 @@ const [Grid] = useVbenVxeGrid({
width: 140,
slots: { default: 'itemCode' },
},
{ field: 'itemName', title: '物料名称', minWidth: 150 },
{ field: 'specification', title: '规格型号', minWidth: 140 },
{ field: 'unitMeasureName', title: '单位', width: 100 },
{ field: 'receivedQuantity', title: '入库数量', width: 120 },
{ field: 'batchCode', title: '批次号', minWidth: 140 },
{
field: 'itemName',
title: '物料名称',
minWidth: 150,
},
{
field: 'specification',
title: '规格型号',
minWidth: 140,
},
{
field: 'unitMeasureName',
title: '单位',
width: 100,
},
{
field: 'receivedQuantity',
title: '入库数量',
width: 120,
},
{
field: 'batchCode',
title: '批次号',
minWidth: 140,
},
],
height: 320,
keepSource: true,

View File

@ -36,12 +36,36 @@ const [Grid] = useVbenVxeGrid({
minWidth: 160,
slots: { default: 'receiptCode' },
},
{ field: 'purchaseOrderCode', title: '采购订单号', minWidth: 150 },
{ field: 'itemCode', title: '物料编码', width: 140 },
{ field: 'itemName', title: '物料名称', minWidth: 150 },
{ field: 'specification', title: '规格型号', minWidth: 140 },
{ field: 'unitMeasureName', title: '单位', width: 100 },
{ field: 'receivedQuantity', title: '入库数量', width: 120 },
{
field: 'purchaseOrderCode',
title: '采购订单号',
minWidth: 150,
},
{
field: 'itemCode',
title: '物料编码',
width: 140,
},
{
field: 'itemName',
title: '物料名称',
minWidth: 150,
},
{
field: 'specification',
title: '规格型号',
minWidth: 140,
},
{
field: 'unitMeasureName',
title: '单位',
width: 100,
},
{
field: 'receivedQuantity',
title: '入库数量',
width: 120,
},
],
height: 320,
keepSource: true,

View File

@ -40,12 +40,11 @@ const selectedItem = ref<MesMdWorkstationApi.Workstation>(); // 选中的工作
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
//
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue !== null,
props.modelValue != null,
);
/** 根据工作站编号回显选择器 */
@ -57,11 +56,7 @@ async function resolveItemById(id: number | undefined) {
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getWorkstation(id);
} catch (error) {
console.error('[MdWorkstationSelect] resolveItemById failed:', error);
}
selectedItem.value = await getWorkstation(id);
}
watch(

View File

@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdWorkstationApi } from '#/api/mes/md/workstation';
import { DICT_TYPE, h, markRaw } from 'vue';
import { h, markRaw } from 'vue';
import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@ -22,7 +22,10 @@ import { MdWorkshopSelect } from './components';
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改工作站的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
@ -39,27 +42,24 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
componentProps: {
placeholder: '请输入工作站编码',
},
dependencies: {
triggerFields: ['id'],
componentProps: (values) => ({
disabled: !!values.id,
}),
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_WORKSTATION_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_WORKSTATION_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',

View File

@ -99,11 +99,11 @@ const [Modal, modalApi] = useVbenModal({
formData.value = undefined;
return;
}
formApi.setState({ schema: useFormSchema(formApi) });
subTabsName.value = 'machine';
//
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) {

View File

@ -78,7 +78,10 @@ const [Form, formApi] = useVbenForm({
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: { placeholder: '请输入备注', rows: 3 },
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
],
showDefaultActions: false,
@ -89,10 +92,26 @@ const [Grid, gridApi] = useVbenVxeGrid({
autoResize: true,
border: true,
columns: [
{ field: 'machineryCode', title: '设备编码', width: 140 },
{ field: 'machineryName', title: '设备名称', minWidth: 160 },
{ field: 'quantity', title: '数量', width: 100 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
field: 'machineryCode',
title: '设备编码',
width: 140,
},
{
field: 'machineryName',
title: '设备名称',
minWidth: 160,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
{
field: 'remark',
title: '备注',
minWidth: 160,
},
{
title: '操作',
width: 90,

View File

@ -89,7 +89,10 @@ const [Form, formApi] = useVbenForm({
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: { placeholder: '请输入备注', rows: 3 },
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
],
showDefaultActions: false,
@ -100,10 +103,26 @@ const [Grid, gridApi] = useVbenVxeGrid({
autoResize: true,
border: true,
columns: [
{ field: 'toolTypeId', title: '工具类型编号', width: 140 },
{ field: 'toolTypeName', title: '工具类型名称', minWidth: 160 },
{ field: 'quantity', title: '数量', width: 100 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
field: 'toolTypeId',
title: '工具类型编号',
width: 140,
},
{
field: 'toolTypeName',
title: '工具类型名称',
minWidth: 160,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
{
field: 'remark',
title: '备注',
minWidth: 160,
},
{
title: '操作',
width: 130,

View File

@ -85,7 +85,10 @@ const [Form, formApi] = useVbenForm({
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: { placeholder: '请输入备注', rows: 3 },
componentProps: {
placeholder: '请输入备注',
rows: 3,
},
},
],
showDefaultActions: false,
@ -96,10 +99,26 @@ const [Grid, gridApi] = useVbenVxeGrid({
autoResize: true,
border: true,
columns: [
{ field: 'postId', title: '岗位编号', width: 140 },
{ field: 'postName', title: '岗位名称', minWidth: 160 },
{ field: 'quantity', title: '数量', width: 100 },
{ field: 'remark', title: '备注', minWidth: 160 },
{
field: 'postId',
title: '岗位编号',
width: 140,
},
{
field: 'postName',
title: '岗位名称',
minWidth: 160,
},
{
field: 'quantity',
title: '数量',
width: 100,
},
{
field: 'remark',
title: '备注',
minWidth: 160,
},
{
title: '操作',
width: 130,

View File

@ -2,9 +2,9 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdWorkshopApi } from '#/api/mes/md/workstation/workshop';
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 { Button } from 'ant-design-vue';
@ -17,7 +17,10 @@ import { getSimpleUserList } from '#/api/system/user';
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改车间的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
@ -41,20 +44,23 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
}),
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_WORKSHOP_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(
MesAutoCodeRuleCode.MD_WORKSHOP_CODE,
);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',

View File

@ -89,10 +89,10 @@ 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.setState({ schema: useFormSchema(data.formType, formApi) });
formApi.setDisabled(formType.value === 'detail');
modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
if (!data?.id) {

View File

@ -41,22 +41,16 @@ const canSubmit = computed(() => // 编辑态草稿可提交
formType.value === 'update' &&
formData.value?.status === MesProCardStatusEnum.PREPARE,
);
// TODO @AI
const getTitle = computed(() => {
switch (formType.value) {
case 'detail': {
return $t('ui.actionTitle.view', ['流转卡']);
}
case 'finish': {
return '完成流转卡';
}
case 'update': {
return $t('ui.actionTitle.edit', ['流转卡']);
}
default: {
return $t('ui.actionTitle.create', ['流转卡']);
}
if (formType.value === 'detail') {
return $t('ui.actionTitle.view', ['流转卡']);
}
if (formType.value === 'finish') {
return '完成流转卡';
}
return formType.value === 'update'
? $t('ui.actionTitle.edit', ['流转卡'])
: $t('ui.actionTitle.create', ['流转卡']);
});
const [Form, formApi] = useVbenForm({
@ -125,7 +119,6 @@ async function handleFinish() {
}
const [Modal, modalApi] = useVbenModal({
// TODO @AI onConfirm //
async onConfirm() {
if (!isEditable.value) {
await modalApi.close();

View File

@ -327,7 +327,9 @@ export function useFormSchema(
fieldName: 'itemCode',
label: '产品编码',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
@ -337,7 +339,9 @@ export function useFormSchema(
fieldName: 'itemName',
label: '产品名称',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
@ -347,7 +351,9 @@ export function useFormSchema(
fieldName: 'unitMeasureName',
label: '单位',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
@ -357,7 +363,9 @@ export function useFormSchema(
fieldName: 'itemSpecification',
label: '规格',
component: 'Input',
componentProps: { disabled: true },
componentProps: {
disabled: true,
},
dependencies: {
triggerFields: ['itemCode'],
show: (values) => !!values.itemCode,
@ -367,7 +375,11 @@ export function useFormSchema(
fieldName: 'feedbackQuantity',
label: '报工数量',
component: 'InputNumber',
componentProps: { class: 'w-full', min: 0, precision: 2 },
componentProps: {
class: 'w-full',
min: 0,
precision: 2,
},
dependencies: {
triggerFields: ['checkFlag'],
// 非质检工序时,报工数量 = 合格 + 不良,禁用直接编辑

View File

@ -3,9 +3,9 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesProProcessApi } from '#/api/mes/pro/process';
import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
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 { Button } from 'ant-design-vue';

View File

@ -5,9 +5,9 @@ import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
import { DICT_TYPE, h } from 'vue';
import { h, markRaw } from 'vue';
import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
@ -98,13 +98,19 @@ export function useGridFormSchema(): VbenFormSchema[] {
fieldName: 'code',
label: '路线编码',
component: 'Input',
componentProps: { allowClear: true, placeholder: '请输入路线编码' },
componentProps: {
allowClear: true,
placeholder: '请输入路线编码',
},
},
{
fieldName: 'name',
label: '路线名称',
component: 'Input',
componentProps: { allowClear: true, placeholder: '请输入路线名称' },
componentProps: {
allowClear: true,
placeholder: '请输入路线名称',
},
},
{
fieldName: 'status',
@ -186,7 +192,11 @@ export function useRouteProcessFormSchema(
fieldName: 'sort',
label: '序号',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 1, precision: 0 },
componentProps: {
class: '!w-full',
min: 1,
precision: 0,
},
rules: z.number().default(1),
},
{
@ -221,28 +231,42 @@ export function useRouteProcessFormSchema(
fieldName: 'keyFlag',
label: '是否关键工序',
component: 'Switch',
componentProps: { checkedChildren: '是', unCheckedChildren: '否' },
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
rules: z.boolean().default(false),
},
{
fieldName: 'checkFlag',
label: '是否质检确认',
component: 'Switch',
componentProps: { checkedChildren: '是', unCheckedChildren: '否' },
componentProps: {
checkedChildren: '是',
unCheckedChildren: '否',
},
rules: z.boolean().default(false),
},
{
fieldName: 'prepareTime',
label: '准备时间(分)',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 0 },
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
rules: z.number().default(0),
},
{
fieldName: 'waitTime',
label: '等待时间(分)',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 0 },
componentProps: {
class: '!w-full',
min: 0,
precision: 0,
},
rules: z.number().default(0),
},
{
@ -250,7 +274,11 @@ export function useRouteProcessFormSchema(
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: { maxLength: 250, placeholder: '请输入备注', rows: 2 },
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
@ -369,7 +397,7 @@ export function useRouteProductFormSchema(
{
fieldName: 'itemId',
label: '产品',
component: MdItemSelect as any,
component: markRaw(MdItemSelect),
componentProps: {
onChange: onItemChange,
},
@ -380,14 +408,22 @@ export function useRouteProductFormSchema(
fieldName: 'quantity',
label: '生产数量',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 1, precision: 0 },
componentProps: {
class: '!w-full',
min: 1,
precision: 0,
},
rules: z.number().default(1),
},
{
fieldName: 'productionTime',
label: '生产用时',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 2 },
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
},
rules: z.number().default(1),
},
{
@ -406,7 +442,11 @@ export function useRouteProductFormSchema(
label: '备注',
component: 'Textarea',
formItemClass: 'col-span-2',
componentProps: { maxLength: 250, placeholder: '请输入备注', rows: 2 },
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}
@ -441,7 +481,7 @@ export function useRouteProductBomFormSchema(
{
fieldName: 'itemId',
label: 'BOM 物料',
component: MdProductBomSelect as any,
component: markRaw(MdProductBomSelect),
componentProps: () => ({
itemId: itemId(),
onChange: onBomChange,
@ -453,14 +493,22 @@ export function useRouteProductBomFormSchema(
fieldName: 'quantity',
label: '用料比例',
component: 'InputNumber',
componentProps: { class: '!w-full', min: 0, precision: 2 },
componentProps: {
class: '!w-full',
min: 0,
precision: 2,
},
rules: z.number().default(1),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: { maxLength: 250, placeholder: '请输入备注', rows: 2 },
componentProps: {
maxLength: 250,
placeholder: '请输入备注',
rows: 2,
},
},
];
}

View File

@ -1 +0,0 @@
export { default as WorkRecordStatusBar } from './status-bar.vue';

View File

@ -12,8 +12,8 @@ import {
} from '#/api/mes/pro/workrecord';
import { $t } from '#/locales';
import { WorkRecordStatusBar } from './components';
import { useGridColumns, useGridFormSchema } from './data';
import WorkRecordStatusBar from './modules/status-bar.vue';
/** 刷新表格 */
function handleRefresh() {

View File

@ -1,2 +1,3 @@
export { default as QcIndicatorResultSpecificationInput } from './result-specification-input.vue';
export { default as QcIndicatorSelectDialog } from './select-dialog.vue';
export { default as QcIndicatorSelect } from './select.vue';

View File

@ -0,0 +1,76 @@
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { MesQcResultValueType } from '@vben/constants';
import { RadioGroup, Select } from 'ant-design-vue';
import { getSimpleDictTypeList } from '#/api/system/dict/type';
defineOptions({ name: 'QcIndicatorResultSpecificationInput' });
const props = withDefaults(
defineProps<{
modelValue?: string;
resultType?: number;
}>(),
{
modelValue: undefined,
resultType: undefined,
},
);
const emit = defineEmits<{
'update:modelValue': [value?: string];
}>();
const fileOptions = [
{ label: '图片/照片', value: 'IMG' },
{ label: '文件', value: 'FILE' },
];
const dictTypeOptions = ref<{ name?: string; type?: string }[]>([]);
const innerValue = computed({
get: () => props.modelValue,
set: (value?: string) => emit('update:modelValue', value),
});
/** 加载字典类型选项(仅字典类型结果值需要) */
async function loadDictTypeOptions() {
if (dictTypeOptions.value.length > 0) {
return;
}
dictTypeOptions.value = await getSimpleDictTypeList();
}
watch(
() => props.resultType,
(value) => {
if (value === MesQcResultValueType.DICT) {
void loadDictTypeOptions();
}
},
{ immediate: true },
);
</script>
<template>
<RadioGroup
v-if="resultType === MesQcResultValueType.FILE"
v-model:value="innerValue"
:options="fileOptions"
/>
<Select
v-else-if="resultType === MesQcResultValueType.DICT"
v-model:value="innerValue"
:field-names="{ label: 'name', value: 'type' }"
:filter-option="
(input: string, option: any) =>
(option.name as string).toLowerCase().includes(input.toLowerCase())
"
:options="dictTypeOptions"
allow-clear
class="w-full"
placeholder="请选择字典类型"
show-search
/>
</template>

View File

@ -55,11 +55,7 @@ async function resolveItemById(id: number | undefined) {
if (selectedItem.value?.id === id) {
return;
}
try {
selectedItem.value = await getIndicator(id);
} catch (error) {
console.error('[QcIndicatorSelect] resolveItemById failed:', error);
}
selectedItem.value = await getIndicator(id);
}
watch(() => props.modelValue, resolveItemById, { immediate: true });

View File

@ -2,7 +2,7 @@ import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesQcIndicatorApi } from '#/api/mes/qc/indicator';
import { h } from 'vue';
import { h, markRaw } from 'vue';
import { DICT_TYPE, MesAutoCodeRuleCode, MesQcResultValueType } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
@ -10,7 +10,8 @@ import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { getSimpleDictTypeList } from '#/api/system/dict/type';
import { QcIndicatorResultSpecificationInput } from './components';
/** 新增/修改的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
@ -93,37 +94,17 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
},
{
fieldName: 'resultSpecification',
label: '文件类型',
component: 'RadioGroup',
componentProps: {
options: [
{ label: '图片/照片', value: 'IMG' },
{ label: '文件', value: 'FILE' },
],
},
label: '结果值属性',
component: markRaw(QcIndicatorResultSpecificationInput),
// 按结果值类型在组件内部切换文件类型 RadioGroup / 字典类型 ApiSelect
dependencies: {
triggerFields: ['resultType'],
show: (values) => values.resultType === MesQcResultValueType.FILE,
},
rules: 'required',
},
{
fieldName: 'resultSpecification',
label: '字典类型',
component: 'ApiSelect',
componentProps: {
allowClear: true,
api: getSimpleDictTypeList,
filterOption: (input: string, option: any) =>
(option.label as string).toLowerCase().includes(input.toLowerCase()),
labelField: 'name',
placeholder: '请选择字典类型',
showSearch: true,
valueField: 'type',
},
dependencies: {
triggerFields: ['resultType'],
show: (values) => values.resultType === MesQcResultValueType.DICT,
if: (values) =>
values.resultType === MesQcResultValueType.FILE ||
values.resultType === MesQcResultValueType.DICT,
componentProps: (values) => ({
resultType: values.resultType,
}),
},
rules: 'required',
},

View File

@ -73,9 +73,21 @@ export function useQcIndicatorResultFormSchema(
/** 检测结果列表的字段 */
export function useQcIndicatorResultGridColumns(): VxeTableGridOptions<MesQcIndicatorResultApi.IndicatorResult>['columns'] {
return [
{ field: 'code', title: '样品编号', width: 200 },
{ field: 'sn', title: '物资SN', minWidth: 200 },
{ field: 'remark', title: '备注', minWidth: 200 },
{
field: 'code',
title: '样品编号',
width: 200,
},
{
field: 'sn',
title: '物资SN',
minWidth: 200,
},
{
field: 'remark',
title: '备注',
minWidth: 200,
},
{
title: '操作',
width: 150,

View File

@ -257,6 +257,7 @@ export function useFormSchema(
precision: 2,
onChange: () => syncUnqualified(formApi),
},
defaultValue: 0,
rules: 'required',
dependencies: {
triggerFields: ['unqualifiedQuantity'],
@ -275,6 +276,7 @@ export function useFormSchema(
precision: 2,
onChange: () => syncUnqualified(formApi),
},
defaultValue: 0,
rules: 'required',
dependencies: {
triggerFields: ['unqualifiedQuantity'],
@ -293,6 +295,7 @@ export function useFormSchema(
precision: 2,
onChange: () => syncUnqualified(formApi),
},
defaultValue: 0,
rules: 'required',
dependencies: {
triggerFields: ['unqualifiedQuantity'],

View File

@ -171,10 +171,15 @@ const [Modal, modalApi] = useVbenModal({
modalApi.unlock();
}
} else if (data?.prefill) {
//
formData.value = { ...data.prefill };
// watcher outQuantity
// checkQuantity
const prefill = {
...data.prefill,
checkQuantity: data.prefill.checkQuantity ?? data.prefill.outQuantity,
};
formData.value = { ...prefill };
// values
await formApi.setValues(data.prefill);
await formApi.setValues(prefill);
}
originalSnapshot.value = JSON.stringify(await formApi.getValues());
},

View File

@ -136,9 +136,7 @@ const [Modal, modalApi] = useVbenModal({
if (!ok) {
return;
}
//
message.success($t('ui.actionMessage.operationSuccess'));
await modalApi.close();
emit('success');
} finally {
modalApi.unlock();
@ -160,7 +158,10 @@ const [Modal, modalApi] = useVbenModal({
formType.value = data.formType;
formApi.setState({ schema: useFormSchema(formType.value, formApi) });
formApi.setDisabled(formType.value === 'detail');
modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
modalApi.setState({
confirmText: formType.value === 'detail' ? undefined : '保存',
showConfirmButton: formType.value !== 'detail',
});
if (data?.id) {
modalApi.lock();
try {

View File

@ -4,9 +4,9 @@ import type { MesQcTemplateApi } from '#/api/mes/qc/template';
import type { MesQcTemplateIndicatorApi } from '#/api/mes/qc/template/indicator';
import type { MesQcTemplateItemApi } from '#/api/mes/qc/template/item';
import { DICT_TYPE, h, markRaw } from 'vue';
import { h, markRaw } from 'vue';
import { CommonStatusEnum, MesAutoCodeRuleCode } from '@vben/constants';
import { CommonStatusEnum, DICT_TYPE, MesAutoCodeRuleCode } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { Button } from 'ant-design-vue';

View File

@ -1 +1,2 @@
export { default as TmToolSelectDialog } from './select-dialog.vue';
export { default as TmToolSelect } from './select.vue';

View File

@ -0,0 +1,206 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesTmToolApi } from '#/api/mes/tm/tool';
import { nextTick, ref } from 'vue';
import { Button, message, Modal } from 'ant-design-vue';
import { useVbenVxeGrid } from '#/adapter/vxe-table';
import { getToolPage } from '#/api/mes/tm/tool';
import { useToolSelectGridColumns, useToolSelectGridFormSchema } from '../data';
defineOptions({ name: 'TmToolSelectDialog' });
const emit = defineEmits<{
selected: [rows: MesTmToolApi.Tool[]];
}>();
const open = ref(false); //
const multiple = ref(false); //
const selectedRows = ref<MesTmToolApi.Tool[]>([]); //
const preSelectedIds = ref<number[]>([]); //
/** 获取当前表格数据 */
function getTableRows() {
return gridApi.grid.getTableData().fullData as MesTmToolApi.Tool[];
}
/** 获取多选记录,包含 VXE reserve 跨页记录 */
function getMultipleSelectedRows() {
const selectedMap = new Map<number, MesTmToolApi.Tool>();
const records = [
...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
...(gridApi.grid.getCheckboxRecords?.() ?? []),
] as MesTmToolApi.Tool[];
records.forEach((row) => {
const rowId = row.id;
if (rowId != null) {
selectedMap.set(rowId, row);
}
});
return [...selectedMap.values()];
}
/** 处理勾选变化 */
function handleCheckboxSelectChange() {
selectedRows.value = getMultipleSelectedRows();
}
/** 处理单选变化 */
function handleRadioChange(row: MesTmToolApi.Tool) {
selectedRows.value = [row];
}
/** 多选模式下切换行勾选 */
async function toggleMultipleRow(row: MesTmToolApi.Tool) {
const selected = gridApi.grid.isCheckedByCheckboxRow(row);
await gridApi.grid.setCheckboxRow(row, !selected);
selectedRows.value = getMultipleSelectedRows();
}
/** 处理行双击 */
async function handleCellDblclick({ row }: { row: MesTmToolApi.Tool }) {
if (multiple.value) {
await toggleMultipleRow(row);
return;
}
selectedRows.value = [row];
await gridApi.grid.setRadioRow(row);
handleConfirm();
}
/** 回显预选工具 */
async function applyPreSelection() {
if (preSelectedIds.value.length === 0) {
return;
}
const rows = getTableRows();
for (const row of rows) {
if (row.id == null || !preSelectedIds.value.includes(row.id)) {
continue;
}
if (multiple.value) {
await gridApi.grid.setCheckboxRow(row, true);
} else {
await gridApi.grid.setRadioRow(row);
selectedRows.value = [row];
return;
}
}
if (multiple.value) {
selectedRows.value = getMultipleSelectedRows();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: {
schema: useToolSelectGridFormSchema(),
},
gridOptions: {
columns: useToolSelectGridColumns(false),
height: 520,
keepSource: true,
checkboxConfig: {
highlight: true,
range: true,
reserve: true,
},
radioConfig: {
highlight: true,
trigger: 'row',
},
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
return await getToolPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
});
},
},
},
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions<MesTmToolApi.Tool>,
gridEvents: {
cellDblclick: handleCellDblclick,
checkboxAll: handleCheckboxSelectChange,
checkboxChange: handleCheckboxSelectChange,
radioChange: ({ row }: { row: MesTmToolApi.Tool }) => {
handleRadioChange(row);
},
},
});
/** 重置查询和选择状态 */
async function resetQueryState() {
selectedRows.value = [];
await gridApi.grid.clearCheckboxRow();
await gridApi.grid.clearCheckboxReserve();
await gridApi.grid.clearRadioRow();
await gridApi.formApi.resetForm();
}
/** 打开工具选择弹窗 */
async function openModal(
selectedIds?: number[],
options?: { multiple?: boolean },
) {
open.value = true;
multiple.value = options?.multiple ?? false;
preSelectedIds.value = selectedIds || [];
await nextTick();
gridApi.setGridOptions({
columns: useToolSelectGridColumns(multiple.value),
});
await resetQueryState();
await gridApi.query();
await nextTick();
await applyPreSelection();
}
/** 关闭工具选择弹窗 */
async function closeModal() {
open.value = false;
await resetQueryState();
}
/** 确认选择工具 */
function handleConfirm() {
const rows = multiple.value ? getMultipleSelectedRows() : selectedRows.value;
if (rows.length === 0) {
message.warning(multiple.value ? '请至少选择一条数据' : '请选择一条数据');
return;
}
emit('selected', multiple.value ? rows : [rows[0]!]);
open.value = false;
}
defineExpose({ open: openModal });
</script>
<template>
<Modal
v-model:open="open"
title="工具选择"
width="75%"
:destroy-on-close="true"
@ok="handleConfirm"
@cancel="closeModal"
>
<Grid table-title="" />
<template #footer>
<Button @click="closeModal"> </Button>
<Button type="primary" @click="handleConfirm"> </Button>
</template>
</Modal>
</template>

View File

@ -1,55 +1,141 @@
<script lang="ts" setup>
import type { SelectValue } from 'ant-design-vue/es/select';
import type { MesTmToolApi } from '#/api/mes/tm/tool';
import { onMounted, ref } from 'vue';
import { computed, ref, useAttrs, watch } from 'vue';
import { Select } from 'ant-design-vue';
import { IconifyIcon } from '@vben/icons';
import { getToolSimpleList } from '#/api/mes/tm/tool';
import { Input, Tooltip } from 'ant-design-vue';
withDefaults(
import { getTool } from '#/api/mes/tm/tool';
import TmToolSelectDialog from './select-dialog.vue';
defineOptions({ name: 'TmToolSelect', inheritAttrs: false });
const props = withDefaults(
defineProps<{
allowClear?: boolean;
disabled?: boolean;
modelValue?: number;
placeholder?: string;
}>(),
{ allowClear: true, disabled: false, modelValue: undefined, placeholder: '请选择工具' },
{
allowClear: true,
disabled: false,
modelValue: undefined,
placeholder: '请选择工具',
},
);
const emit = defineEmits<{
change: [row?: MesTmToolApi.Tool];
'update:modelValue': [value?: number];
change: [item: MesTmToolApi.Tool | undefined];
'update:modelValue': [value: number | undefined];
}>();
const list = ref<MesTmToolApi.Tool[]>([]); //
const attrs = useAttrs(); //
const dialogRef = ref<InstanceType<typeof TmToolSelectDialog>>(); //
const hovering = ref(false); //
const selectedItem = ref<MesTmToolApi.Tool>(); //
/** 加载工具列表 */
async function getList() {
list.value = await getToolSimpleList();
const displayLabel = computed(() => selectedItem.value?.name ?? ''); //
const showClear = computed(
() =>
props.allowClear &&
!props.disabled &&
hovering.value &&
props.modelValue != null,
);
/** 根据工具编号回显选择器 */
async function resolveItemById(id: number | undefined) {
if (id == null) {
selectedItem.value = undefined;
return;
}
if (selectedItem.value?.id === id) {
return;
}
selectedItem.value = await getTool(id);
}
/** 处理工具选择变化 */
function handleChange(value: SelectValue) {
const toolId = typeof value === 'number' ? value : undefined;
emit('update:modelValue', toolId);
emit('change', list.value.find((item) => item.id === toolId));
watch(
() => props.modelValue,
(value) => {
resolveItemById(value);
},
{ immediate: true },
);
/** 清空已选工具 */
function clearSelected() {
selectedItem.value = undefined;
emit('update:modelValue', undefined);
emit('change', undefined);
}
onMounted(getList);
/** 打开工具选择弹窗 */
function handleClick(event: MouseEvent) {
if (props.disabled) {
return;
}
const target = event.target as HTMLElement;
if (showClear.value && target.closest('.ant-input-suffix')) {
event.stopPropagation();
clearSelected();
return;
}
const selectedIds = props.modelValue == null ? [] : [props.modelValue];
dialogRef.value?.open(selectedIds, { multiple: false });
}
/** 回填选中的工具 */
function handleSelected(rows: MesTmToolApi.Tool[]) {
const item = rows[0];
if (!item) {
return;
}
selectedItem.value = item;
emit('update:modelValue', item.id);
emit('change', item);
}
</script>
<template>
<Select
:allow-clear="allowClear"
:disabled="disabled"
:field-names="{ label: 'name', value: 'id' }"
:options="list"
:placeholder="placeholder"
:value="modelValue"
<div
v-bind="attrs"
class="w-full"
option-filter-prop="name"
show-search
@change="handleChange"
/>
:class="disabled ? 'cursor-not-allowed' : 'cursor-pointer'"
@click="handleClick"
@mouseenter="hovering = true"
@mouseleave="hovering = false"
>
<Tooltip :mouse-enter-delay="0.5" :open="selectedItem ? undefined : false">
<template #title>
<div v-if="selectedItem" class="leading-6">
<div>工具编码{{ selectedItem.code || '-' }}</div>
<div>工具名称{{ selectedItem.name || '-' }}</div>
<div v-if="selectedItem.brand">{{ selectedItem.brand }}</div>
<div v-if="selectedItem.specification">
型号规格{{ selectedItem.specification }}
</div>
<div v-if="selectedItem.toolTypeName">
工具类型{{ selectedItem.toolTypeName }}
</div>
</div>
</template>
<Input
:disabled="disabled"
:placeholder="placeholder"
:value="displayLabel"
readonly
>
<template #suffix>
<IconifyIcon
class="size-4"
:icon="showClear ? 'lucide:circle-x' : 'lucide:search'"
/>
</template>
</Input>
</Tooltip>
</div>
<TmToolSelectDialog ref="dialogRef" @selected="handleSelected" />
</template>

View File

@ -19,7 +19,10 @@ import { TmToolTypeSelect } from './type/components';
export type FormType = 'create' | 'detail' | 'update';
/** 新增/修改工具的表单 */
export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
export function useFormSchema(
formType: FormType,
formApi?: VbenFormApi,
): VbenFormSchema[] {
return [
{
fieldName: 'id',
@ -29,6 +32,15 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
show: () => false,
},
},
{
// 选中工具类型是否「编码管理」,用于锁定库存数量为 1隐藏字段
fieldName: 'codeFlag',
component: 'Input',
dependencies: {
triggerFields: [''],
show: () => false,
},
},
{
fieldName: 'code',
label: '工具编码',
@ -41,18 +53,21 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
componentProps: (values) => ({ disabled: !!values.id }),
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(MesAutoCodeRuleCode.TM_TOOL_CODE);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(MesAutoCodeRuleCode.TM_TOOL_CODE);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',
@ -70,7 +85,9 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
componentProps: {
placeholder: '请选择工具类型',
onChange: async (row: any) => {
if (row?.codeFlag) {
// 记录是否编码管理;编码管理类型库存数量锁定为 1
await formApi?.setFieldValue('codeFlag', row?.codeFlag === true);
if (row?.codeFlag === true) {
await formApi?.setFieldValue('quantity', 1);
await formApi?.setFieldValue('availableQuantity', 1);
}
@ -110,6 +127,13 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
precision: 0,
},
rules: 'required',
// 编码管理类型库存数量锁定为 1禁止修改
dependencies: {
triggerFields: ['codeFlag'],
componentProps: (values) => ({
disabled: values.codeFlag === true,
}),
},
},
{
fieldName: 'availableQuantity',
@ -301,3 +325,85 @@ export function useGridColumns(): VxeTableGridOptions<MesTmToolApi.Tool>['column
},
];
}
/** 工具选择弹窗的搜索表单 */
export function useToolSelectGridFormSchema(): VbenFormSchema[] {
return [
{
fieldName: 'code',
label: '工具编码',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入工具编码',
},
},
{
fieldName: 'name',
label: '工具名称',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入工具名称',
},
},
{
fieldName: 'toolTypeId',
label: '工具类型',
component: markRaw(TmToolTypeSelect),
componentProps: {
placeholder: '请选择工具类型',
},
},
{
fieldName: 'brand',
label: '品牌',
component: 'Input',
componentProps: {
allowClear: true,
placeholder: '请输入品牌',
},
},
{
fieldName: 'status',
label: '状态',
component: 'Select',
componentProps: {
allowClear: true,
options: getDictOptions(DICT_TYPE.MES_TM_TOOL_STATUS, 'number'),
placeholder: '请选择状态',
},
},
];
}
/** 工具选择弹窗的字段 */
export function useToolSelectGridColumns(
multiple = false,
): VxeTableGridOptions<MesTmToolApi.Tool>['columns'] {
return [
{ type: multiple ? 'checkbox' : 'radio', width: 50 },
{ field: 'code', title: '工具编码', width: 120 },
{ field: 'name', title: '工具名称', minWidth: 120 },
{ field: 'brand', title: '品牌', minWidth: 100 },
{ field: 'specification', title: '型号规格', minWidth: 100 },
{ field: 'toolTypeName', title: '工具类型', width: 120 },
{ field: 'quantity', title: '库存数量', width: 100 },
{ field: 'availableQuantity', title: '可用数量', width: 100 },
{
field: 'status',
title: '状态',
width: 100,
cellRender: {
name: 'CellDict',
props: { type: DICT_TYPE.MES_TM_TOOL_STATUS },
},
},
{
field: 'createTime',
title: '创建时间',
width: 180,
formatter: 'formatDateTime',
},
];
}

View File

@ -87,10 +87,10 @@ 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.setState({ schema: useFormSchema(data.formType, formApi) });
formApi.setDisabled(formType.value === 'detail');
modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
if (!data?.id) {

View File

@ -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',
@ -34,18 +37,21 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
placeholder: '请输入类型编码',
},
rules: 'required',
suffix: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(MesAutoCodeRuleCode.TM_TOOL_TYPE_CODE);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
suffix:
formType === 'detail'
? undefined
: () =>
h(
Button,
{
type: 'default',
onClick: async () => {
const code = await generateAutoCode(MesAutoCodeRuleCode.TM_TOOL_TYPE_CODE);
await formApi?.setFieldValue('code', code);
},
},
{ default: () => '生成' },
),
},
{
fieldName: 'name',

View File

@ -71,10 +71,10 @@ 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.setState({ schema: useFormSchema(data.formType, formApi) });
formApi.setDisabled(formType.value === 'detail');
modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
if (!data?.id) {

View File

@ -4,9 +4,14 @@ import type { MesWmBarcodeApi } from '#/api/mes/wm/barcode';
import type { MesWmBarcodeConfigApi } from '#/api/mes/wm/barcode/config';
import type { DescriptionItemSchema } from '#/components/description';
import { BarcodeBizTypeEnum, DICT_TYPE, h, markRaw } from 'vue';
import { h, markRaw } from 'vue';
import { CommonStatusEnum } from '@vben/constants';
import {
BarcodeBizTypeEnum,
CommonStatusEnum,
DICT_TYPE,
MesProWorkOrderStatusEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { formatDateTime } from '@vben/utils';
@ -222,6 +227,7 @@ export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
label: '工单',
component: markRaw(ProWorkOrderSelect),
componentProps: {
status: MesProWorkOrderStatusEnum.CONFIRMED,
onChange: (item: any) => syncBizDetail(formApi, item),
},
dependencies: {

Some files were not shown because too many files have changed in this diff Show More