feat(mes): 新增安全生产模块 - 应急处置预案(一期)
背景:蓝羽此前无任何安全生产功能。经全仓库排查(22 个后端分支 + 21 个前端分支),
完整安全生产模块不存在,仅新高电力分支有应急处置预案,作为安全生产第一个能力点先落地。
新增:
- views/mes/pro/emergencyplan:列表 + 新增/编辑表单(预案编码、名称、类型、处置内容、责任单位、状态)
- api/mes/pro/emergencyplan:对接 /mes/pro/emergency-plan 接口
- components/import-modal:预案导入依赖的通用导入弹窗组件(新高电力独有,蓝羽基线缺失)
- dict-enum.ts 补 MES_PRO_EMERGENCY_PLAN_TYPE 字典常量,否则预案类型下拉框全空
配套后端同批提交,建表/菜单/字典 SQL 见 mes_safety_emergency_plan_init.sql。
后续能力点(风险分级管控、隐患排查治理、特种作业票证、安全教育培训、
可视化看板、统计分析)分期补充。
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProEmergencyPlanApi { |
| | | /** åºæ¥å¤ç½®é¢æ¡ */ |
| | | export interface EmergencyPlan { |
| | | id?: number; |
| | | code?: string; // 颿¡ç¼ç |
| | | name?: string; // 颿¡åç§° |
| | | planType?: number; // 颿¡ç±»åï¼1åçµ 2è®¾å¤æ
é 3ç«ç¾ 4èªç¶ç¾å®³ 5å
¶ä» |
| | | content?: string; // å¤ç½®å
容 |
| | | orgUnit?: string; // 责任åä½ |
| | | status?: number; // ç¶æï¼0è稿 10å·²åå¸ |
| | | remark?: string; // 夿³¨ |
| | | createTime?: string; |
| | | } |
| | | } |
| | | |
| | | /** åå»ºåºæ¥å¤ç½®é¢æ¡ */ |
| | | export function createPlan(data: MesProEmergencyPlanApi.EmergencyPlan) { |
| | | return requestClient.post('/mes/pro/emergency-plan/create', data); |
| | | } |
| | | |
| | | /** æ´æ°åºæ¥å¤ç½®é¢æ¡ */ |
| | | export function updatePlan(data: MesProEmergencyPlanApi.EmergencyPlan) { |
| | | return requestClient.put('/mes/pro/emergency-plan/update', data); |
| | | } |
| | | |
| | | /** å é¤åºæ¥å¤ç½®é¢æ¡ */ |
| | | export function deletePlan(ids: number[]) { |
| | | return requestClient.delete('/mes/pro/emergency-plan/delete', { params: { ids: ids.join(',') } }); |
| | | } |
| | | |
| | | /** è·ååºæ¥å¤ç½®é¢æ¡ */ |
| | | export function getPlan(id: number) { |
| | | return requestClient.get<MesProEmergencyPlanApi.EmergencyPlan>( |
| | | `/mes/pro/emergency-plan/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** è·ååºæ¥å¤ç½®é¢æ¡å页 */ |
| | | export function getPlanPage(params: PageParam) { |
| | | return requestClient.get<PageResult<MesProEmergencyPlanApi.EmergencyPlan>>( |
| | | '/mes/pro/emergency-plan/page', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** åå¸åºæ¥å¤ç½®é¢æ¡ */ |
| | | export function publishPlan(id: number) { |
| | | return requestClient.put(`/mes/pro/emergency-plan/publish?id=${id}`); |
| | | } |
| | | |
| | | /** ä¸è½½åºæ¥å¤ç½®é¢æ¡å¯¼å
¥æ¨¡æ¿ */ |
| | | export function importPlanTemplate() { |
| | | return requestClient.download('/mes/pro/emergency-plan/get-import-template'); |
| | | } |
| | | |
| | | /** 导å
¥åºæ¥å¤ç½®é¢æ¡ */ |
| | | export function importPlan(file: File, updateSupport: boolean) { |
| | | return requestClient.upload( |
| | | `/mes/pro/emergency-plan/import?updateSupport=${updateSupport}`, |
| | | { file }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { FileType } from 'ant-design-vue/es/upload/interface'; |
| | | |
| | | import { h, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | |
| | | import { Button, message, Modal as AntModal, Upload } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm, z } from '#/adapter/form'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | /** |
| | | * 导å
¥æ¥å£è¿åç»æã |
| | | * |
| | | * åç«¯åæ¨¡åç导å
¥è¿åå¼ä¸ç»ä¸ï¼è¿éæè¿°å¯å±ç¤ºç»æä¸çº¦å®æç¡®çåæ®µï¼ |
| | | * create* / update* / failure* çä¸å¡ä¸ç¨å段ä¼å¨è¿è¡æ¶ç»ä¸è¯å«ã |
| | | */ |
| | | export interface ImportResultLike { |
| | | /** 被跳è¿çè¡ï¼key 为 Excel è¡å·ï¼value 为åå */ |
| | | skippedRows?: Record<string, string>; |
| | | /** 被跳è¿çè¡æ° */ |
| | | skippedCount?: number; |
| | | /** Excel ä¸å®é
ææ°æ®çè¡æ° */ |
| | | totalRows?: number; |
| | | /** æå导å
¥çè¡æ° */ |
| | | successRows?: number; |
| | | /** æå导å
¥çæ°é */ |
| | | successCount?: number; |
| | | /** 导å
¥å¤±è´¥çæ°é */ |
| | | failureCount?: number; |
| | | /** å
¶å®æ¨¡åçéç¨å段ï¼å¤±è´¥å表 */ |
| | | failureList?: Record<string, string> | string[]; |
| | | /** å
¶å®æ¨¡åçéç¨å段ï¼å¤±è´¥åå */ |
| | | failureMessages?: Record<string, string> | string[]; |
| | | [key: string]: unknown; |
| | | } |
| | | |
| | | export interface ImportModalProps { |
| | | /** å¼¹çªæ é¢ï¼å¦ã导å
¥ç¨æ·ã */ |
| | | title: string; |
| | | /** ä¸è½½çæ¨¡æ¿æä»¶åï¼å¦ãç¨æ·å¯¼å
¥æ¨¡æ¿.xlsã */ |
| | | fileName: string; |
| | | /** æ§è¡å¯¼å
¥çæ¥å£å½æ° (file, updateSupport) => Promise */ |
| | | importFn: (file: File, updateSupport: boolean) => Promise<unknown>; |
| | | /** ä¸è½½æ¨¡æ¿çæ¥å£å½æ° () => Promise(blob) */ |
| | | templateFn: () => Promise<BlobPart>; |
| | | } |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const config = ref<ImportModalProps>({ |
| | | title: '', |
| | | fileName: '', |
| | | importFn: async () => {}, |
| | | templateFn: () => Promise.resolve(new Blob()), |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | formItemClass: 'col-span-2', |
| | | labelWidth: 120, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [ |
| | | { |
| | | fieldName: 'file', |
| | | label: 'æ°æ®æä»¶', |
| | | component: 'Upload', |
| | | rules: 'required', |
| | | help: 'ä»
å
许导å
¥ xlsãxlsx æ ¼å¼æä»¶', |
| | | }, |
| | | { |
| | | fieldName: 'updateSupport', |
| | | label: 'æ¯å¦è¦ç', |
| | | component: 'Switch', |
| | | componentProps: { |
| | | checkedChildren: 'æ¯', |
| | | unCheckedChildren: 'å¦', |
| | | }, |
| | | rules: z.boolean().default(false), |
| | | help: 'æ¯å¦æ´æ°å·²ç»åå¨çæ°æ®', |
| | | }, |
| | | ], |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | /** |
| | | * ä»å¯¼å
¥æ¥å£çè¿åå¼éæåå¯å±ç¤ºçç»æã |
| | | * |
| | | * å
³é®ç¹ï¼ä¸è½åªçæ¥å£ææ²¡ææ¥éå°±æç¤ºãæä½æåãââå端å¯è½è·³è¿äºé¨åè¡ï¼ |
| | | * è¿äºè¡ä¸ä¼æå¼å¸¸ï¼å¿
é¡»æè·³è¿/å¤±è´¥çæ°éåè¡å·æç¡®åè¯ç¨æ·ï¼å¦åæ°æ®æ 声丢失ã |
| | | */ |
| | | function summarizeImportResult(raw: unknown) { |
| | | const result = unwrapImportResult(raw); |
| | | |
| | | // 忍¡ååå«è¿å createCodesãcreateContractNamesãupdateNames çåæ®µï¼ |
| | | // ä¸è½æéç¨ç»ä»¶ç»å®å°æä¸ä¸ªä¸å¡çåæ®µåã |
| | | const successRows = firstNumber(result.successRows, result.successCount) |
| | | ?? countArrayFields(result, /^(?:create|update|success)(?!Count$)/i); |
| | | |
| | | const failureEntries = collectEntries(result, /^failure(?!Count$)/i); |
| | | const skippedEntries = toEntries(result.skippedRows); |
| | | const failureRows = firstNumber(result.failureCount) ?? failureEntries.length; |
| | | const skippedRows = firstNumber(result.skippedCount) ?? skippedEntries.length; |
| | | const totalRows = firstNumber(result.totalRows); |
| | | const recognized = hasOutcomeFields(result); |
| | | |
| | | if (failureRows > 0 && failureEntries.length === 0) { |
| | | failureEntries.push([`失败 ${failureRows} æ¡`, 'å端æªè¿å失败æç»']); |
| | | } |
| | | if (skippedRows > 0 && skippedEntries.length === 0) { |
| | | skippedEntries.push([`è·³è¿ ${skippedRows} æ¡`, 'å端æªè¿åè·³è¿æç»']); |
| | | } |
| | | |
| | | return { |
| | | successRows, |
| | | failureRows, |
| | | skippedRows, |
| | | failureEntries, |
| | | skippedEntries, |
| | | totalRows, |
| | | recognized, |
| | | hasIssue: failureRows > 0 || skippedRows > 0, |
| | | }; |
| | | } |
| | | |
| | | const OUTCOME_KEY_PATTERN = /^(?:create|update|success|failure|skipped|totalRows)/i; |
| | | |
| | | function hasOutcomeFields(result: ImportResultLike) { |
| | | return Object.keys(result).some((key) => OUTCOME_KEY_PATTERN.test(key)); |
| | | } |
| | | |
| | | /** requestClient é»è®¤ä¼è§£å
dataï¼åæ¶å
¼å®¹ Axios / CommonResult çåµå¥ dataã */ |
| | | function unwrapImportResult(raw: unknown): ImportResultLike { |
| | | let current = raw; |
| | | for (let depth = 0; depth < 3; depth++) { |
| | | if (!current || typeof current !== 'object' || Array.isArray(current)) { |
| | | return {}; |
| | | } |
| | | const result = current as ImportResultLike; |
| | | if (hasOutcomeFields(result)) { |
| | | return result; |
| | | } |
| | | current = result.data; |
| | | } |
| | | return {}; |
| | | } |
| | | |
| | | function firstNumber(...values: unknown[]): number | undefined { |
| | | return values.find((value): value is number => typeof value === 'number'); |
| | | } |
| | | |
| | | /** æ±æ» create* / update* / success* æ°ç»çé¿åº¦ã */ |
| | | function countArrayFields(result: ImportResultLike, keyPattern: RegExp) { |
| | | return Object.entries(result).reduce( |
| | | (total, [key, value]) => |
| | | total + (keyPattern.test(key) && Array.isArray(value) ? value.length : 0), |
| | | 0, |
| | | ); |
| | | } |
| | | |
| | | /** åå¹¶ failure* çç»æåæ®µï¼ä¿çä¸å¡åå·å失败åå ã */ |
| | | function collectEntries(result: ImportResultLike, keyPattern: RegExp) { |
| | | return Object.entries(result).flatMap(([key, value]) => |
| | | keyPattern.test(key) ? toEntries(value) : [], |
| | | ); |
| | | } |
| | | |
| | | /** æ Record / string[] / undefined ç»ä¸æ [label, reason][] */ |
| | | function toEntries(input: unknown): Array<[string, string]> { |
| | | if (!input) { |
| | | return []; |
| | | } |
| | | if (Array.isArray(input)) { |
| | | return input.map((item) => [String(item), '']); |
| | | } |
| | | if (typeof input === 'object') { |
| | | return Object.entries(input as Record<string, string>).map(([k, v]) => [ |
| | | k, |
| | | String(v ?? ''), |
| | | ]); |
| | | } |
| | | return []; |
| | | } |
| | | |
| | | /** æç»ææ¼æç¨æ·è½çæç䏿®µè¯ */ |
| | | function buildResultMessage(summary: ReturnType<typeof summarizeImportResult>) { |
| | | const parts: string[] = []; |
| | | if (summary.successRows > 0) { |
| | | parts.push(`æå ${summary.successRows} æ¡`); |
| | | } |
| | | if (summary.skippedRows > 0) { |
| | | parts.push(`è·³è¿ ${summary.skippedRows} æ¡`); |
| | | } |
| | | if (summary.failureRows > 0) { |
| | | parts.push(`失败 ${summary.failureRows} æ¡`); |
| | | } |
| | | if (parts.length === 0) { |
| | | return summary.recognized |
| | | ? '导å
¥å®æï¼ä½æ²¡æå¯å¤ççæ°æ®' |
| | | : '导å
¥è¯·æ±å·²å®æï¼è¯·å·æ°åè¡¨ç¡®è®¤ç»æ'; |
| | | } |
| | | const total = summary.totalRows === undefined ? '' : `ï¼è¡¨æ ¼å
± ${summary.totalRows} è¡ï¼`; |
| | | return `导å
¥å®æï¼${parts.join('ï¼')}${total}`; |
| | | } |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (isOpen) { |
| | | config.value = modalApi.getData<ImportModalProps>() ?? config.value; |
| | | await formApi.resetForm(); |
| | | } |
| | | }, |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | // æäº¤è¡¨å |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | // ç»ä¸å
åºï¼åç«¯åæ¨¡ååå²å®ç°å¯¹ updateSupport çæ¥æºä¸ä¸è´ï¼æçä» query 读ã |
| | | // æçä»è¡¨å读ï¼ï¼æ¼ä¼ ä¼è¢« @NotNull æ¦ä¸å¹¶å¼¹åºç¨æ·æ ¹æ¬çä¸å°å¯¹åºå段ç |
| | | // "æ¯å¦æ¯ææ´æ°ä¸è½ä¸ºç©º"ãè¿éå¼ºå¶æ¾å¼ä¼ å¸å°å¼ï¼é¿å
ä»»ä½é¡µé¢å æªåå§å踩åã |
| | | const updateSupport = data.updateSupport === true; |
| | | const raw = await config.value.importFn(data.file, updateSupport); |
| | | const summary = summarizeImportResult(raw); |
| | | const text = buildResultMessage(summary); |
| | | |
| | | if (summary.hasIssue) { |
| | | // æè·³è¿æå¤±è´¥çè¡ï¼ä¿æå¼¹çªæå¼ï¼ææç»å±ç¤ºç»ç¨æ·ï¼é¿å
æ°æ®æ 声丢失 |
| | | message.warning({ |
| | | content: text, |
| | | duration: 8, |
| | | }); |
| | | showDetail(summary); |
| | | emit('success'); |
| | | return; |
| | | } |
| | | |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success(text); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | |
| | | /** ç¨ Modal.info å±ç¤ºè·³è¿/失败æç»ï¼æ¯ toast æ´è读 */ |
| | | function showDetail(summary: ReturnType<typeof summarizeImportResult>) { |
| | | const sections: Array<{ title: string; rows: Array<[string, string]> }> = []; |
| | | if (summary.skippedEntries.length > 0) { |
| | | sections.push({ title: '被跳è¿çè¡ï¼æªå¯¼å
¥ï¼', rows: summary.skippedEntries }); |
| | | } |
| | | if (summary.failureEntries.length > 0) { |
| | | sections.push({ title: '导å
¥å¤±è´¥', rows: summary.failureEntries }); |
| | | } |
| | | AntModal.info({ |
| | | title: '导å
¥ç»ææç»', |
| | | width: 560, |
| | | content: h( |
| | | 'div', |
| | | { style: 'max-height: 360px; overflow-y: auto;' }, |
| | | sections.map((section) => |
| | | h('div', { style: 'margin-bottom: 12px;' }, [ |
| | | h('div', { style: 'font-weight: 600; margin-bottom: 4px;' }, section.title), |
| | | ...section.rows.map(([label, reason]) => |
| | | h('div', { style: 'font-size: 13px; line-height: 20px;' }, [ |
| | | h('span', { style: 'color: #cf1322;' }, `${label}`), |
| | | h('span', {}, `ï¼${reason || 'æªè¯´æåå '}`), |
| | | ]), |
| | | ), |
| | | ]), |
| | | ), |
| | | ), |
| | | }); |
| | | } |
| | | |
| | | /** ä¸ä¼ å */ |
| | | function beforeUpload(file: FileType) { |
| | | formApi.setFieldValue('file', file); |
| | | return false; |
| | | } |
| | | |
| | | /** ä¸è½½æ¨¡ç */ |
| | | async function handleDownload() { |
| | | const data = await config.value.templateFn(); |
| | | downloadFileFromBlobPart({ fileName: config.value.fileName, source: data }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="config.title" class="w-1/3"> |
| | | <Form class="mx-4"> |
| | | <template #file> |
| | | <div class="w-full"> |
| | | <Upload |
| | | :max-count="1" |
| | | accept=".xls,.xlsx" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <Button type="primary"> éæ© Excel æä»¶ </Button> |
| | | </Upload> |
| | | </div> |
| | | </template> |
| | | </Form> |
| | | <template #prepend-footer> |
| | | <div class="flex flex-auto items-center"> |
| | | <Button @click="handleDownload"> ä¸è½½å¯¼å
¥æ¨¡æ¿ </Button> |
| | | </div> |
| | | </template> |
| | | </Modal> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | export { default } from './import-modal.vue'; |
| | |
| | | MES_EM_ALERT_TYPE: 'mes_em_alert_type', // MES è½èé¢è¦ç±»å |
| | | MES_EM_ALERT_LEVEL: 'mes_em_alert_level', // MES è½èé¢è¦çº§å« |
| | | MES_EM_ALERT_STATUS: 'mes_em_alert_status', // MES è½èé¢è¦å¤çç¶æ |
| | | MES_PRO_EMERGENCY_PLAN_TYPE: 'mes_pro_emergency_plan_type', // MES åºæ¥å¤ç½®é¢æ¡ç±»å |
| | | } as const; |
| | | |
| | | /** ========== WMS - ä»å¨ç®¡ç模å ========== */ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | /** 表åç±»å */ |
| | | export type FormType = 'create' | 'detail' | 'edit'; |
| | | |
| | | /** ç¶æå¸¸é */ |
| | | export const PLAN_STATUS = { |
| | | DRAFT: 0, |
| | | PUBLISHED: 10, |
| | | }; |
| | | |
| | | /** 表åçé
置项 */ |
| | | export function useFormSchema(formType: FormType): VbenFormSchema[] { |
| | | const isReadonly = formType === 'detail'; |
| | | return [ |
| | | { |
| | | fieldName: 'id', |
| | | component: 'Input', |
| | | dependencies: { |
| | | triggerFields: [''], |
| | | show: () => false, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'code', |
| | | label: '颿¡ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: 'ç³»ç»èªå¨çæ', |
| | | disabled: true, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: '颿¡åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é¢æ¡åç§°', |
| | | disabled: isReadonly, |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'planType', |
| | | label: '颿¡ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©é¢æ¡ç±»å', |
| | | disabled: isReadonly, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'orgUnit', |
| | | label: '责任åä½', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥è´£ä»»åä½/é¨é¨', |
| | | disabled: isReadonly, |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'content', |
| | | label: 'å¤ç½®å
容', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥åºæ¥å¤ç½®å
容', |
| | | rows: 4, |
| | | disabled: isReadonly, |
| | | }, |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥å¤æ³¨', |
| | | autoSize: { minRows: 1, maxRows: 1 }, |
| | | disabled: isReadonly, |
| | | }, |
| | | formItemClass: 'col-span-3', |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çæç´¢è¡¨å */ |
| | | export function useGridFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: '颿¡ç¼ç ', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é¢æ¡ç¼ç ', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: '颿¡åç§°', |
| | | component: 'Input', |
| | | componentProps: { |
| | | placeholder: '请è¾å
¥é¢æ¡åç§°', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'planType', |
| | | label: '颿¡ç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©é¢æ¡ç±»å', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: [ |
| | | { label: 'è稿', value: PLAN_STATUS.DRAFT }, |
| | | { label: 'å·²åå¸', value: PLAN_STATUS.PUBLISHED }, |
| | | ], |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** å表çè¡¨æ ¼å */ |
| | | export function useGridColumns(): VxeTableGridOptions['columns'] { |
| | | return [ |
| | | { type: 'checkbox', width: 50 }, |
| | | { type: 'seq', title: 'åºå·', width: 60 }, |
| | | { |
| | | field: 'code', |
| | | title: '颿¡ç¼ç ', |
| | | minWidth: 150, |
| | | slots: { default: 'code' }, |
| | | }, |
| | | { |
| | | field: 'name', |
| | | title: '颿¡åç§°', |
| | | minWidth: 200, |
| | | }, |
| | | { |
| | | field: 'planType', |
| | | title: '颿¡ç±»å', |
| | | width: 110, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_PRO_EMERGENCY_PLAN_TYPE }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'orgUnit', |
| | | title: '责任åä½', |
| | | minWidth: 140, |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | slots: { default: 'status' }, |
| | | }, |
| | | { |
| | | field: 'createTime', |
| | | title: 'å建æ¶é´', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'action', |
| | | title: 'æä½', |
| | | width: 260, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { Page, useVbenModal } from '@vben/common-ui'; |
| | | import { isEmpty } from '@vben/utils'; |
| | | |
| | | import { message, Tag } from 'ant-design-vue'; |
| | | |
| | | import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | deletePlan, |
| | | getPlanPage, |
| | | importPlan, |
| | | importPlanTemplate, |
| | | publishPlan, |
| | | } from '#/api/mes/pro/emergencyplan'; |
| | | import ImportModalComponent from '#/components/import-modal'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { PLAN_STATUS, useGridColumns, useGridFormSchema } from './data'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | /** MES åºæ¥å¤ç½®é¢æ¡å表 */ |
| | | defineOptions({ name: 'MesProEmergencyPlan' }); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [ImportModal, importModalApi] = useVbenModal({ |
| | | connectedComponent: ImportModalComponent, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData({ formType: 'create' }).open(); |
| | | } |
| | | |
| | | function handleEdit(row: MesProEmergencyPlanApi.EmergencyPlan) { |
| | | formModalApi.setData({ formType: 'edit', id: row.id }).open(); |
| | | } |
| | | |
| | | function handleDetail(row: MesProEmergencyPlanApi.EmergencyPlan) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | async function handleDelete(ids: number[]) { |
| | | const hideLoading = message.loading({ |
| | | content: $t('ui.actionMessage.deleting'), |
| | | duration: 0, |
| | | }); |
| | | try { |
| | | await deletePlan(ids); |
| | | message.success($t('ui.actionMessage.deleteSuccess')); |
| | | handleRefresh(); |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | async function handlePublish(row: MesProEmergencyPlanApi.EmergencyPlan) { |
| | | await publishPlan(row.id!); |
| | | message.success('å·²åå¸'); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | /** 导å
¥åºæ¥å¤ç½®é¢æ¡ */ |
| | | function handleImport() { |
| | | importModalApi |
| | | .setData({ |
| | | title: $t('ui.actionTitle.import', ['åºæ¥å¤ç½®é¢æ¡']), |
| | | fileName: 'åºæ¥å¤ç½®é¢æ¡å¯¼å
¥æ¨¡æ¿.xls', |
| | | importFn: importPlan, |
| | | templateFn: importPlanTemplate, |
| | | }) |
| | | .open(); |
| | | } |
| | | |
| | | const checkedIds = ref<number[]>([]); |
| | | function handleRowCheckboxChange({ |
| | | records, |
| | | }: { |
| | | records: MesProEmergencyPlanApi.EmergencyPlan[]; |
| | | }) { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: useGridFormSchema(), |
| | | }, |
| | | gridOptions: { |
| | | columns: useGridColumns(), |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getPlanPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | toolbarConfig: { |
| | | refresh: true, |
| | | search: true, |
| | | }, |
| | | } as VxeTableGridOptions<MesProEmergencyPlanApi.EmergencyPlan>, |
| | | gridEvents: { |
| | | checkboxAll: handleRowCheckboxChange, |
| | | checkboxChange: handleRowCheckboxChange, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | <ImportModal @success="handleRefresh" /> |
| | | <Grid table-title="åºæ¥å¤ç½®é¢æ¡å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['åºæ¥å¤ç½®é¢æ¡']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['mes:pro-emergency-plan:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.import', ['åºæ¥å¤ç½®é¢æ¡']), |
| | | icon: ACTION_ICON.UPLOAD, |
| | | auth: ['mes:pro-emergency-plan:import'], |
| | | onClick: handleImport, |
| | | }, |
| | | { |
| | | label: 'æ¹éå é¤', |
| | | type: 'primary', |
| | | danger: true, |
| | | disabled: isEmpty(checkedIds), |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:pro-emergency-plan:delete'], |
| | | popConfirm: { |
| | | title: `æ¯å¦å 餿é䏿°æ®ï¼`, |
| | | confirm: handleDelete.bind(null, checkedIds), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | <template #code="{ row }"> |
| | | <a class="link" @click="handleDetail(row)">{{ row.code }}</a> |
| | | </template> |
| | | <template #status="{ row }"> |
| | | <Tag v-if="row.status === PLAN_STATUS.DRAFT" color="default">è稿</Tag> |
| | | <Tag v-else-if="row.status === PLAN_STATUS.PUBLISHED" color="success">å·²åå¸</Tag> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.detail'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.VIEW, |
| | | onClick: handleDetail.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['mes:pro-emergency-plan:update'], |
| | | ifShow: row.status === PLAN_STATUS.DRAFT, |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'åå¸', |
| | | type: 'link', |
| | | auth: ['mes:pro-emergency-plan:update'], |
| | | ifShow: row.status === PLAN_STATUS.DRAFT, |
| | | popConfirm: { |
| | | title: '确认åå¸è¯¥é¢æ¡ï¼', |
| | | confirm: handlePublish.bind(null, row), |
| | | }, |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['mes:pro-emergency-plan:delete'], |
| | | ifShow: row.status === PLAN_STATUS.DRAFT, |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.code]), |
| | | confirm: handleDelete.bind(null, [row.id!]), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesProEmergencyPlanApi } from '#/api/mes/pro/emergencyplan'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createPlan, getPlan, updatePlan } from '#/api/mes/pro/emergencyplan'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import type { FormType } from '../data'; |
| | | import { useFormSchema } from '../data'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | const formData = ref<MesProEmergencyPlanApi.EmergencyPlan>(); |
| | | const formType = ref<FormType>('create'); |
| | | |
| | | const getTitle = computed(() => { |
| | | if (formType.value === 'create') { |
| | | return $t('ui.actionTitle.create', ['åºæ¥å¤ç½®é¢æ¡']); |
| | | } else if (formType.value === 'edit') { |
| | | return $t('ui.actionTitle.edit', ['åºæ¥å¤ç½®é¢æ¡']); |
| | | } else { |
| | | return 'åºæ¥å¤ç½®é¢æ¡è¯¦æ
'; |
| | | } |
| | | }); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | | class: 'w-full', |
| | | }, |
| | | labelWidth: 120, |
| | | }, |
| | | wrapperClass: 'grid-cols-3', |
| | | layout: 'vertical', |
| | | schema: useFormSchema(formType.value), |
| | | showDefaultActions: false, |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | const data = |
| | | (await formApi.getValues()) as MesProEmergencyPlanApi.EmergencyPlan; |
| | | try { |
| | | await (formType.value === 'create' |
| | | ? createPlan(data) |
| | | : updatePlan(data)); |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | async onOpenChange(isOpen: boolean) { |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | return; |
| | | } |
| | | const data = modalApi.getData<{ formType: FormType; id?: number }>(); |
| | | formType.value = data.formType; |
| | | formApi.setDisabled(formType.value === 'detail'); |
| | | formApi.updateSchema(useFormSchema(formType.value)); |
| | | if (!data || !data.id) { |
| | | return; |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | formData.value = await getPlan(data.id); |
| | | await formApi.setValues(formData.value); |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="getTitle" class="w-2/3" :show-confirm-button="formType !== 'detail'"> |
| | | <Form class="mx-3" /> |
| | | </Modal> |
| | | </template> |