From 081f242ea3c019f4eddd0fe342c5cf6189da1468 Mon Sep 17 00:00:00 2001
From: spring <2396852758@qq.com>
Date: 星期五, 03 七月 2026 13:58:01 +0800
Subject: [PATCH] fix: 产品设计
---
src/router/routes/modules/mes.ts | 9
src/views/mes/process-design/process/modules/content-list.vue | 132
src/views/mes/process-design/route/components/index.ts | 1
src/views/mes/pd/project/modules/bom-list.vue | 122
src/views/mes/pd/project/modules/material-panel.vue | 28
src/api/mes/pd/mock/enabled.ts | 4
src/views/mes/pd/project/index.vue | 212 +
src/views/mes/pd/project/modules/document-form.vue | 142
src/views/mes/pd/project/modules/drawing-review/review-form.vue | 63
src/api/mes/pd/archive/index.ts | 62
src/views/mes/process-design/parameter/index.vue | 65
src/views/mes/process-design/route/modules/product-list.vue | 134
src/api/mes/pd/bom/index.ts | 59
src/api/mes/pd/mock/seed.ts | 386 ++
src/views/mes/pd/project/detail/index.vue | 224 +
src/views/mes/pd/project/modules/task-draft-banner.vue | 32
src/views/mes/process-design/process/components/index.ts | 1
src/api/mes/pd/project/index.ts | 119
src/views/mes/pd/archive/modules/detail.vue | 104
src/views/mes/pd/project/detail/data.ts | 43
src/views/mes/process-design/route/modules/product-form.vue | 125
src/api/mes/pd/mock/store.ts | 554 +++
src/views/mes/process-design/route/modules/form.vue | 121
src/views/mes/process-design/route/modules/product-bom-list.vue | 171 +
src/api/mes/pro/processdesign/parameter/index.ts | 64
src/views/mes/process-design/route/data.ts | 517 +++
src/views/mes/process-design/process/modules/content-form.vue | 98
src/api/mes/pd/version-change/index.ts | 83
src/api/mes/pd/drawing-review/index.ts | 74
src/views/mes/process-design/template/modules/parameter-select-dialog.vue | 207 +
src/views/mes/pd/project/modules/task/task-form.vue | 114
src/views/mes/process-design/process/modules/form.vue | 114
mdm_frontend_change.md | 2
src/views/mes/process-design/route/modules/process-list.vue | 136
src/views/mes/pd/project/modules/task/data.ts | 147
src/views/mes/process-design/process/data.ts | 274 +
src/views/mes/pd/project/modules/workflow-action-form.vue | 126
.env.development | 3
src/views/mes/pd/project/modules/version-change/version-change-form.vue | 108
src/views/mes/process-design/template/data.ts | 128
src/api/mes/pd/workflow/index.ts | 28
src/views/mes/md/vendor/data.ts | 381 --
src/views/mes/pd/project/modules/document-list.vue | 165 +
src/packages/constants/src/dict-enum.ts | 8
src/views/mes/process-design/process/components/select.vue | 114
src/views/mes/pd/project/modules/form.vue | 106
src/views/mes/pd/archive/data.ts | 71
FRONTEND_DEVELOPMENT.md | 311
src/views/mes/pro/task/data.ts | 4
src/api/mes/pd/task/index.ts | 98
src/views/mes/process-design/route/components/color-picker.vue | 66
CLAUDE.md | 11
src/views/mes/pd/project/data.ts | 535 +++
src/views/mes/pro/card/data.ts | 2
src/views/mes/process-design/template/modules/form.vue | 173 +
src/views/mes/pd/project/modules/create-form.vue | 77
vite.config.ts | 10
src/views/mes/pd/project/modules/publish-form.vue | 103
src/views/mes/pd/project/modules/drawing-review/review-list.vue | 77
src/views/mes/pd/project/modules/task/task-list.vue | 149
src/views/mes/process-design/template/index.vue | 65
src/views/mes/pro/andon/record/data.ts | 2
src/views/mes/pd/project/modules/version-change/data.ts | 167 +
src/api/mes/pro/processdesign/template/index.ts | 53
src/views/mes/pd/project/modules/bom-form.vue | 124
.claude/settings.local.json | 8
src/packages/constants/src/biz-mes-enum.ts | 62
src/views/mes/pd/project/modules/drawing-review/data.ts | 88
src/views/mes/process-design/route/modules/bom-form.vue | 118
src/views/mes/pd/archive/index.vue | 75
src/views/mes/process-design/parameter/modules/form.vue | 99
/dev/null | 103
src/views/mes/md/workstation/data.ts | 2
src/views/mes/process-design/parameter/data.ts | 197 +
src/views/mes/process-design/process/index.vue | 66
src/api/mes/pd/document/index.ts | 71
src/api/mes/pd/mock/utils.ts | 28
src/views/mes/md/item/data.ts | 2
src/views/mes/pd/project/modules/version-change/version-change-list.vue | 186 +
src/views/mes/md/client/data.ts | 387 --
src/views/mes/process-design/route/index.vue | 180 +
src/views/mes/pd/project/modules/workflow-timeline.vue | 53
src/views/mes/process-design/route/modules/process-form.vue | 121
83 files changed, 8,727 insertions(+), 1,127 deletions(-)
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 82123c6..5d7a2ad 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -3,7 +3,13 @@
"allow": [
"WebFetch(domain:github.com)",
"Bash(gh repo *)",
- "WebSearch"
+ "WebSearch",
+ "PowerShell(pnpm typecheck *)",
+ "PowerShell(pnpm dev *)",
+ "PowerShell(Get-ChildItem -Path \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\src\\\\views\\\\mes\" -Directory -Recurse | Where-Object { $_.GetDirectories\\(\\).Count -gt 0 -or $_.Parent.Name -in @\\('mes','md','pro','process-design','cal','dv','qc','wm','tm','pd'\\) } | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Substring\\(66\\) })",
+ "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/views/mes\" -Directory -Recurse | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })",
+ "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/api/mes\" -Recurse -File | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })",
+ "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/router\" -Recurse -File | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })"
]
}
}
diff --git a/.env.development b/.env.development
index cff2557..5ade162 100644
--- a/.env.development
+++ b/.env.development
@@ -19,3 +19,6 @@
VITE_APP_DEFAULT_USERNAME=admin
# 榛樿鐧诲綍瀵嗙爜
VITE_APP_DEFAULT_PASSWORD=admin123
+
+# 浜у搧璁捐妯″潡鏈湴 Mock锛堟棤鍚庣鏃舵紨绀猴級
+VITE_PD_MOCK=true
diff --git a/CLAUDE.md b/CLAUDE.md
index b6cdce2..4954570 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -92,3 +92,14 @@
2. 浣跨敤 VeeValidate + Zod 杩涜琛ㄥ崟楠岃瘉
3. 浣跨敤 `v-dompurify-html` 澶勭悊鐢ㄦ埛杈撳叆鐨� HTML
4. 鏁忔劅鏁版嵁浣跨敤 `secure-ls` 鍔犲瘑瀛樺偍
+
+## MES 妯″潡寮�鍙�
+
+> **閲嶈**锛氬紑鍙� MES 妯″潡鍓嶏紝蹇呴』鍏堥槄璇� `MES_DEVELOPMENT.md`锛岃鏂囦欢鍖呭惈锛�
+> - 椤甸潰鏂囦欢缁撴瀯瑙勮寖锛坉ata.ts / index.vue / form.vue锛�
+> - API 鎺ュ彛缂栧啓妯℃澘
+> - 瀵煎叆璺緞灞傜骇瑙勫垯锛坕ndex.vue 鐢� 4 灞� `..`锛宖orm.vue 鐢� 5 灞� `..`锛�
+> - 瀛楀吀/鏋氫妇娉ㄥ唽浣嶇疆
+> - 璺敱瑙勫垯锛堝姩鎬佽矾鐢憋紝鏃犻渶鎵嬪姩閰嶇疆锛�
+> - Select/SelectDialog 缁勪欢妯″紡
+> - 寮�鍙戞鏌ユ竻鍗�
diff --git a/FRONTEND_DEVELOPMENT.md b/FRONTEND_DEVELOPMENT.md
index 423b6ba..367ad5a 100644
--- a/FRONTEND_DEVELOPMENT.md
+++ b/FRONTEND_DEVELOPMENT.md
@@ -12,13 +12,13 @@
```typescript
// 鉁� 姝g‘锛氱粍浠舵枃浠朵娇鐢� PascalCase
-UserList.vue
-DictTag.vue
-TableAction.vue
+UserList.vue;
+DictTag.vue;
+TableAction.vue;
// 鉂� 閿欒锛氫娇鐢� kebab-case 鎴栧叾浠栧懡鍚�
-user-list.vue
-dict_tag.vue
+user - list.vue;
+dict_tag.vue;
```
#### 缁勪欢瀹氫箟
@@ -26,7 +26,7 @@
```vue
<script lang="ts" setup>
// 鉁� 姝g‘锛氱粺涓�浣跨敤 <script setup lang="ts">
-defineOptions({ name: 'UserList' });
+defineOptions({ name: "UserList" });
// Props 瀹氫箟
interface Props {
@@ -35,13 +35,13 @@
}
const props = withDefaults(defineProps<Props>(), {
- userName: '',
+ userName: "",
});
// Emits 瀹氫箟
interface Emits {
- (e: 'update', value: string): void;
- (e: 'delete', id: number): void;
+ (e: "update", value: string): void;
+ (e: "delete", id: number): void;
}
const emit = defineEmits<Emits>();
@@ -68,10 +68,10 @@
}
// 鉁� 姝g‘锛氫娇鐢� import type 瀵煎叆绫诲瀷
-import type { UserInfo } from '#/api/system/user';
+import type { UserInfo } from "#/api/system/user";
// 鉂� 閿欒锛氭櫘閫氬鍏ョ被鍨�
-import { UserInfo } from '#/api/system/user';
+import { UserInfo } from "#/api/system/user";
```
### 1.2 鐩綍缁撴瀯瑙勮寖
@@ -105,7 +105,7 @@
```typescript
// src/api/system/user/index.ts
-import { request } from '#/utils/request';
+import { request } from "#/utils/request";
export namespace SystemUserApi {
/** 鐢ㄦ埛淇℃伅 */
@@ -137,7 +137,7 @@
/** 鑾峰彇鐢ㄦ埛鍒嗛〉鍒楄〃 */
export function getUserPage(params: SystemUserApi.PageParams) {
- return request.get<SystemUserApi.PageResult>('/system/user/page', { params });
+ return request.get<SystemUserApi.PageResult>("/system/user/page", { params });
}
/** 鑾峰彇鐢ㄦ埛璇︽儏 */
@@ -147,12 +147,12 @@
/** 鍒涘缓鐢ㄦ埛 */
export function createUser(data: SystemUserApi.User) {
- return request.post('/system/user/create', data);
+ return request.post("/system/user/create", data);
}
/** 鏇存柊鐢ㄦ埛 */
export function updateUser(data: SystemUserApi.User) {
- return request.put('/system/user/update', data);
+ return request.put("/system/user/update", data);
}
/** 鍒犻櫎鐢ㄦ埛 */
@@ -199,28 +199,28 @@
```vue
<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { SystemUserApi } from '#/api/system/user';
+import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+import type { SystemUserApi } from "#/api/system/user";
-import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
-import { getUserPage } from '#/api/system/user';
+import { useVbenVxeGrid, TableAction, ACTION_ICON } from "#/adapter/vxe-table";
+import { getUserPage } from "#/api/system/user";
// 琛ㄦ牸鍒楅厤缃�
-const columns: VxeTableGridOptions['columns'] = [
- { type: 'checkbox', width: 50 },
- { field: 'username', title: '鐢ㄦ埛鍚�', width: 120 },
- { field: 'nickname', title: '鏄电О', width: 120 },
+const columns: VxeTableGridOptions["columns"] = [
+ { type: "checkbox", width: 50 },
+ { field: "username", title: "鐢ㄦ埛鍚�", width: 120 },
+ { field: "nickname", title: "鏄电О", width: 120 },
{
- field: 'status',
- title: '鐘舵��',
+ field: "status",
+ title: "鐘舵��",
width: 100,
cellRender: {
- name: 'CellDict',
- props: { type: 'common_status' },
+ name: "CellDict",
+ props: { type: "common_status" },
},
},
- { field: 'createTime', title: '鍒涘缓鏃堕棿', width: 180 },
- { field: 'action', title: '鎿嶄綔', width: 200, slots: { default: 'actions' } },
+ { field: "createTime", title: "鍒涘缓鏃堕棿", width: 180 },
+ { field: "action", title: "鎿嶄綔", width: 200, slots: { default: "actions" } },
];
// 琛ㄦ牸瀹炰緥
@@ -228,23 +228,23 @@
formOptions: {
schema: [
{
- fieldName: 'username',
- label: '鐢ㄦ埛鍚�',
- component: 'Input',
+ fieldName: "username",
+ label: "鐢ㄦ埛鍚�",
+ component: "Input",
},
{
- fieldName: 'status',
- label: '鐘舵��',
- component: 'Select',
+ fieldName: "status",
+ label: "鐘舵��",
+ component: "Select",
componentProps: {
- options: getDictOptions('common_status'),
+ options: getDictOptions("common_status"),
},
},
],
},
gridOptions: {
columns,
- height: 'auto',
+ height: "auto",
keepSource: true,
proxyConfig: {
ajax: {
@@ -258,7 +258,7 @@
},
},
rowConfig: {
- keyField: 'id',
+ keyField: "id",
isHover: true,
},
} as VxeTableGridOptions<SystemUserApi.User>,
@@ -302,22 +302,22 @@
#### 琛ㄦ牸鍒楁覆鏌撳櫒
-| 娓叉煋鍣� | 鐢ㄩ�� | 绀轰緥 |
-|--------|------|------|
-| `CellImage` | 鍗曞浘灞曠ず | `cellRender: { name: 'CellImage' }` |
-| `CellImages` | 澶氬浘灞曠ず | `cellRender: { name: 'CellImages' }` |
-| `CellLink` | 閾炬帴鎸夐挳 | `cellRender: { name: 'CellLink', props: { text: '鏌ョ湅' } }` |
-| `CellTag` | 鏍囩灞曠ず | `cellRender: { name: 'CellTag', props: { color: 'blue' } }` |
-| `CellDict` | 瀛楀吀鏍囩 | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` |
-| `CellSwitch` | 寮�鍏冲垏鎹� | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }` |
+| 娓叉煋鍣� | 鐢ㄩ�� | 绀轰緥 |
+| --------------- | -------- | -------------------------------------------------------------------- |
+| `CellImage` | 鍗曞浘灞曠ず | `cellRender: { name: 'CellImage' }` |
+| `CellImages` | 澶氬浘灞曠ず | `cellRender: { name: 'CellImages' }` |
+| `CellLink` | 閾炬帴鎸夐挳 | `cellRender: { name: 'CellLink', props: { text: '鏌ョ湅' } }` |
+| `CellTag` | 鏍囩灞曠ず | `cellRender: { name: 'CellTag', props: { color: 'blue' } }` |
+| `CellDict` | 瀛楀吀鏍囩 | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` |
+| `CellSwitch` | 寮�鍏冲垏鎹� | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }` |
| `CellOperation` | 鎿嶄綔鎸夐挳 | `cellRender: { name: 'CellOperation', options: ['edit', 'delete'] }` |
### 2.2 琛ㄥ崟寮圭獥 (useVbenModal)
```vue
<script lang="ts" setup>
-import { useVbenModal } from '#/packages/effects/common-ui/src';
-import Form from './modules/form.vue';
+import { useVbenModal } from "#/packages/effects/common-ui/src";
+import Form from "./modules/form.vue";
// 寮圭獥瀹炰緥
const [FormModal, formModalApi] = useVbenModal({
@@ -346,14 +346,14 @@
```vue
<!-- modules/form.vue -->
<script lang="ts" setup>
-import type { SystemUserApi } from '#/api/system/user';
+import type { SystemUserApi } from "#/api/system/user";
-import { computed, ref } from 'vue';
-import { useVbenModal } from '#/packages/effects/common-ui/src';
-import { useVbenForm } from '#/adapter/form';
-import { createUser, updateUser } from '#/api/system/user';
+import { computed, ref } from "vue";
+import { useVbenModal } from "#/packages/effects/common-ui/src";
+import { useVbenForm } from "#/adapter/form";
+import { createUser, updateUser } from "#/api/system/user";
-const emit = defineEmits(['success']);
+const emit = defineEmits(["success"]);
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
@@ -363,7 +363,7 @@
} else {
await createUser(values);
}
- emit('success');
+ emit("success");
modalApi.close();
},
});
@@ -373,27 +373,27 @@
const [Form, formApi] = useVbenForm({
schema: [
{
- fieldName: 'username',
- label: '鐢ㄦ埛鍚�',
- rules: 'required',
- component: 'Input',
+ fieldName: "username",
+ label: "鐢ㄦ埛鍚�",
+ rules: "required",
+ component: "Input",
},
{
- fieldName: 'nickname',
- label: '鏄电О',
- rules: 'required',
- component: 'Input',
+ fieldName: "nickname",
+ label: "鏄电О",
+ rules: "required",
+ component: "Input",
},
{
- fieldName: 'email',
- label: '閭',
- rules: 'email',
- component: 'Input',
+ fieldName: "email",
+ label: "閭",
+ rules: "email",
+ component: "Input",
},
{
- fieldName: 'deptId',
- label: '閮ㄩ棬',
- component: 'TreeSelect',
+ fieldName: "deptId",
+ label: "閮ㄩ棬",
+ component: "TreeSelect",
},
],
});
@@ -422,7 +422,7 @@
```vue
<script lang="ts" setup>
-import DictTag from '#/components/dict-tag/dict-tag.vue';
+import DictTag from "#/components/dict-tag/dict-tag.vue";
</script>
<template>
@@ -436,7 +436,7 @@
```vue
<script lang="ts" setup>
-import DictSelect from '#/components/form-create/components/dict-select.vue';
+import DictSelect from "#/components/form-create/components/dict-select.vue";
</script>
<template>
@@ -462,16 +462,20 @@
#### 瀛楀吀宸ュ叿鍑芥暟
```typescript
-import { getDictOptions, getDictLabel, getDictObj } from '#/packages/effects/hooks/src';
+import {
+ getDictOptions,
+ getDictLabel,
+ getDictObj,
+} from "#/packages/effects/hooks/src";
// 鑾峰彇瀛楀吀閫夐」鍒楄〃锛堢敤浜� Select/Radio 绛夌粍浠讹級
-const options = getDictOptions('common_status', 'number');
+const options = getDictOptions("common_status", "number");
// 鑾峰彇瀛楀吀鏍囩鏂囨湰
-const label = getDictLabel('common_status', 1); // "寮�鍚�"
+const label = getDictLabel("common_status", 1); // "寮�鍚�"
// 鑾峰彇瀛楀吀瀹屾暣瀵硅薄
-const dictObj = getDictObj('common_status', 1);
+const dictObj = getDictObj("common_status", 1);
// { label: '寮�鍚�', value: 1, colorType: 'success', cssClass: '' }
```
@@ -481,7 +485,7 @@
```vue
<script lang="ts" setup>
-import { TableAction, ACTION_ICON } from '#/adapter/vxe-table';
+import { TableAction, ACTION_ICON } from "#/adapter/vxe-table";
</script>
<template>
@@ -523,9 +527,9 @@
```vue
<script lang="ts" setup>
-import ImageUpload from '#/components/upload/image-upload.vue';
+import ImageUpload from "#/components/upload/image-upload.vue";
-const imageUrl = ref('');
+const imageUrl = ref("");
const imageList = ref<string[]>([]);
</script>
@@ -549,7 +553,7 @@
```vue
<script lang="ts" setup>
-import FileUpload from '#/components/upload/file-upload.vue';
+import FileUpload from "#/components/upload/file-upload.vue";
const fileList = ref<string[]>([]);
</script>
@@ -570,26 +574,26 @@
```vue
<script lang="ts" setup>
-import Description from '#/components/description/description.vue';
+import Description from "#/components/description/description.vue";
const data = {
- username: 'admin',
- nickname: '绠$悊鍛�',
- email: 'admin@example.com',
+ username: "admin",
+ nickname: "绠$悊鍛�",
+ email: "admin@example.com",
status: 0,
- createTime: '2024-01-01 12:00:00',
+ createTime: "2024-01-01 12:00:00",
};
const schema = [
- { field: 'username', label: '鐢ㄦ埛鍚�' },
- { field: 'nickname', label: '鏄电О' },
- { field: 'email', label: '閭' },
+ { field: "username", label: "鐢ㄦ埛鍚�" },
+ { field: "nickname", label: "鏄电О" },
+ { field: "email", label: "閭" },
{
- field: 'status',
- label: '鐘舵��',
- render: (value) => h(DictTag, { type: 'common_status', value }),
+ field: "status",
+ label: "鐘舵��",
+ render: (value) => h(DictTag, { type: "common_status", value }),
},
- { field: 'createTime', label: '鍒涘缓鏃堕棿' },
+ { field: "createTime", label: "鍒涘缓鏃堕棿" },
];
</script>
@@ -608,16 +612,16 @@
#### 甯哥敤 Store
-| Store | 鐢ㄩ�� | 瀵煎叆璺緞 |
-|-------|------|----------|
-| `useUserStore` | 鐢ㄦ埛淇℃伅 | `#/packages/stores/src` |
+| Store | 鐢ㄩ�� | 瀵煎叆璺緞 |
+| ---------------- | ---------- | ----------------------- |
+| `useUserStore` | 鐢ㄦ埛淇℃伅 | `#/packages/stores/src` |
| `useAccessStore` | 鏉冮檺/Token | `#/packages/stores/src` |
-| `useDictStore` | 鏁版嵁瀛楀吀 | `#/packages/stores/src` |
+| `useDictStore` | 鏁版嵁瀛楀吀 | `#/packages/stores/src` |
#### 绀轰緥
```typescript
-import { useUserStore, useAccessStore } from '#/packages/stores/src';
+import { useUserStore, useAccessStore } from "#/packages/stores/src";
const userStore = useUserStore();
const accessStore = useAccessStore();
@@ -627,7 +631,7 @@
// 妫�鏌ユ潈闄�
const { hasAccessByCodes } = useAccess();
-const canEdit = hasAccessByCodes(['system:user:update']);
+const canEdit = hasAccessByCodes(["system:user:update"]);
```
---
@@ -638,7 +642,7 @@
```vue
<script lang="ts" setup>
-import { useAccess } from '#/packages/effects/access/src';
+import { useAccess } from "#/packages/effects/access/src";
const { hasAccessByCodes } = useAccess();
</script>
@@ -686,12 +690,12 @@
```vue
<script lang="ts" setup>
-import { $t } from '#/locales';
+import { $t } from "#/locales";
</script>
<template>
- <Button>{{ $t('common.save') }}</Button>
- <span>{{ $t('ui.actionMessage.deleteSuccess', ['鐢ㄦ埛']) }}</span>
+ <Button>{{ $t("common.save") }}</Button>
+ <span>{{ $t("ui.actionMessage.deleteSuccess", ["鐢ㄦ埛"]) }}</span>
</template>
```
@@ -721,7 +725,7 @@
### 6.1 usePagination - 鍒嗛〉
```typescript
-import { usePagination } from '#/packages/effects/hooks/src';
+import { usePagination } from "#/packages/effects/hooks/src";
const { page, pageSize, total, setPage, setTotal } = usePagination();
```
@@ -729,7 +733,7 @@
### 6.2 useTabs - 鏍囩椤垫搷浣�
```typescript
-import { useTabs } from '#/packages/effects/hooks/src';
+import { useTabs } from "#/packages/effects/hooks/src";
const { closeOtherTabs, closeTab, refreshTab } = useTabs();
@@ -743,13 +747,13 @@
### 6.3 useWatermark - 姘村嵃
```typescript
-import { useWatermark } from '#/packages/effects/hooks/src';
+import { useWatermark } from "#/packages/effects/hooks/src";
const { updateWatermark, destroyWatermark } = useWatermark();
// 璁剧疆姘村嵃
await updateWatermark({
- content: '鐢ㄦ埛鍚�',
+ content: "鐢ㄦ埛鍚�",
});
// 閿�姣佹按鍗�
@@ -764,20 +768,20 @@
```typescript
import {
- formatDateTime, // 鏃ユ湡鏍煎紡鍖�
- formatDate, // 鏃ユ湡鏍煎紡鍖栵紙涓嶅惈鏃堕棿锛�
+ formatDateTime, // 鏃ユ湡鏍煎紡鍖�
+ formatDate, // 鏃ユ湡鏍煎紡鍖栵紙涓嶅惈鏃堕棿锛�
downloadFileFromBlobPart, // 鏂囦欢涓嬭浇
- isEmpty, // 鍒ょ┖
- isFunction, // 鍒ゆ柇鍑芥暟
- isString, // 鍒ゆ柇瀛楃涓�
-} from '#/packages/utils/src';
+ isEmpty, // 鍒ょ┖
+ isFunction, // 鍒ゆ柇鍑芥暟
+ isString, // 鍒ゆ柇瀛楃涓�
+} from "#/packages/utils/src";
// 鏃ユ湡鏍煎紡鍖�
-formatDateTime('2024-01-01T12:00:00'); // "2024-01-01 12:00:00"
+formatDateTime("2024-01-01T12:00:00"); // "2024-01-01 12:00:00"
// 鏂囦欢涓嬭浇
downloadFileFromBlobPart({
- fileName: '鐢ㄦ埛鍒楄〃.xlsx',
+ fileName: "鐢ㄦ埛鍒楄〃.xlsx",
source: blobData,
});
@@ -791,10 +795,10 @@
```typescript
import {
- erpNumberFormatter, // 鏁板瓧鏍煎紡鍖�
- fenToYuan, // 鍒嗚浆鍏�
- formatFileSize, // 鏂囦欢澶у皬鏍煎紡鍖�
-} from '#/packages/utils/src';
+ erpNumberFormatter, // 鏁板瓧鏍煎紡鍖�
+ fenToYuan, // 鍒嗚浆鍏�
+ formatFileSize, // 鏂囦欢澶у皬鏍煎紡鍖�
+} from "#/packages/utils/src";
// 淇濈暀涓や綅灏忔暟
erpNumberFormatter(1234.567, 2); // "1,234.57"
@@ -812,28 +816,28 @@
```vue
<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { SystemUserApi } from '#/api/system/user';
+import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+import type { SystemUserApi } from "#/api/system/user";
-import { ref } from 'vue';
+import { ref } from "vue";
-import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src';
-import { getDictOptions } from '#/packages/effects/hooks/src';
-import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src';
+import { confirm, Page, useVbenModal } from "#/packages/effects/common-ui/src";
+import { getDictOptions } from "#/packages/effects/hooks/src";
+import { downloadFileFromBlobPart, isEmpty } from "#/packages/utils/src";
-import { message } from 'ant-design-vue';
+import { message } from "ant-design-vue";
-import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
+import { useVbenVxeGrid, TableAction, ACTION_ICON } from "#/adapter/vxe-table";
import {
getUserPage,
deleteUser,
createUser,
updateUser,
exportUser,
-} from '#/api/system/user';
-import { $t } from '#/locales';
+} from "#/api/system/user";
+import { $t } from "#/locales";
-import Form from './modules/form.vue';
+import Form from "./modules/form.vue";
// ========== 寮圭獥瀹氫箟 ==========
const [FormModal, formModalApi] = useVbenModal({
@@ -848,37 +852,42 @@
formOptions: {
schema: [
{
- fieldName: 'username',
- label: '鐢ㄦ埛鍚�',
- component: 'Input',
- componentProps: { placeholder: '璇疯緭鍏ョ敤鎴峰悕' },
+ fieldName: "username",
+ label: "鐢ㄦ埛鍚�",
+ component: "Input",
+ componentProps: { placeholder: "璇疯緭鍏ョ敤鎴峰悕" },
},
{
- fieldName: 'status',
- label: '鐘舵��',
- component: 'Select',
+ fieldName: "status",
+ label: "鐘舵��",
+ component: "Select",
componentProps: {
- options: getDictOptions('common_status', 'number'),
- placeholder: '璇烽�夋嫨鐘舵��',
+ options: getDictOptions("common_status", "number"),
+ placeholder: "璇烽�夋嫨鐘舵��",
},
},
],
},
gridOptions: {
columns: [
- { type: 'checkbox', width: 50 },
- { field: 'username', title: '鐢ㄦ埛鍚�', width: 120 },
- { field: 'nickname', title: '鏄电О', width: 120 },
+ { type: "checkbox", width: 50 },
+ { field: "username", title: "鐢ㄦ埛鍚�", width: 120 },
+ { field: "nickname", title: "鏄电О", width: 120 },
{
- field: 'status',
- title: '鐘舵��',
+ field: "status",
+ title: "鐘舵��",
width: 100,
- cellRender: { name: 'CellDict', props: { type: 'common_status' } },
+ cellRender: { name: "CellDict", props: { type: "common_status" } },
},
- { field: 'createTime', title: '鍒涘缓鏃堕棿', width: 180 },
- { field: 'action', title: '鎿嶄綔', width: 180, slots: { default: 'actions' } },
+ { field: "createTime", title: "鍒涘缓鏃堕棿", width: 180 },
+ {
+ field: "action",
+ title: "鎿嶄綔",
+ width: 180,
+ slots: { default: "actions" },
+ },
],
- height: 'auto',
+ height: "auto",
keepSource: true,
proxyConfig: {
ajax: {
@@ -891,7 +900,7 @@
},
},
},
- rowConfig: { keyField: 'id', isHover: true },
+ rowConfig: { keyField: "id", isHover: true },
toolbarConfig: { refresh: true, search: true },
} as VxeTableGridOptions<SystemUserApi.User>,
gridEvents: {
@@ -911,7 +920,7 @@
async function handleExport() {
const data = await exportUser(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '鐢ㄦ埛.xls', source: data });
+ downloadFileFromBlobPart({ fileName: "鐢ㄦ埛.xls", source: data });
}
function handleCreate() {
@@ -924,12 +933,12 @@
async function handleDelete(row: SystemUserApi.User) {
await deleteUser(row.id!);
- message.success($t('ui.actionMessage.deleteSuccess', [row.username]));
+ message.success($t("ui.actionMessage.deleteSuccess", [row.username]));
handleRefresh();
}
async function handleDeleteBatch() {
- await confirm($t('ui.actionMessage.deleteBatchConfirm'));
+ await confirm($t("ui.actionMessage.deleteBatchConfirm"));
// ... 鎵归噺鍒犻櫎閫昏緫
}
</script>
diff --git a/mdm_frontend_change.md b/mdm_frontend_change.md
index 3e9c175..5db78df 100644
--- a/mdm_frontend_change.md
+++ b/mdm_frontend_change.md
@@ -1,5 +1,7 @@
# 鐗╂枡涓绘暟鎹ā鍧� (MDM) 鍓嶇鍙樻洿鏂囨。
+> **鍩哄噯鏂囨。**锛氭湰鏂囨。涓� MDM 妯″潡鐨�**涓氬姟涓� API 鍙樻洿鍩哄噯**銆傞〉闈㈠疄鐜版柟寮忥紙缁勪欢銆佽〃鏍笺�佽〃鍗曘�佺洰褰曠粨鏋勶級椤婚伒寰� [`FRONTEND_DEVELOPMENT.md`](./FRONTEND_DEVELOPMENT.md)锛屾湰鏂囨。涓殑 Element Plus 绀轰緥浠呮弿杩颁笟鍔″瓧娈典笌浜や簰锛屼笉鍙洿鎺ョ収鎼� UI 妗嗘灦銆�
+
## 涓�銆佹柊澧為〉闈�
### 1.1 鐗╂枡涓绘暟鎹鐞�
diff --git a/src/api/mes/pd/archive/index.ts b/src/api/mes/pd/archive/index.ts
new file mode 100644
index 0000000..33c6efb
--- /dev/null
+++ b/src/api/mes/pd/archive/index.ts
@@ -0,0 +1,62 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdArchiveApi {
+ /** 鏁板瓧鍖栬璁″綊妗e彴璐� */
+ export interface Archive {
+ id?: number;
+ code?: string;
+ projectId?: number;
+ projectCode?: string;
+ projectName?: string;
+ productCode?: string;
+ productName?: string;
+ version?: string;
+ archiveStatus?: number;
+ archiveTime?: number;
+ archiverId?: number;
+ archiverName?: string;
+ documentCount?: number;
+ bomCount?: number;
+ storagePath?: string;
+ remark?: string;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ code?: string;
+ projectCode?: string;
+ productCode?: string;
+ archiveStatus?: number;
+ }
+}
+
+export function getPdArchivePage(params: MesPdArchiveApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getArchivePage(params);
+ }
+ return requestClient.get<PageResult<MesPdArchiveApi.Archive>>(
+ '/mes/pd/archive/page',
+ { params },
+ );
+}
+
+export function getPdArchive(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getArchive(id);
+ }
+ return requestClient.get<MesPdArchiveApi.Archive>(
+ `/mes/pd/archive/get?id=${id}`,
+ );
+}
+
+export function exportPdArchive(params: MesPdArchiveApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return Promise.resolve(new Blob());
+ }
+ return requestClient.download('/mes/pd/archive/export-excel', { params });
+}
diff --git a/src/api/mes/pd/bom/index.ts b/src/api/mes/pd/bom/index.ts
new file mode 100644
index 0000000..9e4ad3f
--- /dev/null
+++ b/src/api/mes/pd/bom/index.ts
@@ -0,0 +1,59 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdBomApi {
+ /** 璁捐 BOM 琛� */
+ export interface BomLine {
+ id?: number;
+ projectId?: number;
+ itemId?: number;
+ itemCode?: string;
+ itemName?: string;
+ specification?: string;
+ unitMeasureName?: string;
+ quantity?: number;
+ remark?: string;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ projectId: number;
+ itemCode?: string;
+ itemName?: string;
+ }
+}
+
+export function getPdBomPage(params: MesPdBomApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getBomPage(params);
+ }
+ return requestClient.get<PageResult<MesPdBomApi.BomLine>>(
+ '/mes/pd/bom/page',
+ { params },
+ );
+}
+
+export function createPdBomLine(data: MesPdBomApi.BomLine) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.createBomLine(data);
+ }
+ return requestClient.post<number>('/mes/pd/bom/create', data);
+}
+
+export function updatePdBomLine(data: MesPdBomApi.BomLine) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.updateBomLine(data);
+ }
+ return requestClient.put('/mes/pd/bom/update', data);
+}
+
+export function deletePdBomLine(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.deleteBomLine(id);
+ }
+ return requestClient.delete(`/mes/pd/bom/delete?id=${id}`);
+}
diff --git a/src/api/mes/pd/document/index.ts b/src/api/mes/pd/document/index.ts
new file mode 100644
index 0000000..3a637e9
--- /dev/null
+++ b/src/api/mes/pd/document/index.ts
@@ -0,0 +1,71 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdDocumentApi {
+ /** 璁捐璧勬枡 */
+ export interface Document {
+ id?: number;
+ projectId?: number;
+ documentType?: number;
+ name?: string;
+ version?: string;
+ fileUrl?: string;
+ fileName?: string;
+ fileSize?: number;
+ uploaderId?: number;
+ uploaderName?: string;
+ remark?: string;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ projectId: number;
+ documentType?: number;
+ name?: string;
+ version?: string;
+ }
+}
+
+export function getPdDocumentPage(params: MesPdDocumentApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getDocumentPage(params);
+ }
+ return requestClient.get<PageResult<MesPdDocumentApi.Document>>(
+ '/mes/pd/document/page',
+ { params },
+ );
+}
+
+export function getPdDocument(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getDocument(id);
+ }
+ return requestClient.get<MesPdDocumentApi.Document>(
+ `/mes/pd/document/get?id=${id}`,
+ );
+}
+
+export function createPdDocument(data: MesPdDocumentApi.Document) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.createDocument(data);
+ }
+ return requestClient.post<number>('/mes/pd/document/create', data);
+}
+
+export function updatePdDocument(data: MesPdDocumentApi.Document) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.updateDocument(data);
+ }
+ return requestClient.put('/mes/pd/document/update', data);
+}
+
+export function deletePdDocument(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.deleteDocument(id);
+ }
+ return requestClient.delete(`/mes/pd/document/delete?id=${id}`);
+}
diff --git a/src/api/mes/pd/drawing-review/index.ts b/src/api/mes/pd/drawing-review/index.ts
new file mode 100644
index 0000000..3aa08c3
--- /dev/null
+++ b/src/api/mes/pd/drawing-review/index.ts
@@ -0,0 +1,74 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdDrawingReviewApi {
+ /** 鍥剧焊瀹℃牳 */
+ export interface DrawingReview {
+ id?: number;
+ code?: string;
+ projectId?: number;
+ projectCode?: string;
+ projectName?: string;
+ documentId?: number;
+ documentName?: string;
+ documentVersion?: string;
+ reviewerId?: number;
+ reviewerName?: string;
+ status?: number;
+ reviewComment?: string;
+ reviewTime?: number;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ code?: string;
+ projectId?: number;
+ status?: number;
+ reviewerId?: number;
+ }
+
+ export interface ReviewParams {
+ id: number;
+ status: number;
+ reviewComment?: string;
+ }
+}
+
+export function getPdDrawingReviewPage(
+ params: MesPdDrawingReviewApi.PageParams,
+) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getDrawingReviewPage(params);
+ }
+ return requestClient.get<PageResult<MesPdDrawingReviewApi.DrawingReview>>(
+ '/mes/pd/drawing-review/page',
+ { params },
+ );
+}
+
+export function getPdDrawingReview(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getDrawingReview(id);
+ }
+ return requestClient.get<MesPdDrawingReviewApi.DrawingReview>(
+ `/mes/pd/drawing-review/get?id=${id}`,
+ );
+}
+
+export function createPdDrawingReview(data: MesPdDrawingReviewApi.DrawingReview) {
+ if (isPdMockEnabled()) {
+ return Promise.resolve(1);
+ }
+ return requestClient.post<number>('/mes/pd/drawing-review/create', data);
+}
+
+export function reviewPdDrawing(data: MesPdDrawingReviewApi.ReviewParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.reviewDrawing(data);
+ }
+ return requestClient.put('/mes/pd/drawing-review/review', data);
+}
diff --git a/src/api/mes/pd/mock/enabled.ts b/src/api/mes/pd/mock/enabled.ts
new file mode 100644
index 0000000..d22db56
--- /dev/null
+++ b/src/api/mes/pd/mock/enabled.ts
@@ -0,0 +1,4 @@
+/** 鏄惁鍚敤浜у搧璁捐锛圥D锛夋ā鍧楁湰鍦� Mock */
+export function isPdMockEnabled(): boolean {
+ return import.meta.env.VITE_PD_MOCK === 'true';
+}
diff --git a/src/api/mes/pd/mock/seed.ts b/src/api/mes/pd/mock/seed.ts
new file mode 100644
index 0000000..f011b92
--- /dev/null
+++ b/src/api/mes/pd/mock/seed.ts
@@ -0,0 +1,386 @@
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+import type { MesPdWorkflowApi } from '#/api/mes/pd/workflow';
+
+import {
+ MesPdDocumentTypeEnum,
+ MesPdDrawingReviewStatusEnum,
+ MesPdProjectStatusEnum,
+ MesPdVersionChangeTypeEnum,
+} from '#/packages/constants/src';
+
+const DAY = 86_400_000;
+const now = Date.now();
+
+function ts(daysAgo = 0): number {
+ return now - daysAgo * DAY;
+}
+
+/** 璁捐浠诲姟锛氬悓涓�浜у搧鍙娆″彂璧凤紝姣忔瀵瑰簲涓�涓増鏈� */
+export const pdMockProjects: MesPdProjectApi.Project[] = [
+ {
+ id: 1,
+ code: 'DT-2026-0001',
+ name: '鏅鸿兘娓╂帶妯$粍 V1.0 璁捐',
+ productCode: 'PRD-TC-100',
+ productName: '鏅鸿兘娓╂帶妯$粍',
+ version: 'V1.0',
+ status: MesPdProjectStatusEnum.DESIGNING,
+ designerName: '寮犲伐',
+ reviewerName: '鏉庝富绠�',
+ planStartDate: ts(30),
+ planEndDate: ts(-15),
+ remark: '棣栨璁捐锛屼富鎺ф澘 + 浼犳劅鍣ㄤ竴浣撳寲',
+ createTime: ts(35),
+ },
+ {
+ id: 2,
+ code: 'DT-2026-0002',
+ name: '宸ヤ笟缃戝叧 V2.1 璁捐',
+ productCode: 'PRD-GW-200',
+ productName: '宸ヤ笟鐗╄仈缃戠綉鍏�',
+ version: 'V2.1',
+ status: MesPdProjectStatusEnum.REVIEWING,
+ designerName: '鐜嬪伐',
+ reviewerName: '璧电粡鐞�',
+ planStartDate: ts(20),
+ planEndDate: ts(-5),
+ remark: '閫氫俊鍗忚鍗囩骇锛屽澹崇粨鏋勮皟鏁�',
+ createTime: ts(25),
+ },
+ {
+ id: 3,
+ code: 'DT-2025-0012',
+ name: '澶氶�氶亾閲囬泦鏉� V1.2 璁捐',
+ productCode: 'PRD-SC-050',
+ productName: '澶氶�氶亾閲囬泦鏉�',
+ version: 'V1.2',
+ status: MesPdProjectStatusEnum.ARCHIVED,
+ designerName: '闄堝伐',
+ reviewerName: '鏉庝富绠�',
+ planStartDate: ts(90),
+ planEndDate: ts(45),
+ actualFinishDate: ts(40),
+ remark: '宸查噺浜у綊妗�',
+ createTime: ts(95),
+ },
+ {
+ id: 4,
+ code: 'DT-2026-0004',
+ name: '浼烘湇鐢垫満椹卞姩鍣� V0.9 璁捐',
+ productCode: 'PRD-MD-300',
+ productName: '浼烘湇鐢垫満椹卞姩鍣�',
+ version: 'V0.9',
+ status: MesPdProjectStatusEnum.DRAFT,
+ designerName: '鍒樺伐',
+ reviewerName: '璧电粡鐞�',
+ planStartDate: ts(5),
+ planEndDate: ts(-30),
+ createTime: ts(3),
+ },
+ {
+ id: 5,
+ code: 'DT-2024-0008',
+ name: '澶氶�氶亾閲囬泦鏉� V1.0 璁捐',
+ productCode: 'PRD-SC-050',
+ productName: '澶氶�氶亾閲囬泦鏉�',
+ version: 'V1.0',
+ status: MesPdProjectStatusEnum.ARCHIVED,
+ designerName: '闄堝伐',
+ reviewerName: '鏉庝富绠�',
+ planStartDate: ts(200),
+ planEndDate: ts(170),
+ actualFinishDate: ts(165),
+ remark: '鍒濅唬鐗堟湰褰掓。',
+ createTime: ts(205),
+ },
+ {
+ id: 6,
+ code: 'DT-2026-0006',
+ name: '鏅鸿兘娓╂帶妯$粍 V1.1 璁捐',
+ productCode: 'PRD-TC-100',
+ productName: '鏅鸿兘娓╂帶妯$粍',
+ version: 'V1.1',
+ status: MesPdProjectStatusEnum.APPROVED,
+ designerName: '寮犲伐',
+ reviewerName: '鏉庝富绠�',
+ planStartDate: ts(10),
+ planEndDate: ts(-20),
+ remark: '鏁g儹浼樺寲鏀圭増锛屽緟褰掓。',
+ createTime: ts(12),
+ },
+];
+
+export const pdMockArchives: MesPdArchiveApi.Archive[] = [
+ {
+ id: 1,
+ code: 'AR-2025-0012',
+ projectId: 3,
+ projectCode: 'DT-2025-0012',
+ projectName: '澶氶�氶亾閲囬泦鏉� V1.2 璁捐',
+ productCode: 'PRD-SC-050',
+ productName: '澶氶�氶亾閲囬泦鏉�',
+ version: 'V1.2',
+ archiveStatus: 1,
+ archiveTime: ts(38),
+ archiverName: '闄堝伐',
+ documentCount: 3,
+ bomCount: 1,
+ storagePath: '/archive/2026/PRD-SC-050/V1.2',
+ remark: 'V1.2 閲忎骇鐗堟湰瀹屾暣褰掓。',
+ createTime: ts(38),
+ },
+ {
+ id: 2,
+ code: 'AR-2024-0008',
+ projectId: 5,
+ projectCode: 'DT-2024-0008',
+ projectName: '澶氶�氶亾閲囬泦鏉� V1.0 璁捐',
+ productCode: 'PRD-SC-050',
+ productName: '澶氶�氶亾閲囬泦鏉�',
+ version: 'V1.0',
+ archiveStatus: 1,
+ archiveTime: ts(165),
+ archiverName: '闄堝伐',
+ documentCount: 4,
+ bomCount: 8,
+ storagePath: '/archive/2024/PRD-SC-050/V1.0',
+ remark: '鍒濅唬鐗堟湰褰掓。',
+ createTime: ts(165),
+ },
+];
+
+export const pdMockDocuments: MesPdDocumentApi.Document[] = [
+ {
+ id: 1,
+ projectId: 1,
+ documentType: MesPdDocumentTypeEnum.SCHEME,
+ name: '娓╂帶鏂规璇存槑',
+ version: 'V1.0',
+ fileName: 'scheme-v1.pdf',
+ fileUrl: 'https://example.com/scheme-v1.pdf',
+ uploaderName: '寮犲伐',
+ createTime: ts(25),
+ },
+ {
+ id: 2,
+ projectId: 1,
+ documentType: MesPdDocumentTypeEnum.DRAWING,
+ name: '涓绘帶鏉垮師鐞嗗浘',
+ version: 'V1.0',
+ fileName: 'main-sch.pdf',
+ fileUrl: 'https://example.com/main-sch.pdf',
+ uploaderName: '寮犲伐',
+ createTime: ts(22),
+ },
+ {
+ id: 3,
+ projectId: 2,
+ documentType: MesPdDocumentTypeEnum.DRAWING,
+ name: '缃戝叧澶栧3宸ョ▼鍥�',
+ version: 'V2.1',
+ fileName: 'gw-enclosure.dwg',
+ fileUrl: 'https://example.com/gw-enclosure.dwg',
+ uploaderName: '鐜嬪伐',
+ createTime: ts(12),
+ },
+ {
+ id: 4,
+ projectId: 3,
+ documentType: MesPdDocumentTypeEnum.OTHER,
+ name: '褰掓。妫�鏌ユ竻鍗�',
+ version: 'V1.2',
+ fileName: 'archive-checklist.xlsx',
+ uploaderName: '闄堝伐',
+ createTime: ts(39),
+ },
+ {
+ id: 5,
+ projectId: 6,
+ documentType: MesPdDocumentTypeEnum.DRAWING,
+ name: '鏁g儹浼樺寲 PCB 鍥�',
+ version: 'V1.1',
+ fileName: 'thermal-pcb-v1.1.pdf',
+ fileUrl: 'https://example.com/thermal-pcb.pdf',
+ uploaderName: '寮犲伐',
+ createTime: ts(8),
+ },
+ {
+ id: 6,
+ projectId: 1,
+ documentType: MesPdDocumentTypeEnum.BOM,
+ name: 'BOM 娓呭崟',
+ version: 'V1.0',
+ fileName: 'bom-v1.0.xlsx',
+ fileUrl: 'https://example.com/bom-v1.0.xlsx',
+ uploaderName: '寮犲伐',
+ createTime: ts(20),
+ },
+ {
+ id: 7,
+ projectId: 3,
+ documentType: MesPdDocumentTypeEnum.BOM,
+ name: 'BOM 娓呭崟',
+ version: 'V1.2',
+ fileName: 'bom-v1.2.xlsx',
+ fileUrl: 'https://example.com/bom-v1.2.xlsx',
+ uploaderName: '闄堝伐',
+ createTime: ts(48),
+ },
+];
+
+export const pdMockBomLines: MesPdBomApi.BomLine[] = [
+ {
+ id: 1,
+ projectId: 1,
+ itemCode: 'MAT-001',
+ itemName: 'STM32 涓绘帶鑺墖',
+ specification: 'LQFP-64',
+ unitMeasureName: 'PCS',
+ quantity: 1,
+ createTime: ts(20),
+ },
+ {
+ id: 2,
+ projectId: 1,
+ itemCode: 'MAT-018',
+ itemName: 'NTC 鐑晱鐢甸樆',
+ specification: '10K B3950',
+ unitMeasureName: 'PCS',
+ quantity: 2,
+ createTime: ts(20),
+ },
+ {
+ id: 3,
+ projectId: 3,
+ itemCode: 'MAT-102',
+ itemName: '杩愮畻鏀惧ぇ鍣�',
+ specification: 'SOP-8',
+ unitMeasureName: 'PCS',
+ quantity: 4,
+ createTime: ts(50),
+ },
+];
+
+export const pdMockDrawingReviews: MesPdDrawingReviewApi.DrawingReview[] = [
+ {
+ id: 1,
+ code: 'DR-2026-0001',
+ projectId: 2,
+ projectCode: 'DT-2026-0002',
+ projectName: '宸ヤ笟缃戝叧 V2.1 璁捐',
+ documentId: 3,
+ documentName: '缃戝叧澶栧3宸ョ▼鍥�',
+ documentVersion: 'V2.1',
+ reviewerName: '璧电粡鐞�',
+ status: MesPdDrawingReviewStatusEnum.PENDING,
+ createTime: ts(10),
+ },
+ {
+ id: 2,
+ code: 'DR-2026-0002',
+ projectId: 6,
+ projectCode: 'DT-2026-0006',
+ projectName: '鏅鸿兘娓╂帶妯$粍 V1.1 璁捐',
+ documentId: 5,
+ documentName: '鏁g儹浼樺寲 PCB 鍥�',
+ documentVersion: 'V1.1',
+ reviewerName: '鏉庝富绠�',
+ status: MesPdDrawingReviewStatusEnum.APPROVED,
+ reviewTime: ts(6),
+ createTime: ts(7),
+ },
+];
+
+export const pdMockVersionChanges: MesPdVersionChangeApi.VersionChange[] = [
+ {
+ id: 1,
+ code: 'VC-2025-0001',
+ projectId: 3,
+ projectCode: 'DT-2025-0012',
+ projectName: '澶氶�氶亾閲囬泦鏉� V1.2 璁捐',
+ changeType: MesPdVersionChangeTypeEnum.MINOR,
+ fromVersion: 'V1.0',
+ toVersion: 'V1.2',
+ applicantName: '闄堝伐',
+ changeReason: '閫氶亾鏁版墿灞曚笌绮惧害鎻愬崌',
+ changeContent: '鐢� 4 閫氶亾鍗囩骇涓� 8 閫氶亾锛屾洿鎹㈤珮绮惧害 ADC',
+ createTime: ts(92),
+ },
+ {
+ id: 2,
+ code: 'VC-2026-0001',
+ projectId: 6,
+ projectCode: 'DT-2026-0006',
+ projectName: '鏅鸿兘娓╂帶妯$粍 V1.1 璁捐',
+ changeType: MesPdVersionChangeTypeEnum.PATCH,
+ fromVersion: 'V1.0',
+ toVersion: 'V1.1',
+ applicantName: '寮犲伐',
+ changeReason: '浼樺寲鏁g儹甯冨眬',
+ changeContent: '璋冩暣 NTC 鎽嗘斁浣嶇疆锛屾洿鏂� PCB 涓濆嵃',
+ createTime: ts(8),
+ },
+];
+
+export const pdMockWorkflows: Record<number, MesPdWorkflowApi.WorkflowNode[]> = {
+ 1: [
+ { id: 11, projectId: 1, nodeName: '鍙戣捣浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(35) },
+ { id: 12, projectId: 1, nodeName: '鍙戝竷浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(34) },
+ { id: 13, projectId: 1, nodeName: '涓婁紶璧勬枡', operatorName: '寮犲伐', status: 2, remark: '杩涜涓�' },
+ { id: 14, projectId: 1, nodeName: '璧勬枡瀹℃牳', status: 0 },
+ { id: 15, projectId: 1, nodeName: '褰掓。', status: 0 },
+ ],
+ 2: [
+ { id: 21, projectId: 2, nodeName: '鍙戣捣浠诲姟', operatorName: '鐜嬪伐', status: 1, operateTime: ts(25) },
+ { id: 22, projectId: 2, nodeName: '鍙戝竷浠诲姟', operatorName: '鐜嬪伐', status: 1, operateTime: ts(24) },
+ { id: 23, projectId: 2, nodeName: '涓婁紶璧勬枡', operatorName: '鐜嬪伐', status: 1, operateTime: ts(12) },
+ { id: 24, projectId: 2, nodeName: '璧勬枡瀹℃牳', operatorName: '璧电粡鐞�', status: 2, remark: '寰呭鏍�' },
+ { id: 25, projectId: 2, nodeName: '褰掓。', status: 0 },
+ ],
+ 3: [
+ { id: 31, projectId: 3, nodeName: '鍙戣捣浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(95) },
+ { id: 32, projectId: 3, nodeName: '鍙戝竷浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(94) },
+ { id: 33, projectId: 3, nodeName: '涓婁紶璧勬枡', operatorName: '闄堝伐', status: 1, operateTime: ts(50) },
+ { id: 34, projectId: 3, nodeName: '璧勬枡瀹℃牳', operatorName: '鏉庝富绠�', status: 1, operateTime: ts(42) },
+ { id: 35, projectId: 3, nodeName: '褰掓。', operatorName: '闄堝伐', status: 1, operateTime: ts(38) },
+ ],
+ 4: [
+ { id: 41, projectId: 4, nodeName: '鍙戣捣浠诲姟', operatorName: '鍒樺伐', status: 1, operateTime: ts(3) },
+ { id: 42, projectId: 4, nodeName: '鍙戝竷浠诲姟', status: 0 },
+ { id: 43, projectId: 4, nodeName: '涓婁紶璧勬枡', status: 0 },
+ { id: 44, projectId: 4, nodeName: '璧勬枡瀹℃牳', status: 0 },
+ { id: 45, projectId: 4, nodeName: '褰掓。', status: 0 },
+ ],
+ 5: [
+ { id: 51, projectId: 5, nodeName: '鍙戣捣浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(205) },
+ { id: 52, projectId: 5, nodeName: '鍙戝竷浠诲姟', operatorName: '闄堝伐', status: 1, operateTime: ts(200) },
+ { id: 53, projectId: 5, nodeName: '涓婁紶璧勬枡', operatorName: '闄堝伐', status: 1, operateTime: ts(170) },
+ { id: 54, projectId: 5, nodeName: '璧勬枡瀹℃牳', operatorName: '鏉庝富绠�', status: 1, operateTime: ts(168) },
+ { id: 55, projectId: 5, nodeName: '褰掓。', operatorName: '闄堝伐', status: 1, operateTime: ts(165) },
+ ],
+ 6: [
+ { id: 61, projectId: 6, nodeName: '鍙戣捣浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(12) },
+ { id: 62, projectId: 6, nodeName: '鍙戝竷浠诲姟', operatorName: '寮犲伐', status: 1, operateTime: ts(11) },
+ { id: 63, projectId: 6, nodeName: '涓婁紶璧勬枡', operatorName: '寮犲伐', status: 1, operateTime: ts(8) },
+ { id: 64, projectId: 6, nodeName: '璧勬枡瀹℃牳', operatorName: '鏉庝富绠�', status: 1, operateTime: ts(6) },
+ { id: 65, projectId: 6, nodeName: '褰掓。', status: 2, remark: '寰呭綊妗�' },
+ ],
+};
+
+export const pdMockSeq = {
+ project: 7,
+ archive: 3,
+ task: 4,
+ document: 8,
+ bom: 4,
+ drawingReview: 3,
+ versionChange: 3,
+};
+
+/** @deprecated 璁捐浠诲姟鍗抽」鐩紝瀛愪换鍔″垎鍙戝凡搴熷純 */
+export const pdMockTasks = [];
diff --git a/src/api/mes/pd/mock/store.ts b/src/api/mes/pd/mock/store.ts
new file mode 100644
index 0000000..0519016
--- /dev/null
+++ b/src/api/mes/pd/mock/store.ts
@@ -0,0 +1,554 @@
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+import type { MesPdTaskApi } from '#/api/mes/pd/task';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+import type { MesPdWorkflowApi } from '#/api/mes/pd/workflow';
+
+import { MesPdDrawingReviewStatusEnum, MesPdDocumentTypeEnum, MesPdProjectStatusEnum } from '#/packages/constants/src';
+
+import {
+ pdMockArchives,
+ pdMockBomLines,
+ pdMockDocuments,
+ pdMockDrawingReviews,
+ pdMockProjects,
+ pdMockSeq,
+ pdMockTasks,
+ pdMockVersionChanges,
+ pdMockWorkflows,
+} from './seed';
+import { mockDelay, mockNextCode, mockPage } from './utils';
+
+function matchLike(value: unknown, keyword?: string): boolean {
+ if (!keyword) {
+ return true;
+ }
+ return String(value ?? '')
+ .toLowerCase()
+ .includes(String(keyword).toLowerCase());
+}
+
+function syncProjectRef(project: MesPdProjectApi.Project) {
+ const ref = {
+ projectCode: project.code,
+ projectName: project.name,
+ productCode: project.productCode,
+ productName: project.productName,
+ version: project.version,
+ };
+ pdMockTasks.forEach((task) => {
+ if (task.projectId === project.id) {
+ Object.assign(task, ref);
+ }
+ });
+ pdMockDrawingReviews.forEach((item) => {
+ if (item.projectId === project.id) {
+ Object.assign(item, ref);
+ }
+ });
+ pdMockVersionChanges.forEach((item) => {
+ if (item.projectId === project.id) {
+ Object.assign(item, ref);
+ }
+ });
+}
+
+const WORKFLOW_STEPS = [
+ '鍙戣捣浠诲姟',
+ '鍙戝竷浠诲姟',
+ '涓婁紶璧勬枡',
+ '璧勬枡瀹℃牳',
+ '褰掓。',
+] as const;
+
+function initWorkflow(projectId: number, designerName?: string) {
+ pdMockWorkflows[projectId] = WORKFLOW_STEPS.map((nodeName, index) => ({
+ id: projectId * 10 + index,
+ projectId,
+ nodeName,
+ operatorName: index === 0 ? designerName : undefined,
+ status: index === 0 ? 1 : 0,
+ operateTime: index === 0 ? Date.now() : undefined,
+ }));
+}
+
+function advanceWorkflow(projectId: number, completedStep: string, operatorName?: string) {
+ const nodes = pdMockWorkflows[projectId];
+ if (!nodes) {
+ return;
+ }
+ const completedIndex = nodes.findIndex((n) => n.nodeName === completedStep);
+ if (completedIndex >= 0) {
+ nodes[completedIndex]!.status = 1;
+ nodes[completedIndex]!.operateTime = Date.now();
+ if (operatorName) {
+ nodes[completedIndex]!.operatorName = operatorName;
+ }
+ }
+ const next = nodes[completedIndex + 1];
+ if (next) {
+ next.status = 2;
+ if (operatorName) {
+ next.operatorName = operatorName;
+ }
+ }
+}
+
+function createDocumentReviews(projectId: number, project: MesPdProjectApi.Project) {
+ const docs = pdMockDocuments.filter((d) => d.projectId === projectId);
+ for (const doc of docs) {
+ const exists = pdMockDrawingReviews.some((r) => r.documentId === doc.id);
+ if (exists) {
+ continue;
+ }
+ const reviewId = pdMockSeq.drawingReview++;
+ pdMockDrawingReviews.push({
+ id: reviewId,
+ code: mockNextCode('DR-2026', reviewId),
+ projectId,
+ projectCode: project.code,
+ projectName: project.name,
+ documentId: doc.id,
+ documentName: doc.name,
+ documentVersion: doc.version,
+ reviewerName: project.reviewerName,
+ status: MesPdDrawingReviewStatusEnum.PENDING,
+ createTime: Date.now(),
+ });
+ }
+}
+
+export const pdMockStore = {
+ // ---------- 璁捐椤圭洰 ----------
+ getProjectPage(params: MesPdProjectApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockProjects, params, (item) => {
+ return (
+ matchLike(item.code, params.code) &&
+ matchLike(item.name, params.name) &&
+ matchLike(item.productCode, params.productCode) &&
+ (params.status === undefined || item.status === params.status)
+ );
+ }),
+ );
+ },
+
+ getProject(id: number) {
+ const project = pdMockProjects.find((item) => item.id === id);
+ if (!project) {
+ return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+ }
+ return mockDelay({ ...project });
+ },
+
+ createProject(data: MesPdProjectApi.Project) {
+ const id = pdMockSeq.project++;
+ const project: MesPdProjectApi.Project = {
+ ...data,
+ id,
+ code: data.code || mockNextCode('DT-2026', id),
+ status: MesPdProjectStatusEnum.DRAFT,
+ createTime: Date.now(),
+ };
+ pdMockProjects.unshift(project);
+ initWorkflow(id, project.designerName);
+ return mockDelay(id);
+ },
+
+ updateProject(data: MesPdProjectApi.Project) {
+ const index = pdMockProjects.findIndex((item) => item.id === data.id);
+ if (index < 0) {
+ return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+ }
+ pdMockProjects[index] = { ...pdMockProjects[index], ...data };
+ syncProjectRef(pdMockProjects[index]!);
+ return mockDelay(undefined);
+ },
+
+ deleteProject(id: number) {
+ const index = pdMockProjects.findIndex((item) => item.id === id);
+ if (index >= 0) {
+ pdMockProjects.splice(index, 1);
+ }
+ return mockDelay(undefined);
+ },
+
+ publishProject(id: number) {
+ const project = pdMockProjects.find((item) => item.id === id);
+ if (!project || project.status !== MesPdProjectStatusEnum.DRAFT) {
+ return Promise.reject(new Error('浠呰崏绋跨姸鎬佸彲鍙戝竷'));
+ }
+ project.status = MesPdProjectStatusEnum.DESIGNING;
+ advanceWorkflow(id, '鍙戣捣浠诲姟', project.designerName);
+ advanceWorkflow(id, '鍙戝竷浠诲姟', project.designerName);
+ return mockDelay(undefined);
+ },
+
+ submitProjectReview(id: number) {
+ const project = pdMockProjects.find((item) => item.id === id);
+ if (!project || project.status !== MesPdProjectStatusEnum.DESIGNING) {
+ return Promise.reject(new Error('璇峰厛鍙戝竷浠诲姟骞朵笂浼犺祫鏂�'));
+ }
+ const docCount = pdMockDocuments.filter((d) => d.projectId === id).length;
+ if (docCount === 0) {
+ return Promise.reject(new Error('璇峰厛涓婁紶璁捐璧勬枡'));
+ }
+ project.status = MesPdProjectStatusEnum.REVIEWING;
+ createDocumentReviews(id, project);
+ advanceWorkflow(id, '涓婁紶璧勬枡', project.designerName);
+ advanceWorkflow(id, '璧勬枡瀹℃牳', project.reviewerName);
+ return mockDelay(undefined);
+ },
+
+ approveProject(id: number) {
+ const project = pdMockProjects.find((item) => item.id === id);
+ if (!project || project.status !== MesPdProjectStatusEnum.REVIEWING) {
+ return Promise.reject(new Error('褰撳墠鐘舵�佷笉鍙鏍搁�氳繃'));
+ }
+ const pending = pdMockDrawingReviews.filter(
+ (r) =>
+ r.projectId === id &&
+ r.status === MesPdDrawingReviewStatusEnum.PENDING,
+ );
+ if (pending.length > 0) {
+ return Promise.reject(new Error('灏氭湁璧勬枡鏈畬鎴愬鏍�'));
+ }
+ project.status = MesPdProjectStatusEnum.APPROVED;
+ const nodes = pdMockWorkflows[id];
+ const reviewNode = nodes?.find((n) => n.nodeName === '璧勬枡瀹℃牳');
+ if (reviewNode) {
+ reviewNode.status = 1;
+ reviewNode.operateTime = Date.now();
+ }
+ const archiveNode = nodes?.find((n) => n.nodeName === '褰掓。');
+ if (archiveNode) {
+ archiveNode.status = 2;
+ }
+ return mockDelay(undefined);
+ },
+
+ archiveProject(id: number) {
+ const project = pdMockProjects.find((item) => item.id === id);
+ if (!project) {
+ return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+ }
+ if (project.status !== MesPdProjectStatusEnum.APPROVED) {
+ return Promise.reject(new Error('浠呭鏍搁�氳繃鐨勪换鍔″彲褰掓。'));
+ }
+ project.status = MesPdProjectStatusEnum.ARCHIVED;
+ project.actualFinishDate = Date.now();
+ const archiveId = pdMockSeq.archive++;
+ pdMockArchives.unshift({
+ id: archiveId,
+ code: mockNextCode('AR-2026', archiveId),
+ projectId: project.id,
+ projectCode: project.code,
+ projectName: project.name,
+ productCode: project.productCode,
+ productName: project.productName,
+ version: project.version,
+ archiveStatus: 1,
+ archiveTime: Date.now(),
+ archiverName: project.designerName,
+ documentCount: pdMockDocuments.filter((d) => d.projectId === id).length,
+ bomCount: pdMockDocuments.filter(
+ (d) =>
+ d.projectId === id &&
+ d.documentType === MesPdDocumentTypeEnum.BOM,
+ ).length,
+ storagePath: `/archive/2026/${project.productCode}/${project.version}`,
+ createTime: Date.now(),
+ });
+ advanceWorkflow(id, '褰掓。', project.designerName);
+ const archiveNode = pdMockWorkflows[id]?.find((n) => n.nodeName === '褰掓。');
+ if (archiveNode) {
+ archiveNode.status = 1;
+ archiveNode.operateTime = Date.now();
+ }
+ return mockDelay(undefined);
+ },
+
+ // ---------- 褰掓。鍙拌处 ----------
+ getArchivePage(params: MesPdArchiveApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockArchives, params, (item) => {
+ return (
+ matchLike(item.code, params.code) &&
+ matchLike(item.projectCode, params.projectCode) &&
+ matchLike(item.productCode, params.productCode) &&
+ (params.archiveStatus === undefined ||
+ item.archiveStatus === params.archiveStatus)
+ );
+ }),
+ );
+ },
+
+ getArchive(id: number) {
+ const archive = pdMockArchives.find((item) => item.id === id);
+ if (!archive) {
+ return Promise.reject(new Error('褰掓。璁板綍涓嶅瓨鍦�'));
+ }
+ return mockDelay({ ...archive });
+ },
+
+ // ---------- 浠诲姟 ----------
+ getTaskPage(params: MesPdTaskApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockTasks, params, (item) => {
+ return (
+ item.projectId === params.projectId &&
+ matchLike(item.code, params.code) &&
+ matchLike(item.taskName, params.taskName) &&
+ (params.status === undefined || item.status === params.status)
+ );
+ }),
+ );
+ },
+
+ getTask(id: number) {
+ const task = pdMockTasks.find((item) => item.id === id);
+ if (!task) {
+ return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+ }
+ return mockDelay({ ...task });
+ },
+
+ createTask(data: MesPdTaskApi.Task) {
+ const id = pdMockSeq.task++;
+ const project = pdMockProjects.find((p) => p.id === data.projectId);
+ const task: MesPdTaskApi.Task = {
+ ...data,
+ id,
+ code: data.code || mockNextCode('TK-2026', id),
+ projectCode: project?.code,
+ projectName: project?.name,
+ status: data.status ?? 0,
+ createTime: Date.now(),
+ };
+ pdMockTasks.unshift(task);
+ return mockDelay(id);
+ },
+
+ updateTask(data: MesPdTaskApi.Task) {
+ const index = pdMockTasks.findIndex((item) => item.id === data.id);
+ if (index < 0) {
+ return Promise.reject(new Error('璁捐浠诲姟涓嶅瓨鍦�'));
+ }
+ pdMockTasks[index] = { ...pdMockTasks[index], ...data };
+ return mockDelay(undefined);
+ },
+
+ deleteTask(id: number) {
+ const index = pdMockTasks.findIndex((item) => item.id === id);
+ if (index >= 0) {
+ pdMockTasks.splice(index, 1);
+ }
+ return mockDelay(undefined);
+ },
+
+ dispatchTask(data: MesPdTaskApi.DispatchParams) {
+ const task = pdMockTasks.find((item) => item.id === data.id);
+ if (task) {
+ task.status = 1;
+ task.planFinishDate = data.planFinishDate;
+ task.remark = data.remark;
+ }
+ return mockDelay(undefined);
+ },
+
+ completeTask(id: number) {
+ const task = pdMockTasks.find((item) => item.id === id);
+ if (task) {
+ task.status = 3;
+ task.actualFinishDate = Date.now();
+ }
+ return mockDelay(undefined);
+ },
+
+ // ---------- 璧勬枡 ----------
+ getDocumentPage(params: MesPdDocumentApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockDocuments, params, (item) => {
+ return (
+ item.projectId === params.projectId &&
+ (params.documentType === undefined ||
+ item.documentType === params.documentType) &&
+ (params.version === undefined || item.version === params.version) &&
+ matchLike(item.name, params.name)
+ );
+ }),
+ );
+ },
+
+ getDocument(id: number) {
+ const doc = pdMockDocuments.find((item) => item.id === id);
+ if (!doc) {
+ return Promise.reject(new Error('璁捐璧勬枡涓嶅瓨鍦�'));
+ }
+ return mockDelay({ ...doc });
+ },
+
+ createDocument(data: MesPdDocumentApi.Document) {
+ const id = pdMockSeq.document++;
+ pdMockDocuments.unshift({
+ ...data,
+ id,
+ uploaderName: data.uploaderName ?? '褰撳墠鐢ㄦ埛',
+ createTime: Date.now(),
+ });
+ return mockDelay(id);
+ },
+
+ updateDocument(data: MesPdDocumentApi.Document) {
+ const index = pdMockDocuments.findIndex((item) => item.id === data.id);
+ if (index < 0) {
+ return Promise.reject(new Error('璁捐璧勬枡涓嶅瓨鍦�'));
+ }
+ pdMockDocuments[index] = { ...pdMockDocuments[index], ...data };
+ return mockDelay(undefined);
+ },
+
+ deleteDocument(id: number) {
+ const index = pdMockDocuments.findIndex((item) => item.id === id);
+ if (index >= 0) {
+ pdMockDocuments.splice(index, 1);
+ }
+ return mockDelay(undefined);
+ },
+
+ // ---------- BOM ----------
+ getBomPage(params: MesPdBomApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockBomLines, params, (item) => {
+ return (
+ item.projectId === params.projectId &&
+ matchLike(item.itemCode, params.itemCode) &&
+ matchLike(item.itemName, params.itemName)
+ );
+ }),
+ );
+ },
+
+ createBomLine(data: MesPdBomApi.BomLine) {
+ const id = pdMockSeq.bom++;
+ pdMockBomLines.unshift({ ...data, id, createTime: Date.now() });
+ return mockDelay(id);
+ },
+
+ updateBomLine(data: MesPdBomApi.BomLine) {
+ const index = pdMockBomLines.findIndex((item) => item.id === data.id);
+ if (index < 0) {
+ return Promise.reject(new Error('BOM 琛屼笉瀛樺湪'));
+ }
+ pdMockBomLines[index] = { ...pdMockBomLines[index], ...data };
+ return mockDelay(undefined);
+ },
+
+ deleteBomLine(id: number) {
+ const index = pdMockBomLines.findIndex((item) => item.id === id);
+ if (index >= 0) {
+ pdMockBomLines.splice(index, 1);
+ }
+ return mockDelay(undefined);
+ },
+
+ // ---------- 鍥剧焊瀹℃牳 ----------
+ getDrawingReviewPage(params: MesPdDrawingReviewApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockDrawingReviews, params, (item) => {
+ return (
+ item.projectId === params.projectId &&
+ matchLike(item.code, params.code) &&
+ (params.status === undefined || item.status === params.status)
+ );
+ }),
+ );
+ },
+
+ getDrawingReview(id: number) {
+ const row = pdMockDrawingReviews.find((item) => item.id === id);
+ if (!row) {
+ return Promise.reject(new Error('鍥剧焊瀹℃牳鍗曚笉瀛樺湪'));
+ }
+ return mockDelay({ ...row });
+ },
+
+ reviewDrawing(data: MesPdDrawingReviewApi.ReviewParams) {
+ const row = pdMockDrawingReviews.find((item) => item.id === data.id);
+ if (row) {
+ row.status = data.status;
+ row.reviewComment = data.reviewComment;
+ row.reviewTime = Date.now();
+ }
+ return mockDelay(undefined);
+ },
+ getVersionChangePage(params: MesPdVersionChangeApi.PageParams) {
+ return mockDelay(
+ mockPage(pdMockVersionChanges, params, (item) => {
+ return (
+ item.projectId === params.projectId &&
+ matchLike(item.code, params.code) &&
+ (params.changeType === undefined ||
+ item.changeType === params.changeType)
+ );
+ }),
+ );
+ },
+
+ getVersionChange(id: number) {
+ const row = pdMockVersionChanges.find((item) => item.id === id);
+ if (!row) {
+ return Promise.reject(new Error('鐗堟湰鍙樻洿鍗曚笉瀛樺湪'));
+ }
+ return mockDelay({ ...row });
+ },
+
+ createVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+ const id = pdMockSeq.versionChange++;
+ const project = pdMockProjects.find((p) => p.id === data.projectId);
+ pdMockVersionChanges.unshift({
+ ...data,
+ id,
+ code: data.code || mockNextCode('VC-2026', id),
+ projectCode: project?.code,
+ projectName: project?.name,
+ applicantName: data.applicantName ?? '褰撳墠鐢ㄦ埛',
+ createTime: Date.now(),
+ });
+ return mockDelay(id);
+ },
+
+ updateVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+ const index = pdMockVersionChanges.findIndex((item) => item.id === data.id);
+ if (index < 0) {
+ return Promise.reject(new Error('鐗堟湰鍙樻洿鍗曚笉瀛樺湪'));
+ }
+ pdMockVersionChanges[index] = { ...pdMockVersionChanges[index], ...data };
+ return mockDelay(undefined);
+ },
+
+ deleteVersionChange(id: number) {
+ const index = pdMockVersionChanges.findIndex((item) => item.id === id);
+ if (index >= 0) {
+ pdMockVersionChanges.splice(index, 1);
+ }
+ return mockDelay(undefined);
+ },
+
+ approveVersionChange(id: number) {
+ const row = pdMockVersionChanges.find((item) => item.id === id);
+ const project = pdMockProjects.find((p) => p.id === row?.projectId);
+ if (row && project && row.toVersion) {
+ project.version = row.toVersion;
+ }
+ return mockDelay(undefined);
+ },
+
+ // ---------- 娴佺▼ ----------
+ getWorkflowList(projectId: number) {
+ return mockDelay(pdMockWorkflows[projectId] ?? []);
+ },
+};
diff --git a/src/api/mes/pd/mock/utils.ts b/src/api/mes/pd/mock/utils.ts
new file mode 100644
index 0000000..70e85c5
--- /dev/null
+++ b/src/api/mes/pd/mock/utils.ts
@@ -0,0 +1,28 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+/** 妯℃嫙鎺ュ彛寤惰繜 */
+export function mockDelay<T>(data: T, ms = 120): Promise<T> {
+ return new Promise((resolve) => {
+ setTimeout(() => resolve(data), ms);
+ });
+}
+
+/** 鍒嗛〉涓庣畝鍗曞瓧娈佃繃婊� */
+export function mockPage<T extends Record<string, unknown>>(
+ list: T[],
+ params: PageParam & Record<string, unknown>,
+ filter?: (item: T) => boolean,
+): PageResult<T> {
+ const filtered = filter ? list.filter(filter) : [...list];
+ const pageNo = params.pageNo ?? 1;
+ const pageSize = params.pageSize ?? 10;
+ const start = (pageNo - 1) * pageSize;
+ return {
+ list: filtered.slice(start, start + pageSize),
+ total: filtered.length,
+ };
+}
+
+export function mockNextCode(prefix: string, seq: number): string {
+ return `${prefix}-${String(seq).padStart(4, '0')}`;
+}
diff --git a/src/api/mes/pd/project/index.ts b/src/api/mes/pd/project/index.ts
new file mode 100644
index 0000000..a637d27
--- /dev/null
+++ b/src/api/mes/pd/project/index.ts
@@ -0,0 +1,119 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdProjectApi {
+ /** 浜у搧璁捐椤圭洰 */
+ export interface Project {
+ id?: number;
+ code?: string;
+ name?: string;
+ productCode?: string;
+ productName?: string;
+ version?: string;
+ status?: number;
+ designerId?: number;
+ designerName?: string;
+ reviewerId?: number;
+ reviewerName?: string;
+ planStartDate?: number;
+ planEndDate?: number;
+ actualFinishDate?: number;
+ remark?: string;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ code?: string;
+ name?: string;
+ productCode?: string;
+ status?: number;
+ designerId?: number;
+ }
+}
+
+export function getPdProjectPage(params: MesPdProjectApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getProjectPage(params);
+ }
+ return requestClient.get<PageResult<MesPdProjectApi.Project>>(
+ '/mes/pd/project/page',
+ { params },
+ );
+}
+
+export function getPdProject(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getProject(id);
+ }
+ return requestClient.get<MesPdProjectApi.Project>(
+ `/mes/pd/project/get?id=${id}`,
+ );
+}
+
+export function createPdProject(data: MesPdProjectApi.Project) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.createProject(data);
+ }
+ return requestClient.post<number>('/mes/pd/project/create', data);
+}
+
+export function updatePdProject(data: MesPdProjectApi.Project) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.updateProject(data);
+ }
+ return requestClient.put('/mes/pd/project/update', data);
+}
+
+export function deletePdProject(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.deleteProject(id);
+ }
+ return requestClient.delete(`/mes/pd/project/delete?id=${id}`);
+}
+
+export function exportPdProject(params: MesPdProjectApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return Promise.resolve(new Blob());
+ }
+ return requestClient.download('/mes/pd/project/export-excel', { params });
+}
+
+export function submitPdProjectReview(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.submitProjectReview(id);
+ }
+ return requestClient.put('/mes/pd/project/submit-review', undefined, {
+ params: { id },
+ });
+}
+
+export function publishPdProject(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.publishProject(id);
+ }
+ return requestClient.put('/mes/pd/project/publish', undefined, {
+ params: { id },
+ });
+}
+
+export function approvePdProject(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.approveProject(id);
+ }
+ return requestClient.put('/mes/pd/project/approve', undefined, {
+ params: { id },
+ });
+}
+
+export function archivePdProject(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.archiveProject(id);
+ }
+ return requestClient.put('/mes/pd/project/archive', undefined, {
+ params: { id },
+ });
+}
diff --git a/src/api/mes/pd/task/index.ts b/src/api/mes/pd/task/index.ts
new file mode 100644
index 0000000..ebd7c8c
--- /dev/null
+++ b/src/api/mes/pd/task/index.ts
@@ -0,0 +1,98 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdTaskApi {
+ /** 浜у搧璁捐浠诲姟 */
+ export interface Task {
+ id?: number;
+ code?: string;
+ projectId?: number;
+ projectCode?: string;
+ projectName?: string;
+ taskName?: string;
+ taskType?: number;
+ assigneeId?: number;
+ assigneeName?: string;
+ assignerId?: number;
+ assignerName?: string;
+ priority?: number;
+ status?: number;
+ planFinishDate?: number;
+ actualFinishDate?: number;
+ remark?: string;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ code?: string;
+ projectId?: number;
+ taskName?: string;
+ assigneeId?: number;
+ status?: number;
+ }
+
+ export interface DispatchParams {
+ id: number;
+ assigneeId: number;
+ planFinishDate?: number;
+ remark?: string;
+ }
+}
+
+export function getPdTaskPage(params: MesPdTaskApi.PageParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getTaskPage(params);
+ }
+ return requestClient.get<PageResult<MesPdTaskApi.Task>>(
+ '/mes/pd/task/page',
+ { params },
+ );
+}
+
+export function getPdTask(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getTask(id);
+ }
+ return requestClient.get<MesPdTaskApi.Task>(`/mes/pd/task/get?id=${id}`);
+}
+
+export function createPdTask(data: MesPdTaskApi.Task) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.createTask(data);
+ }
+ return requestClient.post<number>('/mes/pd/task/create', data);
+}
+
+export function updatePdTask(data: MesPdTaskApi.Task) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.updateTask(data);
+ }
+ return requestClient.put('/mes/pd/task/update', data);
+}
+
+export function deletePdTask(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.deleteTask(id);
+ }
+ return requestClient.delete(`/mes/pd/task/delete?id=${id}`);
+}
+
+export function dispatchPdTask(data: MesPdTaskApi.DispatchParams) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.dispatchTask(data);
+ }
+ return requestClient.put('/mes/pd/task/dispatch', data);
+}
+
+export function completePdTask(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.completeTask(id);
+ }
+ return requestClient.put('/mes/pd/task/complete', undefined, {
+ params: { id },
+ });
+}
diff --git a/src/api/mes/pd/version-change/index.ts b/src/api/mes/pd/version-change/index.ts
new file mode 100644
index 0000000..7e521fb
--- /dev/null
+++ b/src/api/mes/pd/version-change/index.ts
@@ -0,0 +1,83 @@
+import type { PageParam, PageResult } from '#/packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdVersionChangeApi {
+ /** 鐗堟湰鍙樻洿 */
+ export interface VersionChange {
+ id?: number;
+ code?: string;
+ projectId?: number;
+ projectCode?: string;
+ projectName?: string;
+ changeType?: number;
+ fromVersion?: string;
+ toVersion?: string;
+ changeReason?: string;
+ changeContent?: string;
+ applicantId?: number;
+ applicantName?: string;
+ remark?: string;
+ createTime?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ code?: string;
+ projectId?: number;
+ changeType?: number;
+ }
+}
+
+export function getPdVersionChangePage(
+ params: MesPdVersionChangeApi.PageParams,
+) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getVersionChangePage(params);
+ }
+ return requestClient.get<PageResult<MesPdVersionChangeApi.VersionChange>>(
+ '/mes/pd/version-change/page',
+ { params },
+ );
+}
+
+export function getPdVersionChange(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getVersionChange(id);
+ }
+ return requestClient.get<MesPdVersionChangeApi.VersionChange>(
+ `/mes/pd/version-change/get?id=${id}`,
+ );
+}
+
+export function createPdVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.createVersionChange(data);
+ }
+ return requestClient.post<number>('/mes/pd/version-change/create', data);
+}
+
+export function updatePdVersionChange(data: MesPdVersionChangeApi.VersionChange) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.updateVersionChange(data);
+ }
+ return requestClient.put('/mes/pd/version-change/update', data);
+}
+
+export function deletePdVersionChange(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.deleteVersionChange(id);
+ }
+ return requestClient.delete(`/mes/pd/version-change/delete?id=${id}`);
+}
+
+export function approvePdVersionChange(id: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.approveVersionChange(id);
+ }
+ return requestClient.put('/mes/pd/version-change/approve', undefined, {
+ params: { id },
+ });
+}
diff --git a/src/api/mes/pd/workflow/index.ts b/src/api/mes/pd/workflow/index.ts
new file mode 100644
index 0000000..bb22e8b
--- /dev/null
+++ b/src/api/mes/pd/workflow/index.ts
@@ -0,0 +1,28 @@
+import { requestClient } from '#/api/request';
+
+import { isPdMockEnabled } from '../mock/enabled';
+import { pdMockStore } from '../mock/store';
+
+export namespace MesPdWorkflowApi {
+ /** 璁捐娴佺▼鑺傜偣 */
+ export interface WorkflowNode {
+ id?: number;
+ projectId?: number;
+ nodeName?: string;
+ nodeType?: string;
+ operatorName?: string;
+ status?: number;
+ operateTime?: number;
+ remark?: string;
+ }
+}
+
+export function getPdWorkflowList(projectId: number) {
+ if (isPdMockEnabled()) {
+ return pdMockStore.getWorkflowList(projectId);
+ }
+ return requestClient.get<MesPdWorkflowApi.WorkflowNode[]>(
+ '/mes/pd/workflow/list',
+ { params: { projectId } },
+ );
+}
diff --git a/src/api/mes/pro/processdesign/parameter/index.ts b/src/api/mes/pro/processdesign/parameter/index.ts
new file mode 100644
index 0000000..391964a
--- /dev/null
+++ b/src/api/mes/pro/processdesign/parameter/index.ts
@@ -0,0 +1,64 @@
+import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdParameterApi {
+ /** 宸ヨ壓鍙傛暟 */
+ export interface Parameter {
+ id?: number;
+ code?: string;
+ name?: string;
+ type?: number;
+ unit?: string;
+ requiredFlag?: boolean;
+ dictType?: string;
+ apiUrl?: string;
+ remark?: string;
+ createTime?: Date;
+ }
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟鍒嗛〉 */
+export function getParameterPage(params: PageParam) {
+ return requestClient.get<PageResult<MesPdParameterApi.Parameter>>(
+ '/mes/pro/process-design/parameter/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟绮剧畝鍒楄〃 */
+export function getParameterSimpleList() {
+ return requestClient.get<MesPdParameterApi.Parameter[]>(
+ '/mes/pro/process-design/parameter/simple-list',
+ );
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟璇︽儏 */
+export function getParameter(id: number) {
+ return requestClient.get<MesPdParameterApi.Parameter>(
+ `/mes/pro/process-design/parameter/get?id=${id}`,
+ );
+}
+
+/** 鏂板宸ヨ壓鍙傛暟 */
+export function createParameter(data: MesPdParameterApi.Parameter) {
+ return requestClient.post(
+ '/mes/pro/process-design/parameter/create',
+ data,
+ );
+}
+
+/** 淇敼宸ヨ壓鍙傛暟 */
+export function updateParameter(data: MesPdParameterApi.Parameter) {
+ return requestClient.put(
+ '/mes/pro/process-design/parameter/update',
+ data,
+ );
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟 */
+export function deleteParameter(id: number) {
+ return requestClient.delete(
+ `/mes/pro/process-design/parameter/delete?id=${id}`,
+ );
+}
diff --git a/src/api/mes/pro/processdesign/template/index.ts b/src/api/mes/pro/processdesign/template/index.ts
new file mode 100644
index 0000000..ab05241
--- /dev/null
+++ b/src/api/mes/pro/processdesign/template/index.ts
@@ -0,0 +1,53 @@
+import type { PageParam, PageResult } from '../../../../../packages/effects/request/src';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesPdTemplateApi {
+ /** 宸ヨ壓鍙傛暟妯℃澘 */
+ export interface Template {
+ id?: number;
+ name?: string;
+ type?: number;
+ parameterIds?: number[];
+ createUser?: string;
+ createTime?: Date;
+ }
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟妯℃澘鍒嗛〉 */
+export function getTemplatePage(params: PageParam) {
+ return requestClient.get<PageResult<MesPdTemplateApi.Template>>(
+ '/mes/pro/process-design/template/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ宸ヨ壓鍙傛暟妯℃澘璇︽儏 */
+export function getTemplate(id: number) {
+ return requestClient.get<MesPdTemplateApi.Template>(
+ `/mes/pro/process-design/template/get?id=${id}`,
+ );
+}
+
+/** 鏂板宸ヨ壓鍙傛暟妯℃澘 */
+export function createTemplate(data: MesPdTemplateApi.Template) {
+ return requestClient.post(
+ '/mes/pro/process-design/template/create',
+ data,
+ );
+}
+
+/** 淇敼宸ヨ壓鍙傛暟妯℃澘 */
+export function updateTemplate(data: MesPdTemplateApi.Template) {
+ return requestClient.put(
+ '/mes/pro/process-design/template/update',
+ data,
+ );
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟妯℃澘 */
+export function deleteTemplate(id: number) {
+ return requestClient.delete(
+ `/mes/pro/process-design/template/delete?id=${id}`,
+ );
+}
diff --git a/src/packages/constants/src/biz-mes-enum.ts b/src/packages/constants/src/biz-mes-enum.ts
index 9394395..5419d1b 100644
--- a/src/packages/constants/src/biz-mes-enum.ts
+++ b/src/packages/constants/src/biz-mes-enum.ts
@@ -133,6 +133,9 @@
MD_VENDOR_CODE: 'MD_VENDOR_CODE',
MD_WORKSTATION_CODE: 'MD_WORKSTATION_CODE',
MD_WORKSHOP_CODE: 'MD_WORKSHOP_CODE',
+ PD_PROJECT_CODE: 'PD_PROJECT_CODE',
+ PD_TASK_CODE: 'PD_TASK_CODE',
+ PD_VERSION_CODE: 'PD_VERSION_CODE',
PRO_CARD_CODE: 'PRO_CARD_CODE',
PRO_FEEDBACK_CODE: 'PRO_FEEDBACK_CODE',
PRO_PROCESS_CODE: 'PRO_PROCESS_CODE',
@@ -492,6 +495,49 @@
RIGHT: 2, // 鍙宠ˉ榻�
} as const;
+/** MES 浜у搧璁捐椤圭洰鐘舵�佹灇涓� */
+export const MesPdProjectStatusEnum = {
+ DRAFT: MesOrderStatusConstants.DRAFT,
+ DESIGNING: 10,
+ REVIEWING: MesOrderStatusConstants.APPROVING,
+ APPROVED: MesOrderStatusConstants.APPROVED,
+ ARCHIVED: 20,
+ CANCELLED: MesOrderStatusConstants.CANCELLED,
+} as const;
+
+/** MES 浜у搧璁捐浠诲姟鐘舵�佹灇涓� */
+export const MesPdTaskStatusEnum = {
+ PENDING: 0,
+ ASSIGNED: 1,
+ IN_PROGRESS: 2,
+ COMPLETED: 3,
+ CANCELLED: 4,
+} as const;
+
+/** MES 浜у搧璁捐鍥剧焊瀹℃牳鐘舵�佹灇涓� */
+export const MesPdDrawingReviewStatusEnum = {
+ PENDING: MesOrderStatusConstants.APPROVING,
+ APPROVED: MesOrderStatusConstants.APPROVED,
+ REJECTED: 5,
+} as const;
+
+/** MES 浜у搧璁捐鐗堟湰鍙樻洿绫诲瀷鏋氫妇 */
+export const MesPdVersionChangeTypeEnum = {
+ MAJOR: 1,
+ MINOR: 2,
+ PATCH: 3,
+} as const;
+
+/** MES 浜у搧璁捐璧勬枡绫诲瀷鏋氫妇 */
+export const MesPdDocumentTypeEnum = {
+ SCHEME: 1,
+ DRAWING: 2,
+ MODEL: 3,
+ PRODUCT_DESIGN: 4,
+ OTHER: 5,
+ BOM: 6,
+} as const;
+
/** MES 鏉$爜鏍煎紡鏋氫妇 */
export enum BarcodeFormatEnum {
QR_CODE = 1,
@@ -508,6 +554,22 @@
[BarcodeFormatEnum.UPC_A]: 'UPC_A',
};
+/** MES 宸ヨ壓鍙傛暟绫诲瀷鏋氫妇 */
+export const MesPdParamTypeEnum = {
+ NUMERIC: 1, // 鏁板�兼牸寮�
+ TEXT: 2, // 鏂囨湰鏍煎紡
+ SELECT: 3, // 涓嬫媺閫夐」
+ TIME: 4, // 鏃堕棿鏍煎紡
+ DATE: 5, // 鏃ユ湡鏍煎紡
+} as const;
+
+/** MES 宸ヨ壓妯℃澘绫诲瀷鏋氫妇 */
+export const MesPdTemplateTypeEnum = {
+ PRODUCTION: 1, // 鐢熶骇瑕佹眰
+ SELF_CHECK: 2, // 鑷瑕佹眰
+ PATROL_CHECK: 3, // 宸℃瑕佹眰
+} as const;
+
/** MES 鏉$爜涓氬姟绫诲瀷鏋氫妇 */
export enum BarcodeBizTypeEnum {
WAREHOUSE = 102,
diff --git a/src/packages/constants/src/dict-enum.ts b/src/packages/constants/src/dict-enum.ts
index af6fc57..f0a2d99 100644
--- a/src/packages/constants/src/dict-enum.ts
+++ b/src/packages/constants/src/dict-enum.ts
@@ -228,6 +228,12 @@
MES_PRO_TASK_STATUS: 'mes_pro_task_status', // MES 鐢熶骇浠诲姟鐘舵��
MES_PRO_FEEDBACK_STATUS: 'mes_pro_feedback_status', // MES 鐢熶骇鎶ュ伐鐘舵��
MES_PRO_FEEDBACK_TYPE: 'mes_pro_feedback_type', // MES 鐢熶骇鎶ュ伐绫诲瀷
+ MES_PD_PROJECT_STATUS: 'mes_pd_project_status', // MES 浜у搧璁捐椤圭洰鐘舵��
+ MES_PD_TASK_STATUS: 'mes_pd_task_status', // MES 浜у搧璁捐浠诲姟鐘舵��
+ MES_PD_DRAWING_REVIEW_STATUS: 'mes_pd_drawing_review_status', // MES 鍥剧焊瀹℃牳鐘舵��
+ MES_PD_VERSION_CHANGE_TYPE: 'mes_pd_version_change_type', // MES 鐗堟湰鍙樻洿绫诲瀷
+ MES_PD_DOCUMENT_TYPE: 'mes_pd_document_type', // MES 璁捐璧勬枡绫诲瀷
+ MES_PD_ARCHIVE_STATUS: 'mes_pd_archive_status', // MES 璁捐褰掓。鐘舵��
MES_PRO_FEEDBACK_CHANNEL: 'mes_pro_feedback_channel', // MES 鐢熶骇鎶ュ伐閫斿緞
MES_PRO_ANDON_STATUS: 'mes_pro_andon_status', // MES 瀹夌伅澶勭疆鐘舵��
MES_PRO_ANDON_LEVEL: 'mes_pro_andon_level', // MES 瀹夌伅绾у埆
@@ -268,6 +274,8 @@
MES_WM_STOCK_TAKING_PLAN_PARAM_TYPE: 'mes_wm_stock_taking_plan_param_type', // MES 鐩樼偣鏂规鍙傛暟绫诲瀷
MES_WM_TRANSFER_STATUS: 'mes_wm_transfer_status', // MES 杞Щ鍗曠姸鎬�
MES_WM_TRANSFER_TYPE: 'mes_wm_transfer_type', // MES 杞Щ鍗曠被鍨�
+ MES_PD_PARAM_TYPE: 'mes_pd_param_type', // MES 宸ヨ壓鍙傛暟绫诲瀷
+ MES_PD_TEMPLATE_TYPE: 'mes_pd_template_type', // MES 宸ヨ壓妯℃澘绫诲瀷
} as const;
/** ========== WMS - 浠撳偍绠$悊妯″潡 ========== */
diff --git a/src/router/routes/modules/mes.ts b/src/router/routes/modules/mes.ts
index 2b62534..91aec0a 100644
--- a/src/router/routes/modules/mes.ts
+++ b/src/router/routes/modules/mes.ts
@@ -47,6 +47,15 @@
},
component: () => import('#/views/mes/pro/task/edit/index.vue'),
},
+ {
+ path: 'pd/project/detail/:id',
+ name: 'MesPdProjectDetail',
+ meta: {
+ title: '璁捐浠诲姟璇︽儏',
+ activePath: '/mes/pd/project',
+ },
+ component: () => import('#/views/mes/pd/project/detail/index.vue'),
+ },
],
},
];
diff --git a/src/views/mes/md/client/data.ts b/src/views/mes/md/client/data.ts
index ebc61a8..45df0e7 100644
--- a/src/views/mes/md/client/data.ts
+++ b/src/views/mes/md/client/data.ts
@@ -1,389 +1,10 @@
-import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdClientApi } from '#/api/mes/md/client';
-import { h } from 'vue';
+import { DICT_TYPE } from '#/packages/constants/src';
-import {
- CommonStatusEnum,
- DICT_TYPE,
- MesAutoCodeRuleCode,
-} from '../../../../packages/constants/src';
-import { getDictOptions } from '../../../../packages/effects/hooks/src';
-
-import { Button } from 'ant-design-vue';
-
-import { z } from '#/adapter/form';
-import { generateAutoCode } from '#/api/mes/md/autocode/record';
-
-/** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'create' | 'detail' | 'update';
-
-/** 鏂板/淇敼瀹㈡埛鐨勮〃鍗� */
-export function useFormSchema(
- formType: FormType,
- formApi?: VbenFormApi,
-): VbenFormSchema[] {
- return [
- {
- fieldName: 'id',
- component: 'Input',
- dependencies: {
- triggerFields: [''],
- show: () => false,
- },
- },
- {
- fieldName: 'code',
- label: '瀹㈡埛缂栫爜',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴风紪鐮�',
- },
- rules: 'required',
- 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',
- label: '瀹㈡埛鍚嶇О',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
- },
- rules: z.string().min(1, '瀹㈡埛鍚嶇О涓嶈兘涓虹┖').max(100),
- },
- {
- fieldName: 'nickname',
- label: '瀹㈡埛绠�绉�',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴风畝绉�',
- },
- },
- {
- fieldName: 'englishName',
- label: '瀹㈡埛鑻辨枃鍚嶇О',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴疯嫳鏂囧悕绉�',
- },
- },
- {
- fieldName: 'type',
- label: '瀹㈡埛绫诲瀷',
- component: 'Select',
- componentProps: {
- allowClear: true,
- options: getDictOptions(DICT_TYPE.MES_CLIENT_TYPE, 'number'),
- placeholder: '璇烽�夋嫨瀹㈡埛绫诲瀷',
- },
- rules: 'selectRequired',
- },
- {
- fieldName: 'description',
- label: '瀹㈡埛绠�浠�',
- component: 'Textarea',
- formItemClass: 'col-span-3',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴风畝浠�',
- rows: 2,
- },
- },
- {
- fieldName: 'address',
- label: '瀹㈡埛鍦板潃',
- component: 'Textarea',
- formItemClass: 'col-span-3',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴峰湴鍧�',
- rows: 2,
- },
- },
- {
- fieldName: 'website',
- label: '瀹㈡埛瀹樼綉鍦板潃',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴峰畼缃戝湴鍧�',
- },
- },
- {
- fieldName: 'email',
- label: '瀹㈡埛閭鍦板潃',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴烽偖绠卞湴鍧�',
- },
- rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
- },
- {
- fieldName: 'telephone',
- label: '瀹㈡埛鐢佃瘽',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴风數璇�',
- },
- },
- {
- fieldName: 'logo',
- label: '瀹㈡埛 LOGO',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ鎴� LOGO 鍦板潃',
- },
- },
- {
- fieldName: 'contact1Name',
- label: '鑱旂郴浜�1',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉1',
- },
- },
- {
- fieldName: 'contact1Telephone',
- label: '鑱旂郴浜�1鐢佃瘽',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉1鐢佃瘽',
- },
- },
- {
- fieldName: 'contact1Email',
- label: '鑱旂郴浜�1閭',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉1閭',
- },
- rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
- },
- {
- fieldName: 'contact2Name',
- label: '鑱旂郴浜�2',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉2',
- },
- },
- {
- fieldName: 'contact2Telephone',
- label: '鑱旂郴浜�2鐢佃瘽',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉2鐢佃瘽',
- },
- },
- {
- fieldName: 'contact2Email',
- label: '鑱旂郴浜�2閭',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉2閭',
- },
- rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
- },
- {
- fieldName: 'creditCode',
- label: '绀句細淇$敤浠g爜',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜',
- },
- },
- {
- fieldName: 'status',
- label: '鐘舵��',
- component: 'RadioGroup',
- componentProps: {
- buttonStyle: 'solid',
- optionType: 'button',
- options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
- },
- rules: z.number().default(CommonStatusEnum.ENABLE),
- },
- {
- fieldName: 'remark',
- label: '澶囨敞',
- component: 'Textarea',
- formItemClass: 'col-span-3',
- componentProps: {
- placeholder: '璇疯緭鍏ュ娉�',
- rows: 3,
- },
- },
- ];
-}
-
-/** 瀵煎叆瀹㈡埛鐨勮〃鍗� */
-export function useImportFormSchema(): VbenFormSchema[] {
- return [
- {
- fieldName: 'file',
- label: '瀹㈡埛鏁版嵁',
- component: 'Upload',
- rules: 'required',
- help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
- },
- {
- fieldName: 'updateSupport',
- label: '鏄惁瑕嗙洊',
- component: 'Switch',
- componentProps: {
- checkedChildren: '鏄�',
- unCheckedChildren: '鍚�',
- },
- rules: z.boolean().default(false),
- help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勫鎴锋暟鎹�',
- },
- ];
-}
-
-/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
-export function useGridFormSchema(): VbenFormSchema[] {
- return [
- {
- fieldName: 'code',
- label: '瀹㈡埛缂栫爜',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ鎴风紪鐮�',
- },
- },
- {
- fieldName: 'name',
- label: '瀹㈡埛鍚嶇О',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ鎴峰悕绉�',
- },
- },
- {
- fieldName: 'nickname',
- label: '瀹㈡埛绠�绉�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ鎴风畝绉�',
- },
- },
- {
- fieldName: 'englishName',
- label: '鑻辨枃鍚嶇О',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ鎴疯嫳鏂囧悕绉�',
- },
- },
- {
- fieldName: 'type',
- label: '瀹㈡埛绫诲瀷',
- component: 'Select',
- componentProps: {
- allowClear: true,
- options: getDictOptions(DICT_TYPE.MES_CLIENT_TYPE, 'number'),
- placeholder: '璇烽�夋嫨瀹㈡埛绫诲瀷',
- },
- },
- {
- fieldName: 'status',
- label: '鐘舵��',
- component: 'Select',
- componentProps: {
- allowClear: true,
- options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
- placeholder: '璇烽�夋嫨鐘舵��',
- },
- },
- ];
-}
-
-/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(): VxeTableGridOptions<MesMdClientApi.Client>['columns'] {
- return [
- {
- field: 'code',
- title: '瀹㈡埛缂栫爜',
- minWidth: 150,
- slots: { default: 'code' },
- },
- {
- field: 'name',
- title: '瀹㈡埛鍚嶇О',
- minWidth: 160,
- },
- {
- field: 'nickname',
- title: '瀹㈡埛绠�绉�',
- minWidth: 130,
- },
- {
- field: 'type',
- title: '瀹㈡埛绫诲瀷',
- width: 120,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.MES_CLIENT_TYPE },
- },
- },
- {
- field: 'telephone',
- title: '瀹㈡埛鐢佃瘽',
- minWidth: 140,
- },
- {
- field: 'contact1Name',
- title: '鑱旂郴浜�1',
- width: 120,
- },
- {
- field: 'contact1Telephone',
- title: '鑱旂郴浜�1鐢佃瘽',
- minWidth: 140,
- },
- {
- field: 'status',
- title: '鐘舵��',
- width: 120,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.COMMON_STATUS },
- },
- },
- {
- field: 'createTime',
- title: '鍒涘缓鏃堕棿',
- width: 180,
- formatter: 'formatDateTime',
- },
- {
- title: '鎿嶄綔',
- width: 160,
- fixed: 'right',
- slots: { default: 'actions' },
- },
- ];
-}
-
-/** 瀹㈡埛閫夋嫨寮圭獥鐨勬悳绱㈣〃鍗� */
+/** 瀹㈡埛閫夋嫨寮圭獥鎼滅储琛ㄥ崟 */
export function useClientSelectGridFormSchema(): VbenFormSchema[] {
return [
{
@@ -425,7 +46,7 @@
];
}
-/** 瀹㈡埛閫夋嫨寮圭獥鐨勫瓧娈� */
+/** 瀹㈡埛閫夋嫨寮圭獥鍒楄〃瀛楁 */
export function useClientSelectGridColumns(
multiple = true,
): VxeTableGridOptions<MesMdClientApi.Client>['columns'] {
diff --git a/src/views/mes/md/client/index.vue b/src/views/mes/md/client/index.vue
deleted file mode 100644
index 084f73f..0000000
--- a/src/views/mes/md/client/index.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdClientApi } from '#/api/mes/md/client';
-
-import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-
-import { Button, message } from 'ant-design-vue';
-
-import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { deleteClient, exportClient, getClientPage } from '#/api/mes/md/client';
-import { $t } from '#/locales';
-
-import { useGridColumns, useGridFormSchema } from './data';
-import Form from './modules/form.vue';
-import ImportForm from './modules/import-form.vue';
-
-const [FormModal, formModalApi] = useVbenModal({
- connectedComponent: Form,
- destroyOnClose: true,
-});
-
-const [ImportModal, importModalApi] = useVbenModal({
- connectedComponent: ImportForm,
- destroyOnClose: true,
-});
-
-/** 鍒锋柊琛ㄦ牸 */
-function handleRefresh() {
- gridApi.query();
-}
-
-/** 鍒涘缓瀹㈡埛 */
-function handleCreate() {
- formModalApi.setData({ formType: 'create' }).open();
-}
-
-/** 鏌ョ湅瀹㈡埛 */
-function handleDetail(row: MesMdClientApi.Client) {
- formModalApi.setData({ id: row.id, formType: 'detail' }).open();
-}
-
-/** 缂栬緫瀹㈡埛 */
-function handleEdit(row: MesMdClientApi.Client) {
- formModalApi.setData({ id: row.id, formType: 'update' }).open();
-}
-
-/** 鍒犻櫎瀹㈡埛 */
-async function handleDelete(row: MesMdClientApi.Client) {
- const hideLoading = message.loading({
- content: $t('ui.actionMessage.deleting', [row.name]),
- duration: 0,
- });
- try {
- await deleteClient(row.id!);
- message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
- handleRefresh();
- } finally {
- hideLoading();
- }
-}
-
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportClient(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '瀹㈡埛.xls', source: data });
-}
-
-/** 瀵煎叆瀹㈡埛 */
-function handleImport() {
- importModalApi.open();
-}
-
-const [Grid, gridApi] = useVbenVxeGrid({
- formOptions: {
- schema: useGridFormSchema(),
- },
- gridOptions: {
- columns: useGridColumns(),
- height: 'auto',
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }, formValues) => {
- return await getClientPage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- ...formValues,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- search: true,
- },
- } as VxeTableGridOptions<MesMdClientApi.Client>,
-});
-</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:md-client:create'],
- onClick: handleCreate,
- },
- {
- label: $t('ui.actionTitle.import', ['瀹㈡埛']),
- type: 'primary',
- icon: ACTION_ICON.UPLOAD,
- auth: ['mes:md-client:import'],
- onClick: handleImport,
- },
- {
- label: $t('ui.actionTitle.export'),
- type: 'primary',
- icon: ACTION_ICON.DOWNLOAD,
- auth: ['mes:md-client:export'],
- onClick: handleExport,
- },
- ]"
- />
- </template>
- <template #code="{ row }">
- <Button type="link" @click="handleDetail(row)">
- {{ row.code }}
- </Button>
- </template>
- <template #actions="{ row }">
- <TableAction
- :actions="[
- {
- label: $t('common.edit'),
- type: 'link',
- icon: ACTION_ICON.EDIT,
- auth: ['mes:md-client:update'],
- onClick: handleEdit.bind(null, row),
- },
- {
- label: $t('common.delete'),
- type: 'link',
- danger: true,
- icon: ACTION_ICON.DELETE,
- auth: ['mes:md-client:delete'],
- popConfirm: {
- title: $t('ui.actionMessage.deleteConfirm', [row.name]),
- confirm: handleDelete.bind(null, row),
- },
- },
- ]"
- />
- </template>
- </Grid>
- </Page>
-</template>
diff --git a/src/views/mes/md/client/modules/form.vue b/src/views/mes/md/client/modules/form.vue
deleted file mode 100644
index 56fe9ae..0000000
--- a/src/views/mes/md/client/modules/form.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<script lang="ts" setup>
-import type { FormType } from '../data';
-
-import type { MesMdClientApi } from '#/api/mes/md/client';
-
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { message, Tabs } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { createClient, getClient, updateClient } from '#/api/mes/md/client';
-import { $t } from '#/locales';
-
-import { useFormSchema } from '../data';
-import ClientProductSalesLineList from './product-sales-line-list.vue';
-import ClientProductSalesList from './product-sales-list.vue';
-
-const emit = defineEmits(['success']);
-const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
-const subTabsName = ref('productSalesLine'); // 褰撳墠瀛愯〃椤电
-const formData = ref<MesMdClientApi.Client>();
-
-const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
-const getTitle = computed(() => {
- if (formType.value === 'detail') {
- return '鏌ョ湅瀹㈡埛';
- }
- return formType.value === 'update' ? '淇敼瀹㈡埛' : '鏂板瀹㈡埛';
-});
-
-const [Form, formApi] = useVbenForm({
- commonConfig: {
- componentProps: {
- class: 'w-full',
- },
- formItemClass: 'col-span-1',
- labelWidth: 120,
- },
- wrapperClass: 'grid-cols-3',
- layout: 'horizontal',
- schema: [],
- showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onConfirm() {
- if (isDetail.value) {
- await modalApi.close();
- return;
- }
- const { valid } = await formApi.validate();
- if (!valid) {
- return;
- }
- modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data = (await formApi.getValues()) as MesMdClientApi.Client;
- try {
- await (formData.value?.id ? updateClient(data) : createClient(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;
- }
- 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) {
- return;
- }
- modalApi.lock();
- try {
- formData.value = await getClient(data.id);
- // 璁剧疆鍒� values
- await formApi.setValues(formData.value);
- } finally {
- modalApi.unlock();
- }
- },
-});
-</script>
-
-<template>
- <Modal :title="getTitle" class="w-4/5">
- <Form class="mx-4" />
- <Tabs
- v-if="formType !== 'create' && formData?.id"
- v-model:active-key="subTabsName"
- class="mx-4 mt-4"
- >
- <Tabs.TabPane key="productSalesLine" tab="浜у搧娓呭崟">
- <ClientProductSalesLineList :client-id="formData.id" />
- </Tabs.TabPane>
- <Tabs.TabPane key="productSales" tab="閿�鍞褰�">
- <ClientProductSalesList :client-id="formData.id" />
- </Tabs.TabPane>
- </Tabs>
- </Modal>
-</template>
diff --git a/src/views/mes/md/client/modules/import-form.vue b/src/views/mes/md/client/modules/import-form.vue
deleted file mode 100644
index 1b547c7..0000000
--- a/src/views/mes/md/client/modules/import-form.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script lang="ts" setup>
-import type { FileType } from 'ant-design-vue/es/upload/interface';
-
-import type { MesMdClientApi } from '#/api/mes/md/client';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../../packages/utils/src';
-
-import { Button, message, Upload } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { importClient, importClientTemplate } from '#/api/mes/md/client';
-import { $t } from '#/locales';
-
-import { useImportFormSchema } from '../data';
-
-const emit = defineEmits(['success']);
-
-const [Form, formApi] = useVbenForm({
- commonConfig: {
- componentProps: {
- class: 'w-full',
- },
- formItemClass: 'col-span-2',
- labelWidth: 120,
- },
- layout: 'horizontal',
- schema: useImportFormSchema(),
- showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onConfirm() {
- const { valid } = await formApi.validate();
- if (!valid) {
- return;
- }
- modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data = await formApi.getValues();
- try {
- const result = await importClient(data.file, data.updateSupport);
- const importData = result as MesMdClientApi.ClientImportRespVO;
- let text = `涓婁紶鎴愬姛鏁伴噺锛�${importData.createCodes?.length || 0};`;
- for (const code of importData.createCodes || []) {
- text += `< ${code} >`;
- }
- text += `鏇存柊鎴愬姛鏁伴噺锛�${importData.updateCodes?.length || 0};`;
- for (const code of importData.updateCodes || []) {
- text += `< ${code} >`;
- }
- text += `鏇存柊澶辫触鏁伴噺锛�${Object.keys(importData.failureCodes || {}).length};`;
- for (const code in importData.failureCodes || {}) {
- text += `< ${code}: ${importData.failureCodes?.[code]} >`;
- }
- message.info(text);
- // 鍏抽棴骞舵彁绀�
- await modalApi.close();
- emit('success');
- message.success($t('ui.actionMessage.operationSuccess'));
- } finally {
- modalApi.unlock();
- }
- },
-});
-
-/** 涓婁紶鍓� */
-function beforeUpload(file: FileType) {
- formApi.setFieldValue('file', file);
- return false;
-}
-
-/** 涓嬭浇妯℃澘 */
-async function handleDownload() {
- const data = await importClientTemplate();
- downloadFileFromBlobPart({ fileName: '瀹㈡埛瀵煎叆妯℃澘.xls', source: data });
-}
-</script>
-
-<template>
- <Modal :title="$t('ui.actionTitle.import', ['瀹㈡埛'])" class="w-1/3">
- <Form class="mx-4">
- <template #file>
- <div class="w-full">
- <Upload
- :before-upload="beforeUpload"
- :max-count="1"
- accept=".xls,.xlsx"
- >
- <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>
diff --git a/src/views/mes/md/client/modules/product-sales-line-list.vue b/src/views/mes/md/client/modules/product-sales-line-list.vue
deleted file mode 100644
index de8b427..0000000
--- a/src/views/mes/md/client/modules/product-sales-line-list.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmProductSalesLineApi } from '#/api/mes/wm/productsales/line';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getProductSalesLinePage } from '#/api/mes/wm/productsales/line';
-import ItemForm from '#/views/mes/md/item/modules/form.vue';
-
-const props = defineProps<{
- clientId: number;
-}>();
-
-const [ItemFormModal, itemFormModalApi] = useVbenModal({
- connectedComponent: ItemForm,
- destroyOnClose: true,
-});
-
-/** 鏌ョ湅鐗╂枡璇︽儏 */
-function handleViewItem(row: MesWmProductSalesLineApi.ProductSalesLine) {
- itemFormModalApi.setData({ id: row.itemId, formType: 'detail' }).open();
-}
-
-const [Grid] = useVbenVxeGrid({
- gridOptions: {
- columns: [
- {
- field: 'itemCode',
- title: '鐗╂枡缂栫爜',
- 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,
- },
- ],
- height: 320,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }) => {
- return await getProductSalesLinePage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- clientId: props.clientId,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- },
- } as VxeTableGridOptions<MesWmProductSalesLineApi.ProductSalesLine>,
-});
-</script>
-
-<template>
- <ItemFormModal />
- <Grid table-title="浜у搧娓呭崟">
- <template #itemCode="{ row }">
- <Button type="link" @click="handleViewItem(row)">
- {{ row.itemCode }}
- </Button>
- </template>
- </Grid>
-</template>
diff --git a/src/views/mes/md/client/modules/product-sales-list.vue b/src/views/mes/md/client/modules/product-sales-list.vue
deleted file mode 100644
index fe171b0..0000000
--- a/src/views/mes/md/client/modules/product-sales-list.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmProductSalesApi } from '#/api/mes/wm/productsales';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { DICT_TYPE } from '../../../../../packages/constants/src';
-
-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,
- slots: { default: 'code' },
- },
- {
- field: 'name',
- title: '鍑哄簱鍗曞悕绉�',
- minWidth: 150,
- },
- {
- field: 'salesOrderCode',
- title: '閿�鍞鍗曠紪鍙�',
- minWidth: 140,
- },
- {
- field: 'salesDate',
- title: '鍑哄簱鏃ユ湡',
- width: 180,
- formatter: 'formatDateTime',
- },
- {
- field: 'status',
- title: '鍗曟嵁鐘舵��',
- width: 120,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.MES_WM_PRODUCT_SALES_STATUS },
- },
- },
- ],
- height: 320,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }) => {
- return await getProductSalesPage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- clientId: props.clientId,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- },
- } as VxeTableGridOptions<MesWmProductSalesApi.ProductSales>,
-});
-</script>
-
-<template>
- <DetailModal />
- <Grid table-title="閿�鍞褰�">
- <template #code="{ row }">
- <Button type="link" @click="handleViewSales(row)">
- {{ row.code }}
- </Button>
- </template>
- </Grid>
-</template>
diff --git a/src/views/mes/md/item/data.ts b/src/views/mes/md/item/data.ts
index bb418e6..3e8be0e 100644
--- a/src/views/mes/md/item/data.ts
+++ b/src/views/mes/md/item/data.ts
@@ -96,7 +96,7 @@
label: '鐗╂枡鍒嗙被',
component: markRaw(MdItemTypeSelect),
componentProps: {
- onChange: async (itemType: any) => {
+ onChange: async (itemType: { itemOrProduct?: string }) => {
await formApi?.setFieldValue(
'itemOrProduct',
itemType?.itemOrProduct,
diff --git a/src/views/mes/md/unitmeasure/data.ts b/src/views/mes/md/unitmeasure/data.ts
deleted file mode 100644
index cd7885e..0000000
--- a/src/views/mes/md/unitmeasure/data.ts
+++ /dev/null
@@ -1,196 +0,0 @@
-import type { VbenFormSchema } from '#/adapter/form';
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
-
-import { CommonStatusEnum, DICT_TYPE } from '../../../../packages/constants/src';
-import { getDictOptions } from '../../../../packages/effects/hooks/src';
-
-import { z } from '#/adapter/form';
-import { getUnitMeasureSimpleList } from '#/api/mes/md/unitmeasure';
-
-/** 鏂板/淇敼璁¢噺鍗曚綅鐨勮〃鍗� */
-export function useFormSchema(): VbenFormSchema[] {
- return [
- {
- fieldName: 'id',
- component: 'Input',
- dependencies: {
- triggerFields: [''],
- show: () => false,
- },
- },
- {
- fieldName: 'code',
- label: '鍗曚綅缂栫爜',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ崟浣嶇紪鐮�',
- },
- rules: 'required',
- },
- {
- fieldName: 'name',
- label: '鍗曚綅鍚嶇О',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ュ崟浣嶅悕绉�',
- },
- rules: 'required',
- },
- {
- fieldName: 'primaryFlag',
- label: '鏄惁涓诲崟浣�',
- component: 'RadioGroup',
- componentProps: {
- buttonStyle: 'solid',
- optionType: 'button',
- options: getDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING, 'boolean'),
- },
- rules: z.boolean().default(true),
- },
- {
- fieldName: 'primaryId',
- label: '涓诲崟浣�',
- component: 'ApiSelect',
- componentProps: {
- allowClear: true,
- api: async () => {
- const list = await getUnitMeasureSimpleList();
- return list.filter((item) => item.primaryFlag === true);
- },
- labelField: 'name',
- placeholder: '璇烽�夋嫨涓诲崟浣�',
- valueField: 'id',
- },
- dependencies: {
- triggerFields: ['primaryFlag'],
- show: (values) => values.primaryFlag === false,
- },
- },
- {
- fieldName: 'changeRate',
- label: '涓庝富鍗曚綅鎹㈢畻姣斾緥',
- component: 'InputNumber',
- componentProps: {
- class: '!w-full',
- min: 0,
- precision: 4,
- step: 1,
- },
- dependencies: {
- triggerFields: ['primaryFlag'],
- show: (values) => values.primaryFlag === false,
- },
- },
- {
- fieldName: 'status',
- label: '鐘舵��',
- component: 'RadioGroup',
- componentProps: {
- buttonStyle: 'solid',
- optionType: 'button',
- options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
- },
- rules: z.number().default(CommonStatusEnum.ENABLE),
- },
- {
- fieldName: 'remark',
- label: '澶囨敞',
- component: 'Textarea',
- componentProps: {
- placeholder: '璇疯緭鍏ュ娉�',
- rows: 3,
- },
- },
- ];
-}
-
-/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
-export function useGridFormSchema(): VbenFormSchema[] {
- return [
- {
- fieldName: 'code',
- label: '鍗曚綅缂栫爜',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ崟浣嶇紪鐮�',
- },
- },
- {
- fieldName: 'name',
- label: '鍗曚綅鍚嶇О',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ュ崟浣嶅悕绉�',
- },
- },
- {
- fieldName: 'status',
- label: '鐘舵��',
- component: 'Select',
- componentProps: {
- allowClear: true,
- options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
- placeholder: '璇烽�夋嫨鐘舵��',
- },
- },
- ];
-}
-
-/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(): VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>['columns'] {
- return [
- {
- field: 'code',
- title: '鍗曚綅缂栫爜',
- minWidth: 160,
- },
- {
- field: 'name',
- title: '鍗曚綅鍚嶇О',
- minWidth: 160,
- },
- {
- field: 'primaryFlag',
- title: '鏄惁涓诲崟浣�',
- width: 120,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
- },
- },
- {
- field: 'changeRate',
- title: '涓庝富鍗曚綅鎹㈢畻姣斾緥',
- minWidth: 150,
- },
- {
- field: 'status',
- title: '鐘舵��',
- width: 120,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.COMMON_STATUS },
- },
- },
- {
- field: 'remark',
- title: '澶囨敞',
- minWidth: 180,
- },
- {
- field: 'createTime',
- title: '鍒涘缓鏃堕棿',
- width: 180,
- formatter: 'formatDateTime',
- },
- {
- title: '鎿嶄綔',
- width: 160,
- fixed: 'right',
- slots: { default: 'actions' },
- },
- ];
-}
diff --git a/src/views/mes/md/unitmeasure/modules/form.vue b/src/views/mes/md/unitmeasure/modules/form.vue
deleted file mode 100644
index 7eec15a..0000000
--- a/src/views/mes/md/unitmeasure/modules/form.vue
+++ /dev/null
@@ -1,92 +0,0 @@
-<script lang="ts" setup>
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
-
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { message } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import {
- createUnitMeasure,
- getUnitMeasure,
- updateUnitMeasure,
-} from '#/api/mes/md/unitmeasure';
-import { $t } from '#/locales';
-
-import { useFormSchema } from '../data';
-
-const emit = defineEmits(['success']);
-const formData = ref<MesMdUnitMeasureApi.UnitMeasure>();
-const getTitle = computed(() => {
- return formData.value?.id
- ? $t('ui.actionTitle.edit', ['璁¢噺鍗曚綅'])
- : $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']);
-});
-
-const [Form, formApi] = useVbenForm({
- commonConfig: {
- componentProps: {
- class: 'w-full',
- },
- formItemClass: 'col-span-2',
- labelWidth: 140,
- },
- layout: 'horizontal',
- schema: useFormSchema(),
- showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onConfirm() {
- const { valid } = await formApi.validate();
- if (!valid) {
- return;
- }
- modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data = (await formApi.getValues()) as MesMdUnitMeasureApi.UnitMeasure;
- if (data.primaryFlag) {
- data.primaryId = undefined;
- data.changeRate = undefined;
- }
- try {
- await (formData.value?.id
- ? updateUnitMeasure(data)
- : createUnitMeasure(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<MesMdUnitMeasureApi.UnitMeasure>();
- if (!data || !data.id) {
- return;
- }
- modalApi.lock();
- try {
- formData.value = await getUnitMeasure(data.id);
- // 璁剧疆鍒� values
- await formApi.setValues(formData.value);
- } finally {
- modalApi.unlock();
- }
- },
-});
-</script>
-
-<template>
- <Modal :title="getTitle" class="w-1/3">
- <Form class="mx-4" />
- </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/data.ts b/src/views/mes/md/vendor/data.ts
index a1a2032..6a0bec1 100644
--- a/src/views/mes/md/vendor/data.ts
+++ b/src/views/mes/md/vendor/data.ts
@@ -1,383 +1,10 @@
-import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-import { h } from 'vue';
+import { DICT_TYPE } from '#/packages/constants/src';
-import {
- CommonStatusEnum,
- DICT_TYPE,
- MesAutoCodeRuleCode,
-} from '../../../../packages/constants/src';
-import { getDictOptions } from '../../../../packages/effects/hooks/src';
-
-import { Button } from 'ant-design-vue';
-
-import { z } from '#/adapter/form';
-import { generateAutoCode } from '#/api/mes/md/autocode/record';
-
-/** 琛ㄥ崟绫诲瀷 */
-export type FormType = 'create' | 'detail' | 'update';
-
-/** 鏂板/淇敼渚涘簲鍟嗙殑琛ㄥ崟 */
-export function useFormSchema(
- formType: FormType,
- formApi?: VbenFormApi,
-): VbenFormSchema[] {
- return [
- {
- fieldName: 'id',
- component: 'Input',
- dependencies: {
- triggerFields: [''],
- show: () => false,
- },
- },
- {
- fieldName: 'code',
- label: '渚涘簲鍟嗙紪鐮�',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜',
- },
- rules: 'required',
- 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',
- label: '渚涘簲鍟嗗悕绉�',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О',
- },
- rules: z.string().min(1, '渚涘簲鍟嗗悕绉颁笉鑳戒负绌�').max(100),
- },
- {
- fieldName: 'nickname',
- label: '渚涘簲鍟嗙畝绉�',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢绠�绉�',
- },
- },
- {
- fieldName: 'englishName',
- label: '鑻辨枃鍚嶇О',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢鑻辨枃鍚嶇О',
- },
- },
- {
- fieldName: 'level',
- label: '渚涘簲鍟嗙瓑绾�',
- component: 'Select',
- componentProps: {
- allowClear: true,
- options: getDictOptions(DICT_TYPE.MES_VENDOR_LEVEL),
- placeholder: '璇烽�夋嫨渚涘簲鍟嗙瓑绾�',
- },
- },
- {
- fieldName: 'description',
- label: '渚涘簲鍟嗙畝浠�',
- component: 'Textarea',
- formItemClass: 'col-span-3',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢绠�浠�',
- rows: 2,
- },
- },
- {
- fieldName: 'address',
- label: '渚涘簲鍟嗗湴鍧�',
- component: 'Textarea',
- formItemClass: 'col-span-3',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢鍦板潃',
- rows: 2,
- },
- },
- {
- fieldName: 'website',
- label: '瀹樼綉鍦板潃',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢瀹樼綉鍦板潃',
- },
- },
- {
- fieldName: 'email',
- label: '閭鍦板潃',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢閭鍦板潃',
- },
- rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
- },
- {
- fieldName: 'telephone',
- label: '渚涘簲鍟嗙數璇�',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢鐢佃瘽',
- },
- },
- {
- fieldName: 'score',
- label: '渚涘簲鍟嗚瘎鍒�',
- component: 'InputNumber',
- componentProps: {
- class: '!w-full',
- max: 100,
- min: 0,
- precision: 0,
- },
- },
- {
- fieldName: 'contact1Name',
- label: '鑱旂郴浜�1',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉1',
- },
- },
- {
- fieldName: 'contact1Telephone',
- label: '鑱旂郴浜�1鐢佃瘽',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉1鐢佃瘽',
- },
- },
- {
- fieldName: 'contact1Email',
- label: '鑱旂郴浜�1閭',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉1閭',
- },
- rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
- },
- {
- fieldName: 'contact2Name',
- label: '鑱旂郴浜�2',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉2',
- },
- },
- {
- fieldName: 'contact2Telephone',
- label: '鑱旂郴浜�2鐢佃瘽',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉2鐢佃瘽',
- },
- },
- {
- fieldName: 'contact2Email',
- label: '鑱旂郴浜�2閭',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヨ仈绯讳汉2閭',
- },
- rules: z.string().email('閭鏍煎紡涓嶆纭�').or(z.literal('')).optional(),
- },
- {
- fieldName: 'creditCode',
- label: '绀句細淇$敤浠g爜',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ョ粺涓�绀句細淇$敤浠g爜',
- },
- },
- {
- fieldName: 'status',
- label: '鐘舵��',
- component: 'RadioGroup',
- componentProps: {
- buttonStyle: 'solid',
- optionType: 'button',
- options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
- },
- rules: z.number().default(CommonStatusEnum.ENABLE),
- },
- {
- fieldName: 'logo',
- label: '渚涘簲鍟� LOGO',
- component: 'Input',
- componentProps: {
- placeholder: '璇疯緭鍏ヤ緵搴斿晢 LOGO 鍦板潃',
- },
- },
- {
- fieldName: 'remark',
- label: '澶囨敞',
- component: 'Textarea',
- formItemClass: 'col-span-3',
- componentProps: {
- placeholder: '璇疯緭鍏ュ娉�',
- rows: 3,
- },
- },
- ];
-}
-
-/** 瀵煎叆渚涘簲鍟嗙殑琛ㄥ崟 */
-export function useImportFormSchema(): VbenFormSchema[] {
- return [
- {
- fieldName: 'file',
- label: '渚涘簲鍟嗘暟鎹�',
- component: 'Upload',
- rules: 'required',
- help: '浠呭厑璁稿鍏� xls銆亁lsx 鏍煎紡鏂囦欢',
- },
- {
- fieldName: 'updateSupport',
- label: '鏄惁瑕嗙洊',
- component: 'Switch',
- componentProps: {
- checkedChildren: '鏄�',
- unCheckedChildren: '鍚�',
- },
- rules: z.boolean().default(false),
- help: '鏄惁鏇存柊宸茬粡瀛樺湪鐨勪緵搴斿晢鏁版嵁',
- },
- ];
-}
-
-/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
-export function useGridFormSchema(): VbenFormSchema[] {
- return [
- {
- fieldName: 'code',
- label: '渚涘簲鍟嗙紪鐮�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ヤ緵搴斿晢缂栫爜',
- },
- },
- {
- fieldName: 'name',
- label: '渚涘簲鍟嗗悕绉�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ヤ緵搴斿晢鍚嶇О',
- },
- },
- {
- fieldName: 'nickname',
- label: '渚涘簲鍟嗙畝绉�',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ヤ緵搴斿晢绠�绉�',
- },
- },
- {
- fieldName: 'englishName',
- label: '鑻辨枃鍚嶇О',
- component: 'Input',
- componentProps: {
- allowClear: true,
- placeholder: '璇疯緭鍏ヨ嫳鏂囧悕绉�',
- },
- },
- {
- fieldName: 'status',
- label: '鐘舵��',
- component: 'Select',
- componentProps: {
- allowClear: true,
- options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
- placeholder: '璇烽�夋嫨鐘舵��',
- },
- },
- ];
-}
-
-/** 鍒楄〃鐨勫瓧娈� */
-export function useGridColumns(): VxeTableGridOptions<MesMdVendorApi.Vendor>['columns'] {
- return [
- {
- field: 'code',
- title: '渚涘簲鍟嗙紪鐮�',
- minWidth: 150,
- slots: { default: 'code' },
- },
- {
- field: 'name',
- title: '渚涘簲鍟嗗悕绉�',
- minWidth: 180,
- },
- {
- field: 'nickname',
- title: '渚涘簲鍟嗙畝绉�',
- width: 120,
- },
- {
- field: 'level',
- title: '渚涘簲鍟嗙瓑绾�',
- width: 130,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.MES_VENDOR_LEVEL },
- },
- },
- {
- field: 'score',
- title: '渚涘簲鍟嗚瘎鍒�',
- width: 120,
- },
- {
- field: 'telephone',
- title: '渚涘簲鍟嗙數璇�',
- minWidth: 140,
- },
- {
- field: 'status',
- title: '鐘舵��',
- width: 120,
- cellRender: {
- name: 'CellDict',
- props: { type: DICT_TYPE.COMMON_STATUS },
- },
- },
- {
- field: 'remark',
- title: '澶囨敞',
- minWidth: 180,
- },
- {
- title: '鎿嶄綔',
- width: 160,
- fixed: 'right',
- slots: { default: 'actions' },
- },
- ];
-}
-
-/** 渚涘簲鍟嗛�夋嫨寮圭獥鐨勬悳绱㈣〃鍗� */
+/** 渚涘簲鍟嗛�夋嫨寮圭獥鎼滅储琛ㄥ崟 */
export function useVendorSelectGridFormSchema(): VbenFormSchema[] {
return [
{
@@ -419,7 +46,7 @@
];
}
-/** 渚涘簲鍟嗛�夋嫨寮圭獥鐨勫瓧娈� */
+/** 渚涘簲鍟嗛�夋嫨寮圭獥鍒楄〃瀛楁 */
export function useVendorSelectGridColumns(
multiple = true,
): VxeTableGridOptions<MesMdVendorApi.Vendor>['columns'] {
diff --git a/src/views/mes/md/vendor/index.vue b/src/views/mes/md/vendor/index.vue
deleted file mode 100644
index 3f68372..0000000
--- a/src/views/mes/md/vendor/index.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-
-import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-
-import { Button, message } from 'ant-design-vue';
-
-import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { deleteVendor, exportVendor, getVendorPage } from '#/api/mes/md/vendor';
-import { $t } from '#/locales';
-
-import { useGridColumns, useGridFormSchema } from './data';
-import Form from './modules/form.vue';
-import ImportForm from './modules/import-form.vue';
-
-const [FormModal, formModalApi] = useVbenModal({
- connectedComponent: Form,
- destroyOnClose: true,
-});
-
-const [ImportModal, importModalApi] = useVbenModal({
- connectedComponent: ImportForm,
- destroyOnClose: true,
-});
-
-/** 鍒锋柊琛ㄦ牸 */
-function handleRefresh() {
- gridApi.query();
-}
-
-/** 鍒涘缓渚涘簲鍟� */
-function handleCreate() {
- formModalApi.setData({ formType: 'create' }).open();
-}
-
-/** 鏌ョ湅渚涘簲鍟� */
-function handleDetail(row: MesMdVendorApi.Vendor) {
- formModalApi.setData({ id: row.id, formType: 'detail' }).open();
-}
-
-/** 缂栬緫渚涘簲鍟� */
-function handleEdit(row: MesMdVendorApi.Vendor) {
- formModalApi.setData({ id: row.id, formType: 'update' }).open();
-}
-
-/** 鍒犻櫎渚涘簲鍟� */
-async function handleDelete(row: MesMdVendorApi.Vendor) {
- const hideLoading = message.loading({
- content: $t('ui.actionMessage.deleting', [row.name]),
- duration: 0,
- });
- try {
- await deleteVendor(row.id!);
- message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
- handleRefresh();
- } finally {
- hideLoading();
- }
-}
-
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportVendor(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '渚涘簲鍟�.xls', source: data });
-}
-
-/** 瀵煎叆渚涘簲鍟� */
-function handleImport() {
- importModalApi.open();
-}
-
-const [Grid, gridApi] = useVbenVxeGrid({
- formOptions: {
- schema: useGridFormSchema(),
- },
- gridOptions: {
- columns: useGridColumns(),
- height: 'auto',
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }, formValues) => {
- return await getVendorPage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- ...formValues,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- search: true,
- },
- } as VxeTableGridOptions<MesMdVendorApi.Vendor>,
-});
-</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:md-vendor:create'],
- onClick: handleCreate,
- },
- {
- label: $t('ui.actionTitle.import', ['渚涘簲鍟�']),
- type: 'primary',
- icon: ACTION_ICON.UPLOAD,
- auth: ['mes:md-vendor:import'],
- onClick: handleImport,
- },
- {
- label: $t('ui.actionTitle.export'),
- type: 'primary',
- icon: ACTION_ICON.DOWNLOAD,
- auth: ['mes:md-vendor:export'],
- onClick: handleExport,
- },
- ]"
- />
- </template>
- <template #code="{ row }">
- <Button type="link" @click="handleDetail(row)">
- {{ row.code }}
- </Button>
- </template>
- <template #actions="{ row }">
- <TableAction
- :actions="[
- {
- label: $t('common.edit'),
- type: 'link',
- icon: ACTION_ICON.EDIT,
- auth: ['mes:md-vendor:update'],
- onClick: handleEdit.bind(null, row),
- },
- {
- label: $t('common.delete'),
- type: 'link',
- danger: true,
- icon: ACTION_ICON.DELETE,
- auth: ['mes:md-vendor:delete'],
- popConfirm: {
- title: $t('ui.actionMessage.deleteConfirm', [row.name]),
- confirm: handleDelete.bind(null, row),
- },
- },
- ]"
- />
- </template>
- </Grid>
- </Page>
-</template>
diff --git a/src/views/mes/md/vendor/modules/form.vue b/src/views/mes/md/vendor/modules/form.vue
deleted file mode 100644
index 4c2b919..0000000
--- a/src/views/mes/md/vendor/modules/form.vue
+++ /dev/null
@@ -1,115 +0,0 @@
-<script lang="ts" setup>
-import type { FormType } from '../data';
-
-import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-
-import { computed, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { message, Tabs } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { createVendor, getVendor, updateVendor } from '#/api/mes/md/vendor';
-import { $t } from '#/locales';
-
-import { useFormSchema } from '../data';
-import VendorItemReceiptLineList from './item-receipt-line-list.vue';
-import VendorItemReceiptList from './item-receipt-list.vue';
-
-const emit = defineEmits(['success']);
-const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
-const subTabsName = ref('itemReceiptLine'); // 褰撳墠瀛愯〃椤电
-const formData = ref<MesMdVendorApi.Vendor>();
-
-const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
-const getTitle = computed(() => {
- if (formType.value === 'detail') {
- return $t('ui.actionTitle.view', ['渚涘簲鍟�']);
- }
- return formType.value === 'update'
- ? $t('ui.actionTitle.edit', ['渚涘簲鍟�'])
- : $t('ui.actionTitle.create', ['渚涘簲鍟�']);
-});
-
-const [Form, formApi] = useVbenForm({
- commonConfig: {
- componentProps: {
- class: 'w-full',
- },
- formItemClass: 'col-span-1',
- labelWidth: 120,
- },
- wrapperClass: 'grid-cols-3',
- layout: 'horizontal',
- schema: [],
- showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onConfirm() {
- if (isDetail.value) {
- await modalApi.close();
- return;
- }
- const { valid } = await formApi.validate();
- if (!valid) {
- return;
- }
- modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data = (await formApi.getValues()) as MesMdVendorApi.Vendor;
- try {
- await (formData.value?.id ? updateVendor(data) : createVendor(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;
- }
- 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) {
- return;
- }
- modalApi.lock();
- try {
- formData.value = await getVendor(data.id);
- // 璁剧疆鍒� values
- await formApi.setValues(formData.value);
- } finally {
- modalApi.unlock();
- }
- },
-});
-</script>
-
-<template>
- <Modal :title="getTitle" class="w-4/5">
- <Form class="mx-4" />
- <Tabs
- v-if="formType !== 'create' && formData?.id"
- v-model:active-key="subTabsName"
- class="mx-4 mt-4"
- >
- <Tabs.TabPane key="itemReceiptLine" tab="鐗╂枡娓呭崟">
- <VendorItemReceiptLineList :vendor-id="formData.id" />
- </Tabs.TabPane>
- <Tabs.TabPane key="itemReceipt" tab="閲囪喘璁板綍">
- <VendorItemReceiptList :vendor-id="formData.id" />
- </Tabs.TabPane>
- </Tabs>
- </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/modules/import-form.vue b/src/views/mes/md/vendor/modules/import-form.vue
deleted file mode 100644
index 2e0c814..0000000
--- a/src/views/mes/md/vendor/modules/import-form.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script lang="ts" setup>
-import type { FileType } from 'ant-design-vue/es/upload/interface';
-
-import type { MesMdVendorApi } from '#/api/mes/md/vendor';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../../packages/utils/src';
-
-import { Button, message, Upload } from 'ant-design-vue';
-
-import { useVbenForm } from '#/adapter/form';
-import { importVendor, importVendorTemplate } from '#/api/mes/md/vendor';
-import { $t } from '#/locales';
-
-import { useImportFormSchema } from '../data';
-
-const emit = defineEmits(['success']);
-
-const [Form, formApi] = useVbenForm({
- commonConfig: {
- componentProps: {
- class: 'w-full',
- },
- formItemClass: 'col-span-2',
- labelWidth: 120,
- },
- layout: 'horizontal',
- schema: useImportFormSchema(),
- showDefaultActions: false,
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onConfirm() {
- const { valid } = await formApi.validate();
- if (!valid) {
- return;
- }
- modalApi.lock();
- // 鎻愪氦琛ㄥ崟
- const data = await formApi.getValues();
- try {
- const result = await importVendor(data.file, data.updateSupport);
- const importData = result as MesMdVendorApi.VendorImportRespVO;
- let text = `涓婁紶鎴愬姛鏁伴噺锛�${importData.createCodes?.length || 0};`;
- for (const code of importData.createCodes || []) {
- text += `< ${code} >`;
- }
- text += `鏇存柊鎴愬姛鏁伴噺锛�${importData.updateCodes?.length || 0};`;
- for (const code of importData.updateCodes || []) {
- text += `< ${code} >`;
- }
- text += `鏇存柊澶辫触鏁伴噺锛�${Object.keys(importData.failureCodes || {}).length};`;
- for (const code in importData.failureCodes || {}) {
- text += `< ${code}: ${importData.failureCodes?.[code]} >`;
- }
- message.info(text);
- // 鍏抽棴骞舵彁绀�
- await modalApi.close();
- emit('success');
- message.success($t('ui.actionMessage.operationSuccess'));
- } finally {
- modalApi.unlock();
- }
- },
-});
-
-/** 涓婁紶鍓� */
-function beforeUpload(file: FileType) {
- formApi.setFieldValue('file', file);
- return false;
-}
-
-/** 涓嬭浇妯℃澘 */
-async function handleDownload() {
- const data = await importVendorTemplate();
- downloadFileFromBlobPart({ fileName: '渚涘簲鍟嗗鍏ユā鏉�.xls', source: data });
-}
-</script>
-
-<template>
- <Modal :title="$t('ui.actionTitle.import', ['渚涘簲鍟�'])" class="w-1/3">
- <Form class="mx-4">
- <template #file>
- <div class="w-full">
- <Upload
- :before-upload="beforeUpload"
- :max-count="1"
- accept=".xls,.xlsx"
- >
- <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>
diff --git a/src/views/mes/md/vendor/modules/item-receipt-detail.vue b/src/views/mes/md/vendor/modules/item-receipt-detail.vue
deleted file mode 100644
index 0fafb7a..0000000
--- a/src/views/mes/md/vendor/modules/item-receipt-detail.vue
+++ /dev/null
@@ -1,162 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmItemReceiptApi } from '#/api/mes/wm/itemreceipt';
-import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
-
-import { nextTick, ref } from 'vue';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-import { formatDateTime } from '../../../../../packages/utils/src';
-
-import { Descriptions, Spin } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getItemReceipt } from '#/api/mes/wm/itemreceipt';
-import { getItemReceiptLinePage } from '#/api/mes/wm/itemreceipt/line';
-
-const loading = ref(false); // 璇︽儏鍔犺浇鐘舵��
-const receiptId = ref<number>(); // 褰撳墠閲囪喘鍏ュ簱鍗曠紪鍙�
-const receipt = ref<MesWmItemReceiptApi.ItemReceipt>(); // 褰撳墠閲囪喘鍏ュ簱鍗曡鎯�
-
-/** 鏍煎紡鍖栫┖鍊� */
-function formatEmpty(value: null | number | string | undefined) {
- return value ?? '-';
-}
-
-/** 鏍煎紡鍖栨棩鏈� */
-function formatDate(value: Date | number | string | undefined) {
- return value ? (formatDateTime(value) as string) : '-';
-}
-
-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,
- },
- ],
- height: 280,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }) => {
- if (!receiptId.value) {
- return { list: [], total: 0 };
- }
- return await getItemReceiptLinePage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- receiptId: receiptId.value,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- },
- } as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
-});
-
-const [Modal, modalApi] = useVbenModal({
- async onOpenChange(isOpen: boolean) {
- if (!isOpen) {
- receiptId.value = undefined;
- receipt.value = undefined;
- return;
- }
- const data = modalApi.getData<{ id?: number }>();
- if (!data?.id) {
- return;
- }
- receiptId.value = data.id;
- loading.value = true;
- modalApi.lock();
- try {
- receipt.value = await getItemReceipt(data.id);
- await nextTick();
- await gridApi.query();
- } finally {
- loading.value = false;
- modalApi.unlock();
- }
- },
-});
-</script>
-
-<template>
- <Modal
- title="閲囪喘鍏ュ簱鍗曡鎯�"
- class="w-[900px]"
- :show-cancel-button="false"
- :show-confirm-button="false"
- >
- <Spin :spinning="loading">
- <Descriptions bordered size="small" :column="3">
- <Descriptions.Item label="鍏ュ簱鍗曠紪鍙�">
- {{ formatEmpty(receipt?.code) }}
- </Descriptions.Item>
- <Descriptions.Item label="鍏ュ簱鍗曞悕绉�">
- {{ formatEmpty(receipt?.name) }}
- </Descriptions.Item>
- <Descriptions.Item label="鍏ュ簱鏃ユ湡">
- {{ formatDate(receipt?.receiptDate) }}
- </Descriptions.Item>
- <Descriptions.Item label="鍒拌揣閫氱煡鍗�">
- {{ formatEmpty(receipt?.noticeCode) }}
- </Descriptions.Item>
- <Descriptions.Item label="渚涘簲鍟�">
- {{ formatEmpty(receipt?.vendorName) }}
- </Descriptions.Item>
- <Descriptions.Item label="閲囪喘璁㈠崟鍙�">
- {{ formatEmpty(receipt?.purchaseOrderCode) }}
- </Descriptions.Item>
- <Descriptions.Item label="浠撳簱">
- {{ formatEmpty(receipt?.warehouseName) }}
- </Descriptions.Item>
- <Descriptions.Item label="搴撳尯">
- {{ formatEmpty(receipt?.locationName) }}
- </Descriptions.Item>
- <Descriptions.Item label="搴撲綅">
- {{ formatEmpty(receipt?.areaName) }}
- </Descriptions.Item>
- <Descriptions.Item label="澶囨敞" :span="3">
- {{ formatEmpty(receipt?.remark) }}
- </Descriptions.Item>
- </Descriptions>
- <div class="mt-4">
- <Grid table-title="鍏ュ簱鐗╂枡" />
- </div>
- </Spin>
- </Modal>
-</template>
diff --git a/src/views/mes/md/vendor/modules/item-receipt-line-list.vue b/src/views/mes/md/vendor/modules/item-receipt-line-list.vue
deleted file mode 100644
index 2811d89..0000000
--- a/src/views/mes/md/vendor/modules/item-receipt-line-list.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getItemReceiptLinePage } from '#/api/mes/wm/itemreceipt/line';
-import ItemForm from '#/views/mes/md/item/modules/form.vue';
-
-const props = defineProps<{
- vendorId: number;
-}>();
-
-const [ItemFormModal, itemFormModalApi] = useVbenModal({
- connectedComponent: ItemForm,
- destroyOnClose: true,
-});
-
-/** 鏌ョ湅鐗╂枡璇︽儏 */
-function handleViewItem(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
- itemFormModalApi.setData({ id: row.itemId, formType: 'detail' }).open();
-}
-
-const [Grid] = useVbenVxeGrid({
- gridOptions: {
- columns: [
- {
- field: 'itemCode',
- title: '鐗╂枡缂栫爜',
- 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,
- },
- ],
- height: 320,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }) => {
- return await getItemReceiptLinePage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- vendorId: props.vendorId,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- },
- } as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
-});
-</script>
-
-<template>
- <ItemFormModal />
- <Grid table-title="鐗╂枡娓呭崟">
- <template #itemCode="{ row }">
- <Button type="link" @click="handleViewItem(row)">
- {{ row.itemCode }}
- </Button>
- </template>
- </Grid>
-</template>
diff --git a/src/views/mes/md/vendor/modules/item-receipt-list.vue b/src/views/mes/md/vendor/modules/item-receipt-list.vue
deleted file mode 100644
index e712049..0000000
--- a/src/views/mes/md/vendor/modules/item-receipt-list.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<script lang="ts" setup>
-import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesWmItemReceiptLineApi } from '#/api/mes/wm/itemreceipt/line';
-
-import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
-
-import { Button } from 'ant-design-vue';
-
-import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getItemReceiptLinePage } from '#/api/mes/wm/itemreceipt/line';
-
-import ItemReceiptDetail from './item-receipt-detail.vue';
-
-const props = defineProps<{
- vendorId: number;
-}>();
-
-const [DetailModal, detailModalApi] = useVbenModal({
- connectedComponent: ItemReceiptDetail,
- destroyOnClose: true,
-});
-
-/** 鏌ョ湅閲囪喘鍏ュ簱鍗曡鎯� */
-function handleViewReceipt(row: MesWmItemReceiptLineApi.ItemReceiptLine) {
- if (row.receiptId) {
- detailModalApi.setData({ id: row.receiptId }).open();
- }
-}
-
-const [Grid] = useVbenVxeGrid({
- gridOptions: {
- columns: [
- {
- field: 'receiptCode',
- title: '鍏ュ簱鍗曠紪鍙�',
- 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,
- },
- ],
- height: 320,
- keepSource: true,
- proxyConfig: {
- ajax: {
- query: async ({ page }) => {
- return await getItemReceiptLinePage({
- pageNo: page.currentPage,
- pageSize: page.pageSize,
- vendorId: props.vendorId,
- });
- },
- },
- },
- rowConfig: {
- keyField: 'id',
- isHover: true,
- },
- toolbarConfig: {
- refresh: true,
- },
- } as VxeTableGridOptions<MesWmItemReceiptLineApi.ItemReceiptLine>,
-});
-</script>
-
-<template>
- <DetailModal />
- <Grid table-title="閲囪喘璁板綍">
- <template #receiptCode="{ row }">
- <Button type="link" @click="handleViewReceipt(row)">
- {{ row.receiptCode }}
- </Button>
- </template>
- </Grid>
-</template>
diff --git a/src/views/mes/md/workstation/data.ts b/src/views/mes/md/workstation/data.ts
index 69d6170..d10750e 100644
--- a/src/views/mes/md/workstation/data.ts
+++ b/src/views/mes/md/workstation/data.ts
@@ -18,7 +18,7 @@
import { getWarehouseSimpleList } from '#/api/mes/wm/warehouse';
import { getWarehouseAreaSimpleList } from '#/api/mes/wm/warehouse/area';
import { getWarehouseLocationSimpleList } from '#/api/mes/wm/warehouse/location';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
import { MdWorkshopSelect } from './components';
diff --git a/src/views/mes/pd/archive/data.ts b/src/views/mes/pd/archive/data.ts
new file mode 100644
index 0000000..7146fcf
--- /dev/null
+++ b/src/views/mes/pd/archive/data.ts
@@ -0,0 +1,71 @@
+import type { VbenFormSchema } from "#/adapter/form";
+import type { VxeTableGridOptions } from "#/adapter/vxe-table";
+
+import { DICT_TYPE } from "#/packages/constants/src";
+import { getDictOptions } from "#/packages/effects/hooks/src";
+
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: "code",
+ label: "褰掓。缂栧彿",
+ component: "Input",
+ componentProps: { placeholder: "璇疯緭鍏ュ綊妗g紪鍙�" },
+ },
+ {
+ fieldName: "projectCode",
+ label: "浠诲姟缂栫爜",
+ component: "Input",
+ componentProps: { placeholder: "璇疯緭鍏ヤ换鍔$紪鐮�" },
+ },
+ {
+ fieldName: "productCode",
+ label: "浜у搧缂栫爜",
+ component: "Input",
+ componentProps: { placeholder: "璇疯緭鍏ヤ骇鍝佺紪鐮�" },
+ },
+ {
+ fieldName: "archiveStatus",
+ label: "褰掓。鐘舵��",
+ component: "Select",
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_ARCHIVE_STATUS, "number"),
+ placeholder: "璇烽�夋嫨褰掓。鐘舵��",
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions["columns"] {
+ return [
+ {
+ field: "code",
+ title: "褰掓。缂栧彿",
+ minWidth: 130,
+ slots: { default: "code" },
+ },
+ { field: "projectCode", title: "浠诲姟缂栫爜", minWidth: 120 },
+ { field: "projectName", title: "浠诲姟鍚嶇О", minWidth: 140 },
+ { field: "productCode", title: "浜у搧缂栫爜", minWidth: 120 },
+ { field: "productName", title: "浜у搧鍚嶇О", minWidth: 140 },
+ { field: "version", title: "鐗堟湰鍙�", minWidth: 90 },
+ {
+ field: "archiveStatus",
+ title: "褰掓。鐘舵��",
+ minWidth: 100,
+ cellRender: {
+ name: "CellDict",
+ props: { type: DICT_TYPE.MES_PD_ARCHIVE_STATUS },
+ },
+ },
+ { field: "documentCount", title: "璧勬枡鏁�", minWidth: 80 },
+ { field: "archiverName", title: "褰掓。浜�", minWidth: 100 },
+ {
+ field: "archiveTime",
+ title: "褰掓。鏃堕棿",
+ minWidth: 160,
+ formatter: "formatDateTime",
+ },
+ ];
+}
diff --git a/src/views/mes/pd/archive/index.vue b/src/views/mes/pd/archive/index.vue
new file mode 100644
index 0000000..9949bdb
--- /dev/null
+++ b/src/views/mes/pd/archive/index.vue
@@ -0,0 +1,75 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+
+import { ref } from 'vue';
+
+import { Page } from '#/packages/effects/common-ui/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { exportPdArchive, getPdArchivePage } from '#/api/mes/pd/archive';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Detail from './modules/detail.vue';
+
+const detailRef = ref<InstanceType<typeof Detail>>();
+
+function handleDetail(row: MesPdArchiveApi.Archive) {
+ detailRef.value?.open(row.id!);
+}
+
+async function handleExport() {
+ const data = await exportPdArchive(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '璁捐褰掓。鍙拌处.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useGridFormSchema() },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPdArchivePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesPdArchiveApi.Archive>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <Detail ref="detailRef" />
+
+ <Grid table-title="浜у搧鐗堟湰褰掓。鍙拌处">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '瀵煎嚭',
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pd-archive:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pd/archive/modules/detail.vue b/src/views/mes/pd/archive/modules/detail.vue
new file mode 100644
index 0000000..6b3875f
--- /dev/null
+++ b/src/views/mes/pd/archive/modules/detail.vue
@@ -0,0 +1,104 @@
+<script lang="ts" setup>
+import type { MesPdArchiveApi } from '#/api/mes/pd/archive';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, Descriptions, Tabs } from 'ant-design-vue';
+
+import { getPdArchive } from '#/api/mes/pd/archive';
+
+import MaterialPanel from '#/views/mes/pd/project/modules/material-panel.vue';
+import VersionChangeList from '#/views/mes/pd/project/modules/version-change/version-change-list.vue';
+
+const detail = ref<MesPdArchiveApi.Archive>();
+const activeTab = ref('version-change');
+const materialVersion = ref<string>();
+const materialVersionHint = ref<string>();
+
+function handleViewMaterials(row: MesPdVersionChangeApi.VersionChange) {
+ materialVersion.value = row.toVersion;
+ materialVersionHint.value = `鍙樻洿鍗� ${row.code}锛�${row.fromVersion} 鈫� ${row.toVersion}`;
+ activeTab.value = 'material';
+}
+
+function clearMaterialVersionFilter() {
+ materialVersion.value = detail.value?.version;
+ materialVersionHint.value = undefined;
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ detail.value = undefined;
+ activeTab.value = 'version-change';
+ materialVersion.value = undefined;
+ materialVersionHint.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ modalApi.lock();
+ try {
+ detail.value = await getPdArchive(data.id);
+ materialVersion.value = detail.value.version;
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+defineExpose({
+ open: (id: number) => modalApi.setData({ id }).open(),
+});
+</script>
+
+<template>
+ <Modal class="w-4/5" title="褰掓。璇︽儏">
+ <Descriptions v-if="detail" :column="3" bordered class="mx-4 mb-4" size="small">
+ <Descriptions.Item label="褰掓。缂栧彿">{{ detail.code }}</Descriptions.Item>
+ <Descriptions.Item label="浜у搧缂栫爜">{{ detail.productCode }}</Descriptions.Item>
+ <Descriptions.Item label="浜у搧鍚嶇О">{{ detail.productName }}</Descriptions.Item>
+ <Descriptions.Item label="璁捐鐗堟湰">{{ detail.version }}</Descriptions.Item>
+ <Descriptions.Item label="浠诲姟缂栫爜">{{ detail.projectCode }}</Descriptions.Item>
+ <Descriptions.Item label="浠诲姟鍚嶇О">{{ detail.projectName }}</Descriptions.Item>
+ <Descriptions.Item label="璧勬枡鏁�">{{ detail.documentCount ?? '-' }}</Descriptions.Item>
+ <Descriptions.Item label="褰掓。浜�">{{ detail.archiverName || '-' }}</Descriptions.Item>
+ <Descriptions.Item label="褰掓。璺緞" :span="3">
+ {{ detail.storagePath || '-' }}
+ </Descriptions.Item>
+ <Descriptions.Item label="澶囨敞" :span="3">{{ detail.remark || '-' }}</Descriptions.Item>
+ </Descriptions>
+
+ <template v-if="detail?.projectId">
+ <Tabs v-model:active-key="activeTab" class="mx-4">
+ <Tabs.TabPane key="version-change" tab="鐗堟湰鍙樻洿璁板綍">
+ <VersionChangeList
+ form-type="detail"
+ :project-id="detail.projectId"
+ :project-version="detail.version"
+ @view-materials="handleViewMaterials"
+ />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="material" tab="鐗堟湰璧勬枡">
+ <Alert
+ v-if="materialVersionHint"
+ class="mb-3"
+ closable
+ show-icon
+ type="info"
+ :message="`姝e湪鏌ョ湅 ${materialVersion} 鐗堟湰璧勬枡锛�${materialVersionHint}锛塦"
+ @close="clearMaterialVersionFilter"
+ />
+ <MaterialPanel
+ form-type="detail"
+ :project-id="detail.projectId"
+ table-title="鐗堟湰璧勬枡"
+ :version="materialVersion"
+ />
+ </Tabs.TabPane>
+ </Tabs>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/data.ts b/src/views/mes/pd/project/data.ts
new file mode 100644
index 0000000..9d9cac6
--- /dev/null
+++ b/src/views/mes/pd/project/data.ts
@@ -0,0 +1,535 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesMdItemApi } from '#/api/mes/md/item';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { h, markRaw } from 'vue';
+
+import {
+ DICT_TYPE,
+ MesAutoCodeRuleCode,
+ MesPdDocumentTypeEnum,
+} from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { isPdMockEnabled } from '#/api/mes/pd/mock/enabled';
+import { mockNextCode } from '#/api/mes/pd/mock/utils';
+import { getRangePickerDefaultProps } from '#/utils';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+export type FormType = 'create' | 'detail' | 'update';
+
+async function generateTaskCode(formApi?: VbenFormApi) {
+ const code = isPdMockEnabled()
+ ? mockNextCode('DT-2026', Math.floor(Date.now() % 10_000))
+ : await generateAutoCode(MesAutoCodeRuleCode.PD_PROJECT_CODE);
+ await formApi?.setFieldValue('code', code);
+}
+
+async function syncTaskNameFromProduct(
+ formApi: VbenFormApi | undefined,
+ productName?: string,
+) {
+ const version = (await formApi?.getFieldValue('version')) as
+ | string
+ | undefined;
+ const name = productName?.trim();
+ if (name && version?.trim()) {
+ await formApi?.setFieldValue('name', `${name} ${version.trim()} 璁捐`);
+ }
+}
+
+/** 浜у搧閫夋嫨锛氶�夋嫨鍚庤嚜鍔ㄥ甫鍑轰骇鍝佺紪鐮� */
+function useProductSelectSchema(
+ formApi: VbenFormApi | undefined,
+ readonly: boolean,
+): VbenFormSchema[] {
+ if (readonly) {
+ return [
+ {
+ fieldName: 'productCode',
+ label: '浜у搧缂栫爜',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'productName',
+ label: '浜у搧鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ ];
+ }
+ return [
+ {
+ fieldName: 'productId',
+ label: '浜у搧',
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨浜у搧',
+ onChange: async (item: MesMdItemApi.Item | undefined) => {
+ await formApi?.setFieldValue('productCode', item?.code ?? '');
+ await formApi?.setFieldValue('productName', item?.name ?? '');
+ await syncTaskNameFromProduct(formApi, item?.name);
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'productCode',
+ label: '浜у搧缂栫爜',
+ component: 'Input',
+ componentProps: {
+ disabled: true,
+ placeholder: '閫夋嫨浜у搧鍚庤嚜鍔ㄥ甫鍑�',
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'productName',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ ];
+}
+
+/** 鍙戣捣璁捐浠诲姟琛ㄥ崟 */
+export function useCreateTaskSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'code',
+ label: '浠诲姟缂栫爜',
+ component: 'Input',
+ componentProps: { placeholder: '璇风偣鍑荤敓鎴愪换鍔$紪鐮�' },
+ rules: 'required',
+ suffix: () =>
+ h(
+ Button,
+ { type: 'default', onClick: () => generateTaskCode(formApi) },
+ { default: () => '鐢熸垚' },
+ ),
+ },
+ ...useProductSelectSchema(formApi, false),
+ {
+ fieldName: 'version',
+ label: '璁捐鐗堟湰',
+ component: 'Input',
+ componentProps: { placeholder: '濡� V1.0銆乂2.1' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'name',
+ label: '浠诲姟鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '灏嗘牴鎹骇鍝佷笌鐗堟湰鑷姩鐢熸垚锛屽彲淇敼' },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['productName', 'version'],
+ trigger(values: Record<string, string>, form) {
+ const productName = values.productName?.trim();
+ const version = values.version?.trim();
+ if (productName && version) {
+ form.setFieldValue('name', `${productName} ${version} 璁捐`);
+ }
+ },
+ },
+ },
+ {
+ fieldName: 'designerName',
+ label: '涓昏璁″笀',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヤ富璁捐甯�' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'reviewerName',
+ label: '瀹℃牳浜�',
+ component: 'Input',
+ componentProps: { placeholder: '璧勬枡瀹℃牳璐熻矗浜�' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'planDateRange',
+ label: '璁″垝鍛ㄦ湡',
+ component: 'RangePicker',
+ componentProps: getRangePickerDefaultProps(),
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '浠诲姟璇存槑',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '绠�杩版湰娆¤璁$洰鏍囥�佸彉鏇磋儗鏅瓑',
+ rows: 3,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+/** 鍙戝竷浠诲姟纭琛ㄥ崟 */
+export function usePublishFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'publishRemark',
+ label: '鍙戝竷璇存槑',
+ component: 'Textarea',
+ componentProps: {
+ placeholder: '鍙�夛紝濉啓鍙戝竷瑕佹眰鎴栨敞鎰忎簨椤�',
+ rows: 3,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+export type WorkflowActionType = 'approve' | 'archive' | 'submit-review';
+
+export interface WorkflowActionConfig {
+ title: string;
+ alertMessage: string;
+ alertDescription: string;
+ remarkLabel: string;
+ remarkTag: string;
+ remarkPlaceholder: string;
+ successMessage: string;
+ alertType: 'info' | 'success' | 'warning';
+}
+
+export const WORKFLOW_ACTION_CONFIG: Record<
+ WorkflowActionType,
+ WorkflowActionConfig
+> = {
+ 'submit-review': {
+ title: '鎻愪氦璧勬枡瀹℃牳',
+ alertMessage: '纭鎻愪氦瀹℃牳',
+ alertDescription:
+ '鎻愪氦鍚庡皢杩涘叆璧勬枡瀹℃牳鐜妭锛岃纭璁捐璧勬枡宸蹭笂浼犲畬鏁淬�傛彁浜ゅ悗璁捐甯堝皢鏃犳硶缁х画淇敼璧勬枡銆�',
+ remarkLabel: '鎻愪氦璇存槑',
+ remarkTag: '銆愭彁浜よ鏄庛��',
+ remarkPlaceholder: '鍙�夛紝濉啓鎻愪氦瀹℃牳鐨勮ˉ鍏呰鏄�',
+ successMessage: '宸叉彁浜よ祫鏂欏鏍�',
+ alertType: 'info',
+ },
+ approve: {
+ title: '瀹℃牳閫氳繃',
+ alertMessage: '纭瀹℃牳閫氳繃',
+ alertDescription:
+ '瀹℃牳閫氳繃鍚庝换鍔¤繘鍏ャ�屽凡閫氳繃銆嶇姸鎬侊紝鍙繘琛屽綊妗f搷浣溿�傝纭璁捐璧勬枡绗﹀悎瑕佹眰銆�',
+ remarkLabel: '瀹℃牳鎰忚',
+ remarkTag: '銆愬鏍告剰瑙併��',
+ remarkPlaceholder: '鍙�夛紝濉啓瀹℃牳閫氳繃鎰忚',
+ successMessage: '璧勬枡瀹℃牳閫氳繃',
+ alertType: 'success',
+ },
+ archive: {
+ title: '褰掓。璁捐浠诲姟',
+ alertMessage: '纭褰掓。',
+ alertDescription:
+ '褰掓。鍚庡皢鐢熸垚鏈増鏈綊妗e彴璐﹁褰曪紝浠诲姟鐘舵�佸彉涓哄凡褰掓。锛岃璁¤祫鏂欏皢閿佸畾涓嶅彲淇敼銆�',
+ remarkLabel: '褰掓。璇存槑',
+ remarkTag: '銆愬綊妗h鏄庛��',
+ remarkPlaceholder: '鍙�夛紝濉啓褰掓。澶囨敞',
+ successMessage: '浠诲姟宸插綊妗�',
+ alertType: 'warning',
+ },
+};
+
+/** 娴佺▼鎿嶄綔纭琛ㄥ崟 */
+export function useWorkflowActionFormSchema(
+ action: WorkflowActionType,
+): VbenFormSchema[] {
+ const config = WORKFLOW_ACTION_CONFIG[action];
+ return [
+ {
+ fieldName: 'actionRemark',
+ label: config.remarkLabel,
+ component: 'Textarea',
+ componentProps: {
+ placeholder: config.remarkPlaceholder,
+ rows: 3,
+ },
+ formItemClass: 'col-span-2',
+ },
+ ];
+}
+
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '浠诲姟缂栫爜',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+ },
+ {
+ fieldName: 'name',
+ label: '浠诲姟鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヤ换鍔″悕绉�' },
+ },
+ {
+ fieldName: 'productCode',
+ label: '浜у搧缂栫爜',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヤ骇鍝佺紪鐮�' },
+ },
+ {
+ fieldName: 'status',
+ label: '浠诲姟鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_PROJECT_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨椤圭洰鐘舵��',
+ },
+ },
+ ];
+}
+
+export function useGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'code', title: '浠诲姟缂栫爜', minWidth: 140, slots: { default: 'code' } },
+ { field: 'name', title: '浠诲姟鍚嶇О', minWidth: 160 },
+ { field: 'productCode', title: '浜у搧缂栫爜', minWidth: 120 },
+ { field: 'productName', title: '浜у搧鍚嶇О', minWidth: 140 },
+ { field: 'version', title: '鐗堟湰鍙�', minWidth: 90 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_PROJECT_STATUS },
+ },
+ },
+ { field: 'designerName', title: '涓昏璁″笀', minWidth: 100 },
+ { field: 'reviewerName', title: '瀹℃牳浜�', minWidth: 100 },
+ { field: 'planEndDate', title: '璁″垝瀹屾垚', minWidth: 120, formatter: 'formatDate' },
+ { field: 'createTime', title: '鍒涘缓鏃堕棿', minWidth: 160, formatter: 'formatDateTime' },
+ { field: 'action', title: '鎿嶄綔', width: 220, fixed: 'right', slots: { default: 'actions' } },
+ ];
+}
+
+export function useFormSchema(
+ formType: FormType,
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ const readonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'code',
+ label: '浠诲姟缂栫爜',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+ rules: 'required',
+ suffix:
+ formType === 'detail'
+ ? undefined
+ : () =>
+ h(
+ Button,
+ {
+ type: 'default',
+ onClick: async () => {
+ const code = isPdMockEnabled()
+ ? mockNextCode(
+ 'DT-2026',
+ Math.floor(Date.now() % 10_000),
+ )
+ : await generateAutoCode(
+ MesAutoCodeRuleCode.PD_PROJECT_CODE,
+ );
+ await formApi?.setFieldValue('code', code);
+ },
+ },
+ { default: () => '鐢熸垚' },
+ ),
+ },
+ ...useProductSelectSchema(formApi, readonly),
+ {
+ fieldName: 'version',
+ label: '鐗堟湰鍙�',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '濡� V1.0' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'name',
+ label: '浠诲姟鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '濡傦細鏅鸿兘娓╂帶妯$粍 V1.0 璁捐' },
+ rules: 'required',
+ dependencies: {
+ triggerFields: ['productName', 'version'],
+ trigger(values: Record<string, string>, form) {
+ if (readonly) {
+ return;
+ }
+ const productName = values.productName?.trim();
+ const version = values.version?.trim();
+ if (productName && version) {
+ form.setFieldValue('name', `${productName} ${version} 璁捐`);
+ }
+ },
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '浠诲姟鐘舵��',
+ component: 'Select',
+ componentProps: {
+ disabled: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_PROJECT_STATUS, 'number'),
+ },
+ dependencies: { triggerFields: [''], show: () => formType !== 'create' },
+ },
+ {
+ fieldName: 'designerName',
+ label: '涓昏璁″笀',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ富璁捐甯�' },
+ },
+ {
+ fieldName: 'reviewerName',
+ label: '瀹℃牳浜�',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ鏍镐汉' },
+ },
+ {
+ fieldName: 'planDateRange',
+ label: '璁″垝鍛ㄦ湡',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ disabled: readonly,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+ formItemClass: 'col-span-3',
+ },
+ ];
+}
+
+/** 璁捐璧勬枡绫诲瀷閫夐」锛堜笂浼犳椂閫夋嫨锛� */
+export function getPdDocumentTypeOptions() {
+ return [
+ { label: '璁捐鏂规', value: MesPdDocumentTypeEnum.SCHEME },
+ { label: '璁捐鍥剧焊', value: MesPdDocumentTypeEnum.DRAWING },
+ { label: '璁捐妯″瀷', value: MesPdDocumentTypeEnum.MODEL },
+ { label: '浜у搧璁捐', value: MesPdDocumentTypeEnum.PRODUCT_DESIGN },
+ { label: 'BOM 娓呭崟', value: MesPdDocumentTypeEnum.BOM },
+ { label: '鍏朵粬闄勪欢', value: MesPdDocumentTypeEnum.OTHER },
+ ];
+}
+
+/** 缁熶竴璁捐璧勬枡鍒楄〃鍒� */
+export function useMaterialListGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '璧勬枡鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヨ祫鏂欏悕绉�' },
+ },
+ {
+ fieldName: 'documentType',
+ label: '璧勬枡绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getPdDocumentTypeOptions(),
+ placeholder: '璇烽�夋嫨璧勬枡绫诲瀷',
+ },
+ },
+ ];
+}
+
+export function useMaterialListGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ {
+ field: 'documentType',
+ title: '璧勬枡绫诲瀷',
+ minWidth: 110,
+ formatter: ({ cellValue }: { cellValue: number }) => {
+ const option = getPdDocumentTypeOptions().find(
+ (item) => item.value === cellValue,
+ );
+ return option?.label ?? String(cellValue ?? '');
+ },
+ },
+ { field: 'name', title: '璧勬枡鍚嶇О', minWidth: 180 },
+ { field: 'version', title: '鐗堟湰', minWidth: 90 },
+ { field: 'fileName', title: '鏂囦欢鍚�', minWidth: 200 },
+ { field: 'uploaderName', title: '涓婁紶浜�', minWidth: 100 },
+ {
+ field: 'createTime',
+ title: '涓婁紶鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 140,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** @deprecated 鎸夌被鍨嬪垎 Tab 鏃朵娇鐢紝璇锋敼鐢� useMaterialListGridColumns */
+export function useDocumentGridColumns(documentType: number): VxeTableGridOptions['columns'] {
+ const typeLabel =
+ documentType === MesPdDocumentTypeEnum.SCHEME
+ ? '鏂规'
+ : documentType === MesPdDocumentTypeEnum.DRAWING
+ ? '鍥剧焊'
+ : documentType === MesPdDocumentTypeEnum.MODEL
+ ? '妯″瀷'
+ : documentType === MesPdDocumentTypeEnum.PRODUCT_DESIGN
+ ? '浜у搧璁捐'
+ : '璧勬枡';
+
+ return [
+ { field: 'name', title: `${typeLabel}鍚嶇О`, minWidth: 180 },
+ { field: 'version', title: '鐗堟湰', minWidth: 90 },
+ { field: 'fileName', title: '鏂囦欢鍚�', minWidth: 200 },
+ { field: 'uploaderName', title: '涓婁紶浜�', minWidth: 100 },
+ { field: 'createTime', title: '涓婁紶鏃堕棿', minWidth: 160, formatter: 'formatDateTime' },
+ { field: 'action', title: '鎿嶄綔', width: 140, slots: { default: 'actions' } },
+ ];
+}
+
+/** BOM 琛ㄦ牸鍒� */
+export function useBomGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'itemCode', title: '鐗╂枡缂栫爜', minWidth: 120 },
+ { field: 'itemName', title: '鐗╂枡鍚嶇О', minWidth: 160 },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', minWidth: 140 },
+ { field: 'unitMeasureName', title: '鍗曚綅', minWidth: 80 },
+ { field: 'quantity', title: '鐢ㄩ噺', minWidth: 90 },
+ { field: 'remark', title: '澶囨敞', minWidth: 140 },
+ { field: 'action', title: '鎿嶄綔', width: 140, slots: { default: 'actions' } },
+ ];
+}
+
+export type { MesPdProjectApi };
diff --git a/src/views/mes/pd/project/detail/data.ts b/src/views/mes/pd/project/detail/data.ts
new file mode 100644
index 0000000..5ad04da
--- /dev/null
+++ b/src/views/mes/pd/project/detail/data.ts
@@ -0,0 +1,43 @@
+import type { DescriptionItemSchema } from '#/components/description';
+
+import { h } from 'vue';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { formatDate, formatDateTime } from '#/packages/utils/src';
+
+import { DictTag } from '#/components/dict-tag';
+
+/** 璁捐浠诲姟璇︽儏鎽樿 */
+export function useDetailSchema(): DescriptionItemSchema[] {
+ return [
+ { field: 'code', label: '浠诲姟缂栫爜' },
+ { field: 'name', label: '浠诲姟鍚嶇О' },
+ { field: 'productCode', label: '浜у搧缂栫爜' },
+ { field: 'productName', label: '浜у搧鍚嶇О' },
+ { field: 'version', label: '璁捐鐗堟湰' },
+ {
+ field: 'status',
+ label: '浠诲姟鐘舵��',
+ render: (val) =>
+ h(DictTag, { type: DICT_TYPE.MES_PD_PROJECT_STATUS, value: val }),
+ },
+ { field: 'designerName', label: '璁捐甯�' },
+ { field: 'reviewerName', label: '瀹℃牳浜�' },
+ {
+ field: 'planStartDate',
+ label: '璁″垝寮�濮�',
+ render: (val) => (val ? formatDate(val) : '-'),
+ },
+ {
+ field: 'planEndDate',
+ label: '璁″垝瀹屾垚',
+ render: (val) => (val ? formatDate(val) : '-'),
+ },
+ {
+ field: 'createTime',
+ label: '鍙戣捣鏃堕棿',
+ render: (val) => (val ? formatDateTime(val) : '-'),
+ },
+ { field: 'remark', label: '澶囨敞' },
+ ];
+}
diff --git a/src/views/mes/pd/project/detail/index.vue b/src/views/mes/pd/project/detail/index.vue
new file mode 100644
index 0000000..ea6bf27
--- /dev/null
+++ b/src/views/mes/pd/project/detail/index.vue
@@ -0,0 +1,224 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { computed, onMounted, ref } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { useTabs } from '#/packages/effects/hooks/src';
+import { MesPdProjectStatusEnum } from '#/packages/constants/src';
+
+import { Card, Tabs } from 'ant-design-vue';
+
+import {
+ getPdProject,
+} from '#/api/mes/pd/project';
+import { useDescription } from '#/components/description';
+import { ACTION_ICON, TableAction } from '#/adapter/vxe-table';
+import { $t } from '#/locales';
+
+import Form from '../modules/form.vue';
+import PublishForm from '../modules/publish-form.vue';
+import WorkflowActionForm from '../modules/workflow-action-form.vue';
+import ReviewList from '../modules/drawing-review/review-list.vue';
+import MaterialPanel from '../modules/material-panel.vue';
+import TaskDraftBanner from '../modules/task-draft-banner.vue';
+import WorkflowTimeline from '../modules/workflow-timeline.vue';
+import { useDetailSchema } from './data';
+
+const route = useRoute();
+const router = useRouter();
+const tabs = useTabs();
+
+const loading = ref(false);
+const projectId = ref(0);
+const project = ref<MesPdProjectApi.Project>({});
+const activeTab = ref('workflow');
+const workflowKey = ref(0);
+
+const isDraft = computed(
+ () => project.value.status === MesPdProjectStatusEnum.DRAFT,
+);
+const isDesigning = computed(
+ () => project.value.status === MesPdProjectStatusEnum.DESIGNING,
+);
+const isReviewing = computed(
+ () => project.value.status === MesPdProjectStatusEnum.REVIEWING,
+);
+const isApproved = computed(
+ () => project.value.status === MesPdProjectStatusEnum.APPROVED,
+);
+const isArchived = computed(
+ () => project.value.status === MesPdProjectStatusEnum.ARCHIVED,
+);
+
+const isFormEditable = computed(
+ () =>
+ !isArchived.value &&
+ project.value.status !== MesPdProjectStatusEnum.CANCELLED &&
+ (isDraft.value || isDesigning.value),
+);
+
+const materialFormType = computed<FormType>(() =>
+ isDesigning.value ? 'update' : 'detail',
+);
+
+const showReviewTab = computed(
+ () => isReviewing.value || isApproved.value || isArchived.value,
+);
+
+const [Descriptions] = useDescription({
+ bordered: false,
+ column: 4,
+ schema: useDetailSchema(),
+});
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [PublishModal, publishModalApi] = useVbenModal({
+ connectedComponent: PublishForm,
+ destroyOnClose: true,
+});
+
+const [WorkflowActionModal, workflowActionModalApi] = useVbenModal({
+ connectedComponent: WorkflowActionForm,
+ destroyOnClose: true,
+});
+
+async function loadProjectDetail() {
+ loading.value = true;
+ try {
+ project.value = await getPdProject(projectId.value);
+ workflowKey.value += 1;
+ if (isDesigning.value) {
+ activeTab.value = 'material';
+ } else if (isReviewing.value) {
+ activeTab.value = 'review';
+ }
+ } finally {
+ loading.value = false;
+ }
+}
+
+function handleBack() {
+ tabs.closeCurrentTab();
+ router.push({ path: '/mes/pd/project' });
+}
+
+function handleEdit() {
+ formModalApi.setData({ formType: 'update', id: projectId.value }).open();
+}
+
+function handlePublish() {
+ publishModalApi.setData({ id: projectId.value }).open();
+}
+
+function handleWorkflowAction(action: 'approve' | 'archive' | 'submit-review') {
+ workflowActionModalApi.setData({ id: projectId.value, action }).open();
+}
+
+onMounted(() => {
+ projectId.value = Number(route.params.id);
+ loadProjectDetail();
+});
+</script>
+
+<template>
+ <Page
+ auto-content-height
+ :loading="loading"
+ :title="project.name || '璁捐浠诲姟璇︽儏'"
+ >
+ <FormModal @success="loadProjectDetail" />
+ <PublishModal @success="loadProjectDetail" />
+ <WorkflowActionModal @success="loadProjectDetail" />
+
+ <template #extra>
+ <TableAction
+ :actions="[
+ {
+ label: '杩斿洖',
+ type: 'default',
+ icon: 'lucide:arrow-left',
+ onClick: handleBack,
+ },
+ {
+ label: $t('common.edit'),
+ type: 'primary',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-project:update'],
+ ifShow: isFormEditable,
+ onClick: handleEdit,
+ },
+ {
+ label: '鍙戝竷浠诲姟',
+ type: 'primary',
+ auth: ['mes:pd-project:update'],
+ ifShow: isDraft,
+ onClick: handlePublish,
+ },
+ {
+ label: '鎻愪氦瀹℃牳',
+ type: 'default',
+ auth: ['mes:pd-project:submit-review'],
+ ifShow: isDesigning,
+ onClick: () => handleWorkflowAction('submit-review'),
+ },
+ {
+ label: '瀹℃牳閫氳繃',
+ type: 'primary',
+ auth: ['mes:pd-drawing-review:review'],
+ ifShow: isReviewing,
+ onClick: () => handleWorkflowAction('approve'),
+ },
+ {
+ label: '褰掓。',
+ type: 'default',
+ auth: ['mes:pd-project:archive'],
+ ifShow: isApproved,
+ onClick: () => handleWorkflowAction('archive'),
+ },
+ ]"
+ />
+ </template>
+
+ <TaskDraftBanner
+ v-if="isDraft"
+ :designer-name="project.designerName"
+ :product-name="project.productName"
+ :version="project.version"
+ @edit="handleEdit"
+ @publish="handlePublish"
+ />
+
+ <Card class="min-h-[10%]">
+ <Descriptions :data="project" />
+ </Card>
+
+ <Card class="mt-4 min-h-[60%]">
+ <Tabs v-model:active-key="activeTab">
+ <Tabs.TabPane key="workflow" tab="娴佺▼杩涘害" :force-render="true">
+ <WorkflowTimeline :key="workflowKey" :project-id="projectId" />
+ </Tabs.TabPane>
+ <Tabs.TabPane key="material" tab="璁捐璧勬枡" :force-render="true">
+ <p v-if="isDraft" class="mb-3 text-sm text-amber-600">
+ 璇峰厛鍙戝竷浠诲姟鍚庡啀涓婁紶璁捐璧勬枡
+ </p>
+ <MaterialPanel :form-type="materialFormType" :project-id="projectId" />
+ </Tabs.TabPane>
+ <Tabs.TabPane
+ v-if="showReviewTab"
+ key="review"
+ tab="璧勬枡瀹℃牳"
+ :force-render="true"
+ >
+ <ReviewList form-type="detail" :project-id="projectId" />
+ </Tabs.TabPane>
+ </Tabs>
+ </Card>
+ </Page>
+</template>
diff --git a/src/views/mes/pd/project/index.vue b/src/views/mes/pd/project/index.vue
new file mode 100644
index 0000000..1428d31
--- /dev/null
+++ b/src/views/mes/pd/project/index.vue
@@ -0,0 +1,212 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '#/packages/effects/common-ui/src';
+import { MesPdProjectStatusEnum } from '#/packages/constants/src';
+import { downloadFileFromBlobPart } from '#/packages/utils/src';
+
+import { Button, message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deletePdProject,
+ exportPdProject,
+ getPdProjectPage,
+} from '#/api/mes/pd/project';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import CreateForm from './modules/create-form.vue';
+import Form from './modules/form.vue';
+import PublishForm from './modules/publish-form.vue';
+import WorkflowActionForm from './modules/workflow-action-form.vue';
+
+const router = useRouter();
+
+const [CreateFormModal, createFormModalApi] = useVbenModal({
+ connectedComponent: CreateForm,
+ destroyOnClose: true,
+});
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [PublishModal, publishModalApi] = useVbenModal({
+ connectedComponent: PublishForm,
+ destroyOnClose: true,
+});
+
+const [WorkflowActionModal, workflowActionModalApi] = useVbenModal({
+ connectedComponent: WorkflowActionForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreateSuccess(projectId: number) {
+ handleRefresh();
+ router.push({ name: 'MesPdProjectDetail', params: { id: projectId } });
+}
+
+function handleCreate() {
+ createFormModalApi.open();
+}
+
+function handleDetail(row: MesPdProjectApi.Project) {
+ router.push({ name: 'MesPdProjectDetail', params: { id: row.id } });
+}
+
+function handleEdit(row: MesPdProjectApi.Project) {
+ formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+function handlePublish(row: MesPdProjectApi.Project) {
+ publishModalApi.setData({ id: row.id }).open();
+}
+
+function handleWorkflowAction(
+ row: MesPdProjectApi.Project,
+ action: 'approve' | 'archive' | 'submit-review',
+) {
+ workflowActionModalApi.setData({ id: row.id, action }).open();
+}
+
+async function handleDelete(row: MesPdProjectApi.Project) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.name]),
+ duration: 0,
+ });
+ try {
+ await deletePdProject(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+async function handleExport() {
+ const data = await exportPdProject(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '璁捐浠诲姟.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useGridFormSchema() },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPdProjectPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesPdProjectApi.Project>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <CreateFormModal @success="handleCreateSuccess" />
+ <FormModal @success="handleRefresh" />
+ <PublishModal @success="handleRefresh" />
+ <WorkflowActionModal @success="handleRefresh" />
+
+ <Grid table-title="璁捐浠诲姟">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鍙戣捣璁捐浠诲姟',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pd-project:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pd-project:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鍙戝竷浠诲姟',
+ type: 'link',
+ auth: ['mes:pd-project:update'],
+ ifShow: row.status === MesPdProjectStatusEnum.DRAFT,
+ onClick: handlePublish.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-project:update'],
+ ifShow:
+ row.status === MesPdProjectStatusEnum.DRAFT ||
+ row.status === MesPdProjectStatusEnum.DESIGNING,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎻愪氦瀹℃牳',
+ type: 'link',
+ auth: ['mes:pd-project:submit-review'],
+ ifShow: row.status === MesPdProjectStatusEnum.DESIGNING,
+ onClick: () => handleWorkflowAction(row, 'submit-review'),
+ },
+ {
+ label: '瀹℃牳閫氳繃',
+ type: 'link',
+ auth: ['mes:pd-drawing-review:review'],
+ ifShow: row.status === MesPdProjectStatusEnum.REVIEWING,
+ onClick: () => handleWorkflowAction(row, 'approve'),
+ },
+ {
+ label: '褰掓。',
+ type: 'link',
+ auth: ['mes:pd-project:archive'],
+ ifShow: row.status === MesPdProjectStatusEnum.APPROVED,
+ onClick: () => handleWorkflowAction(row, 'archive'),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pd-project:delete'],
+ ifShow: row.status === MesPdProjectStatusEnum.DRAFT,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/pd/project/modules/bom-form.vue b/src/views/mes/pd/project/modules/bom-form.vue
new file mode 100644
index 0000000..7699f06
--- /dev/null
+++ b/src/views/mes/pd/project/modules/bom-form.vue
@@ -0,0 +1,124 @@
+<script lang="ts" setup>
+import type { MesMdItemApi } from '#/api/mes/md/item';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createPdBomLine, updatePdBomLine } from '#/api/mes/pd/bom';
+import { $t } from '#/locales';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+const emit = defineEmits(['success']);
+
+const isUpdate = ref(false);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'projectId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'itemId',
+ label: '鐗╂枡',
+ component: MdItemSelect,
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐗╂枡',
+ onChange: async (item: MesMdItemApi.Item | undefined) => {
+ await formApi.setValues({
+ itemCode: item?.code,
+ itemName: item?.name,
+ specification: item?.specification,
+ unitMeasureName: item?.unitMeasureName,
+ });
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'itemCode',
+ label: '鐗╂枡缂栫爜',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'itemName',
+ label: '鐗╂枡鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'quantity',
+ label: '鐢ㄩ噺',
+ component: 'InputNumber',
+ componentProps: { min: 0, precision: 4, class: 'w-full' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: { placeholder: '璇疯緭鍏ュ娉�', rows: 2 },
+ },
+ ],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesPdBomApi.BomLine;
+ try {
+ if (isUpdate.value) {
+ await updatePdBomLine(data);
+ } else {
+ await createPdBomLine(data);
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ projectId: number; row?: MesPdBomApi.BomLine }>();
+ isUpdate.value = !!data.row?.id;
+ modalApi.setState({ title: isUpdate.value ? '缂栬緫 BOM 琛�' : '娣诲姞 BOM 琛�' });
+ if (data.row) {
+ await formApi.setValues(data.row);
+ return;
+ }
+ await formApi.setValues({ projectId: data.projectId, quantity: 1 });
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[560px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/bom-list.vue b/src/views/mes/pd/project/modules/bom-list.vue
new file mode 100644
index 0000000..a6874ca
--- /dev/null
+++ b/src/views/mes/pd/project/modules/bom-list.vue
@@ -0,0 +1,122 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdBomApi } from '#/api/mes/pd/bom';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { deletePdBomLine, getPdBomPage } from '#/api/mes/pd/bom';
+import { $t } from '#/locales';
+
+import { useBomGridColumns } from '../data';
+import BomForm from './bom-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ projectId: number;
+}>();
+
+const isEditable = computed(() =>
+ ['create', 'update'].includes(props.formType),
+);
+
+const [BomFormModal, bomFormModalApi] = useVbenModal({
+ connectedComponent: BomForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ bomFormModalApi.setData({ projectId: props.projectId }).open();
+}
+
+function handleEdit(row: MesPdBomApi.BomLine) {
+ bomFormModalApi.setData({ row, projectId: props.projectId }).open();
+}
+
+async function handleDelete(row: MesPdBomApi.BomLine) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.itemName]),
+ duration: 0,
+ });
+ try {
+ await deletePdBomLine(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.itemName]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ columns: useBomGridColumns(),
+ height: 360,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }) => {
+ return await getPdBomPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ projectId: props.projectId,
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true },
+ } as VxeTableGridOptions<MesPdBomApi.BomLine>,
+});
+</script>
+
+<template>
+ <BomFormModal @success="handleRefresh" />
+ <Grid>
+ <template v-if="isEditable" #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '娣诲姞 BOM 琛�',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ ifShow: isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ ifShow: isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.itemName]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/create-form.vue b/src/views/mes/pd/project/modules/create-form.vue
new file mode 100644
index 0000000..0663079
--- /dev/null
+++ b/src/views/mes/pd/project/modules/create-form.vue
@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { Alert, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createPdProject } from '#/api/mes/pd/project';
+import { $t } from '#/locales';
+
+import { useCreateTaskSchema } from '../data';
+
+const emit = defineEmits<{
+ success: [projectId: number];
+}>();
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-2',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const raw = (await formApi.getValues()) as MesPdProjectApi.Project & {
+ planDateRange?: [string, string];
+ productId?: number;
+ };
+ const { productId: _productId, planDateRange, ...data } = raw;
+ if (planDateRange?.length === 2) {
+ data.planStartDate = new Date(planDateRange[0]).getTime();
+ data.planEndDate = new Date(planDateRange[1]).getTime();
+ }
+ try {
+ const id = await createPdProject(data);
+ emit('success', id);
+ message.success('璁捐浠诲姟宸插彂璧凤紝璇峰彂甯冧换鍔″悗寮�濮嬩笂浼犺祫鏂�');
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ await formApi.resetForm();
+ return;
+ }
+ formApi.setState({ schema: useCreateTaskSchema(formApi) });
+ modalApi.setState({ title: '鍙戣捣璁捐浠诲姟' });
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[720px]">
+ <Alert
+ class="mx-4 mb-4"
+ message="鍙戣捣浠诲姟"
+ description="涓烘寚瀹氫骇鍝佺殑鏌愪竴璁捐鐗堟湰鍒涘缓璁捐浠诲姟銆備换鍔′繚瀛樹负鑽夌锛屽彂甯冨悗鏂瑰彲涓婁紶璁捐璧勬枡銆�"
+ show-icon
+ type="info"
+ />
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/document-form.vue b/src/views/mes/pd/project/modules/document-form.vue
new file mode 100644
index 0000000..9d015cf
--- /dev/null
+++ b/src/views/mes/pd/project/modules/document-form.vue
@@ -0,0 +1,142 @@
+<script lang="ts" setup>
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { MesPdDocumentTypeEnum } from '#/packages/constants/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createPdDocument,
+ getPdDocument,
+ updatePdDocument,
+} from '#/api/mes/pd/document';
+import { $t } from '#/locales';
+
+import { getPdDocumentTypeOptions } from '../data';
+
+const emit = defineEmits(['success']);
+
+const isUpdate = ref(false);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'projectId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'documentType',
+ label: '璧勬枡绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ options: getPdDocumentTypeOptions(),
+ placeholder: '璇烽�夋嫨璧勬枡绫诲瀷',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'name',
+ label: '璧勬枡鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヨ祫鏂欏悕绉�' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'version',
+ label: '鐗堟湰鍙�',
+ component: 'Input',
+ componentProps: { placeholder: '濡� V1.0' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'fileUrl',
+ label: '鏂囦欢鍦板潃',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ユ枃浠� URL 鎴栦笂浼犲悗濉叆' },
+ },
+ {
+ fieldName: 'fileName',
+ label: '鏂囦欢鍚�',
+ component: 'Input',
+ componentProps: { placeholder: '濡� bom-list.xlsx' },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: { placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+ },
+ ],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesPdDocumentApi.Document;
+ try {
+ if (isUpdate.value) {
+ await updatePdDocument(data);
+ } else {
+ await createPdDocument(data);
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{
+ id?: number;
+ projectId: number;
+ documentType?: number;
+ }>();
+ isUpdate.value = !!data.id;
+ modalApi.setState({ title: isUpdate.value ? '缂栬緫闄勪欢' : '涓婁紶闄勪欢' });
+ if (data.id) {
+ modalApi.lock();
+ try {
+ const detail = await getPdDocument(data.id);
+ await formApi.setValues(detail);
+ } finally {
+ modalApi.unlock();
+ }
+ return;
+ }
+ await formApi.setValues({
+ projectId: data.projectId,
+ documentType: data.documentType ?? MesPdDocumentTypeEnum.SCHEME,
+ });
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[560px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/document-list.vue b/src/views/mes/pd/project/modules/document-list.vue
new file mode 100644
index 0000000..cfb129f
--- /dev/null
+++ b/src/views/mes/pd/project/modules/document-list.vue
@@ -0,0 +1,165 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdDocumentApi } from '#/api/mes/pd/document';
+
+import { computed, watch } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { MesPdDocumentTypeEnum } from '#/packages/constants/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deletePdDocument,
+ getPdDocumentPage,
+} from '#/api/mes/pd/document';
+import { $t } from '#/locales';
+
+import {
+ useMaterialListGridColumns,
+ useMaterialListGridFormSchema,
+} from '../data';
+import DocumentForm from './document-form.vue';
+
+const props = withDefaults(
+ defineProps<{
+ formType: FormType;
+ gridHeight?: number | string;
+ projectId: number;
+ tableTitle?: string;
+ version?: string;
+ }>(),
+ {
+ gridHeight: 360,
+ tableTitle: '璁捐璧勬枡',
+ },
+);
+
+const isEditable = computed(() =>
+ ['create', 'update'].includes(props.formType),
+);
+
+const [DocumentFormModal, documentFormModalApi] = useVbenModal({
+ connectedComponent: DocumentForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ documentFormModalApi
+ .setData({
+ projectId: props.projectId,
+ documentType: MesPdDocumentTypeEnum.SCHEME,
+ })
+ .open();
+}
+
+function handleEdit(row: MesPdDocumentApi.Document) {
+ documentFormModalApi.setData({ id: row.id, projectId: props.projectId }).open();
+}
+
+async function handleDelete(row: MesPdDocumentApi.Document) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.name]),
+ duration: 0,
+ });
+ try {
+ await deletePdDocument(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+function handlePreview(row: MesPdDocumentApi.Document) {
+ if (row.fileUrl) {
+ window.open(row.fileUrl, '_blank');
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useMaterialListGridFormSchema() },
+ gridOptions: {
+ columns: useMaterialListGridColumns(),
+ height: props.gridHeight,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPdDocumentPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ projectId: props.projectId,
+ version: props.version,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesPdDocumentApi.Document>,
+});
+
+watch(
+ () => props.version,
+ () => {
+ gridApi.query();
+ },
+);
+</script>
+
+<template>
+ <DocumentFormModal @success="handleRefresh" />
+ <Grid :table-title="tableTitle">
+ <template v-if="isEditable" #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '涓婁紶闄勪欢',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '棰勮',
+ type: 'link',
+ ifShow: !!row.fileUrl,
+ onClick: handlePreview.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ ifShow: isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ ifShow: isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/drawing-review/data.ts b/src/views/mes/pd/project/modules/drawing-review/data.ts
new file mode 100644
index 0000000..5b598d8
--- /dev/null
+++ b/src/views/mes/pd/project/modules/drawing-review/data.ts
@@ -0,0 +1,88 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+export function useDrawingReviewGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '瀹℃牳鍗曞彿',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ュ鏍稿崟鍙�' },
+ },
+ {
+ fieldName: 'status',
+ label: '瀹℃牳鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_DRAWING_REVIEW_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨瀹℃牳鐘舵��',
+ },
+ },
+ ];
+}
+
+export function useDrawingReviewGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'code', title: '瀹℃牳鍗曞彿', minWidth: 130 },
+ { field: 'documentName', title: '璧勬枡鍚嶇О', minWidth: 160 },
+ { field: 'documentVersion', title: '璧勬枡鐗堟湰', minWidth: 90 },
+ { field: 'reviewerName', title: '瀹℃牳浜�', minWidth: 100 },
+ {
+ field: 'status',
+ title: '瀹℃牳鐘舵��',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_DRAWING_REVIEW_STATUS },
+ },
+ },
+ {
+ field: 'reviewTime',
+ title: '瀹℃牳鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+export function useDrawingReviewFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'documentName',
+ label: '璧勬枡鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'status',
+ label: '瀹℃牳缁撴灉',
+ component: 'RadioGroup',
+ componentProps: {
+ options: getDictOptions(DICT_TYPE.MES_PD_DRAWING_REVIEW_STATUS, 'number'),
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'reviewComment',
+ label: '瀹℃牳鎰忚',
+ component: 'Textarea',
+ componentProps: { placeholder: '璇疯緭鍏ュ鏍告剰瑙�', rows: 4 },
+ },
+ ];
+}
diff --git a/src/views/mes/pd/project/modules/drawing-review/review-form.vue b/src/views/mes/pd/project/modules/drawing-review/review-form.vue
new file mode 100644
index 0000000..9bdaf2b
--- /dev/null
+++ b/src/views/mes/pd/project/modules/drawing-review/review-form.vue
@@ -0,0 +1,63 @@
+<script lang="ts" setup>
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getPdDrawingReview, reviewPdDrawing } from '#/api/mes/pd/drawing-review';
+import { $t } from '#/locales';
+
+import { useDrawingReviewFormSchema } from './data';
+
+const emit = defineEmits(['success']);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: useDrawingReviewFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const data = await formApi.getValues();
+ await reviewPdDrawing(data as MesPdDrawingReviewApi.ReviewParams);
+ emit('success');
+ message.success('瀹℃牳瀹屾垚');
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ modalApi.lock();
+ try {
+ const detail = await getPdDrawingReview(data.id);
+ await formApi.setValues(detail);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[560px]" title="璧勬枡瀹℃牳">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/drawing-review/review-list.vue b/src/views/mes/pd/project/modules/drawing-review/review-list.vue
new file mode 100644
index 0000000..c08b75a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/drawing-review/review-list.vue
@@ -0,0 +1,77 @@
+<script lang="ts" setup>
+import type { FormType } from '../../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdDrawingReviewApi } from '#/api/mes/pd/drawing-review';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getPdDrawingReviewPage } from '#/api/mes/pd/drawing-review';
+
+import {
+ useDrawingReviewGridColumns,
+ useDrawingReviewGridFormSchema,
+} from './data';
+import ReviewForm from './review-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ projectId: number;
+}>();
+
+const [ReviewModal, reviewModalApi] = useVbenModal({
+ connectedComponent: ReviewForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleReview(row: MesPdDrawingReviewApi.DrawingReview) {
+ reviewModalApi.setData({ id: row.id }).open();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useDrawingReviewGridFormSchema() },
+ gridOptions: {
+ columns: useDrawingReviewGridColumns(),
+ height: 360,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPdDrawingReviewPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ projectId: props.projectId,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesPdDrawingReviewApi.DrawingReview>,
+});
+</script>
+
+<template>
+ <ReviewModal @success="handleRefresh" />
+ <Grid table-title="璧勬枡瀹℃牳">
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '瀹℃牳',
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-drawing-review:review'],
+ onClick: handleReview.bind(null, row),
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/form.vue b/src/views/mes/pd/project/modules/form.vue
new file mode 100644
index 0000000..375f096
--- /dev/null
+++ b/src/views/mes/pd/project/modules/form.vue
@@ -0,0 +1,106 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getPdProject, updatePdProject } from '#/api/mes/pd/project';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const formType = ref<FormType>('update');
+const formData = ref<MesPdProjectApi.Project>();
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return '鏌ョ湅璁捐浠诲姟';
+ }
+ return '缂栬緫璁捐浠诲姟';
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ wrapperClass: 'grid-cols-3',
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const raw = (await formApi.getValues()) as MesPdProjectApi.Project & {
+ planDateRange?: [string, string];
+ productId?: number;
+ };
+ const { productId: _productId, planDateRange, ...data } = raw;
+ if (planDateRange?.length === 2) {
+ data.planStartDate = new Date(planDateRange[0]).getTime();
+ data.planEndDate = new Date(planDateRange[1]).getTime();
+ }
+ try {
+ await updatePdProject(data);
+ formData.value = { ...formData.value, ...data };
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } 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.setState({ schema: useFormSchema(data.formType, formApi) });
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getPdProject(data.id);
+ await formApi.setValues({
+ ...formData.value,
+ planDateRange:
+ formData.value.planStartDate && formData.value.planEndDate
+ ? [formData.value.planStartDate, formData.value.planEndDate]
+ : undefined,
+ });
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/material-panel.vue b/src/views/mes/pd/project/modules/material-panel.vue
new file mode 100644
index 0000000..63daa47
--- /dev/null
+++ b/src/views/mes/pd/project/modules/material-panel.vue
@@ -0,0 +1,28 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import DocumentList from './document-list.vue';
+
+withDefaults(
+ defineProps<{
+ formType: FormType;
+ gridHeight?: number | string;
+ projectId: number;
+ tableTitle?: string;
+ version?: string;
+ }>(),
+ {
+ tableTitle: '璁捐璧勬枡',
+ },
+);
+</script>
+
+<template>
+ <DocumentList
+ :form-type="formType"
+ :grid-height="gridHeight"
+ :project-id="projectId"
+ :table-title="tableTitle"
+ :version="version"
+ />
+</template>
diff --git a/src/views/mes/pd/project/modules/publish-form.vue b/src/views/mes/pd/project/modules/publish-form.vue
new file mode 100644
index 0000000..dd9ad6f
--- /dev/null
+++ b/src/views/mes/pd/project/modules/publish-form.vue
@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { formatDate } from '#/packages/utils/src';
+
+import { Alert, Descriptions, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ getPdProject,
+ publishPdProject,
+ updatePdProject,
+} from '#/api/mes/pd/project';
+
+import { usePublishFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+
+const project = ref<MesPdProjectApi.Project>();
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: usePublishFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!project.value?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const { publishRemark } = await formApi.getValues();
+ if (publishRemark) {
+ const mergedRemark = [project.value.remark, `銆愬彂甯冭鏄庛��${publishRemark}`]
+ .filter(Boolean)
+ .join('\n');
+ await updatePdProject({ ...project.value, remark: mergedRemark });
+ }
+ await publishPdProject(project.value.id);
+ emit('success');
+ message.success('浠诲姟宸插彂甯冿紝璇蜂笂浼犺璁¤祫鏂�');
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ project.value = undefined;
+ await formApi.resetForm();
+ return;
+ }
+ const data = modalApi.getData<{ id: number }>();
+ modalApi.setState({ title: '鍙戝竷璁捐浠诲姟' });
+ modalApi.lock();
+ try {
+ project.value = await getPdProject(data.id);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[640px]">
+ <Alert
+ class="mx-4 mb-4"
+ message="纭鍙戝竷"
+ description="鍙戝竷鍚庝换鍔¤繘鍏ャ�岃璁′腑銆嶇姸鎬侊紝璁捐甯堝彲涓婁紶璁捐璧勬枡銆傚彂甯冨墠璇风‘璁や骇鍝併�佺増鏈強浜哄憳瀹夋帓鏃犺銆�"
+ show-icon
+ type="warning"
+ />
+ <Descriptions
+ v-if="project"
+ bordered
+ class="mx-4 mb-4"
+ :column="2"
+ size="small"
+ >
+ <Descriptions.Item label="浠诲姟缂栫爜">{{ project.code }}</Descriptions.Item>
+ <Descriptions.Item label="浠诲姟鍚嶇О">{{ project.name }}</Descriptions.Item>
+ <Descriptions.Item label="浜у搧缂栫爜">{{ project.productCode }}</Descriptions.Item>
+ <Descriptions.Item label="浜у搧鍚嶇О">{{ project.productName }}</Descriptions.Item>
+ <Descriptions.Item label="璁捐鐗堟湰">{{ project.version }}</Descriptions.Item>
+ <Descriptions.Item label="涓昏璁″笀">{{ project.designerName || '-' }}</Descriptions.Item>
+ <Descriptions.Item label="瀹℃牳浜�">{{ project.reviewerName || '-' }}</Descriptions.Item>
+ <Descriptions.Item label="璁″垝瀹屾垚">
+ {{ project.planEndDate ? formatDate(project.planEndDate) : '-' }}
+ </Descriptions.Item>
+ </Descriptions>
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/task-draft-banner.vue b/src/views/mes/pd/project/modules/task-draft-banner.vue
new file mode 100644
index 0000000..c812428
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task-draft-banner.vue
@@ -0,0 +1,32 @@
+<script lang="ts" setup>
+import { Button } from 'ant-design-vue';
+
+defineProps<{
+ designerName?: string;
+ productName?: string;
+ version?: string;
+}>();
+
+const emit = defineEmits<{
+ edit: [];
+ publish: [];
+}>();
+</script>
+
+<template>
+ <div
+ class="mb-4 flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 px-4 py-3"
+ >
+ <div class="text-sm text-amber-900">
+ <span class="font-medium">浠诲姟宸插彂璧凤紙鑽夌锛�</span>
+ <span class="ml-2 text-amber-700">
+ {{ productName }} {{ version }} 路 璁捐甯� {{ designerName || '鏈寚瀹�' }}
+ </span>
+ <span class="ml-2 text-amber-600">鈥� 璇风‘璁や俊鎭悗鍙戝竷浠诲姟锛屽彂甯冨悗鏂瑰彲涓婁紶璁捐璧勬枡</span>
+ </div>
+ <div class="flex shrink-0 gap-2">
+ <Button size="small" @click="emit('edit')">淇敼浠诲姟</Button>
+ <Button size="small" type="primary" @click="emit('publish')">鍙戝竷浠诲姟</Button>
+ </div>
+ </div>
+</template>
diff --git a/src/views/mes/pd/project/modules/task/data.ts b/src/views/mes/pd/project/modules/task/data.ts
new file mode 100644
index 0000000..f7d551a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task/data.ts
@@ -0,0 +1,147 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { DICT_TYPE } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+export type TaskFormType = 'create' | 'detail' | 'dispatch' | 'update';
+
+export function useTaskGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '浠诲姟缂栫爜',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+ },
+ {
+ fieldName: 'taskName',
+ label: '浠诲姟鍚嶇О',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ヤ换鍔″悕绉�' },
+ },
+ {
+ fieldName: 'status',
+ label: '浠诲姟鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_TASK_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨浠诲姟鐘舵��',
+ },
+ },
+ ];
+}
+
+export function useTaskGridColumns(): VxeTableGridOptions['columns'] {
+ return [
+ { field: 'code', title: '浠诲姟缂栫爜', minWidth: 130 },
+ { field: 'taskName', title: '浠诲姟鍚嶇О', minWidth: 140 },
+ { field: 'assigneeName', title: '鎵ц浜�', minWidth: 100 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_TASK_STATUS },
+ },
+ },
+ {
+ field: 'planFinishDate',
+ title: '璁″垝瀹屾垚',
+ minWidth: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ {
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+export function useTaskFormSchema(formType: TaskFormType): VbenFormSchema[] {
+ const readonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'projectId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'code',
+ label: '浠诲姟缂栫爜',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ换鍔$紪鐮�' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'taskName',
+ label: '浠诲姟鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ヤ换鍔″悕绉�' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'assigneeName',
+ label: '鎵ц浜�',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ユ墽琛屼汉' },
+ },
+ {
+ fieldName: 'planFinishDate',
+ label: '璁″垝瀹屾垚',
+ component: 'DatePicker',
+ componentProps: { disabled: readonly, class: 'w-full', valueFormat: 'x' },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+ },
+ ];
+}
+
+export function useTaskDispatchSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'assigneeName',
+ label: '鎵ц浜�',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ユ墽琛屼汉濮撳悕' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'planFinishDate',
+ label: '璁″垝瀹屾垚',
+ component: 'DatePicker',
+ componentProps: { class: 'w-full', valueFormat: 'x' },
+ },
+ {
+ fieldName: 'remark',
+ label: '鍒嗗彂璇存槑',
+ component: 'Textarea',
+ componentProps: { placeholder: '璇疯緭鍏ュ垎鍙戣鏄�', rows: 3 },
+ },
+ ];
+}
diff --git a/src/views/mes/pd/project/modules/task/task-form.vue b/src/views/mes/pd/project/modules/task/task-form.vue
new file mode 100644
index 0000000..ea95b92
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task/task-form.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { TaskFormType } from './data';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createPdTask,
+ dispatchPdTask,
+ getPdTask,
+ updatePdTask,
+} from '#/api/mes/pd/task';
+import { $t } from '#/locales';
+
+import { useTaskDispatchSchema, useTaskFormSchema } from './data';
+
+const emit = defineEmits(['success']);
+
+const mode = ref<TaskFormType>('create');
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ if (mode.value === 'dispatch') {
+ const data = await formApi.getValues();
+ await dispatchPdTask({
+ id: data.id,
+ assigneeId: 0,
+ planFinishDate: data.planFinishDate,
+ remark: data.remark,
+ });
+ } else if (mode.value === 'create') {
+ await createPdTask(await formApi.getValues());
+ } else if (mode.value === 'update') {
+ await updatePdTask(await formApi.getValues());
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{
+ formType: TaskFormType;
+ id?: number;
+ projectId?: number;
+ }>();
+ mode.value = data.formType;
+ const readonly = data.formType === 'detail';
+ formApi.setState({
+ schema:
+ data.formType === 'dispatch'
+ ? useTaskDispatchSchema()
+ : useTaskFormSchema(data.formType),
+ });
+ formApi.setDisabled(readonly);
+ modalApi.setState({
+ showConfirmButton: data.formType !== 'detail',
+ title:
+ data.formType === 'dispatch'
+ ? '浠诲姟鍒嗗彂'
+ : data.formType === 'detail'
+ ? '鏌ョ湅浠诲姟'
+ : data.formType === 'update'
+ ? '缂栬緫浠诲姟'
+ : '鏂板浠诲姟',
+ });
+ if (data.formType === 'create' && data.projectId) {
+ await formApi.setValues({ projectId: data.projectId });
+ return;
+ }
+ if (!data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const detail = await getPdTask(data.id);
+ await formApi.setValues(detail);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[560px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/task/task-list.vue b/src/views/mes/pd/project/modules/task/task-list.vue
new file mode 100644
index 0000000..8ab4700
--- /dev/null
+++ b/src/views/mes/pd/project/modules/task/task-list.vue
@@ -0,0 +1,149 @@
+<script lang="ts" setup>
+import type { FormType } from '../../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdTaskApi } from '#/api/mes/pd/task';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ completePdTask,
+ deletePdTask,
+ getPdTaskPage,
+} from '#/api/mes/pd/task';
+import { $t } from '#/locales';
+
+import { useTaskGridColumns, useTaskGridFormSchema } from './data';
+import TaskForm from './task-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ projectId: number;
+}>();
+
+const isEditable = computed(() =>
+ ['create', 'update'].includes(props.formType),
+);
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: TaskForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi
+ .setData({ formType: 'create', projectId: props.projectId })
+ .open();
+}
+
+function handleEdit(row: MesPdTaskApi.Task) {
+ formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+function handleDispatch(row: MesPdTaskApi.Task) {
+ formModalApi.setData({ formType: 'dispatch', id: row.id }).open();
+}
+
+async function handleComplete(row: MesPdTaskApi.Task) {
+ await completePdTask(row.id!);
+ message.success('浠诲姟宸插畬鎴�');
+ handleRefresh();
+}
+
+async function handleDelete(row: MesPdTaskApi.Task) {
+ await deletePdTask(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.taskName]));
+ handleRefresh();
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useTaskGridFormSchema() },
+ gridOptions: {
+ columns: useTaskGridColumns(),
+ height: 360,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPdTaskPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ projectId: props.projectId,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: { keyField: 'id', isHover: true },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesPdTaskApi.Task>,
+});
+</script>
+
+<template>
+ <FormModal @success="handleRefresh" />
+ <Grid>
+ <template v-if="isEditable" #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['璁捐浠诲姟']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pd-task:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鍒嗗彂',
+ type: 'link',
+ auth: ['mes:pd-task:dispatch'],
+ ifShow: isEditable,
+ onClick: handleDispatch.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-task:update'],
+ ifShow: isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '瀹屾垚',
+ type: 'link',
+ auth: ['mes:pd-task:complete'],
+ ifShow: isEditable,
+ onClick: handleComplete.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pd-task:delete'],
+ ifShow: isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.taskName]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/version-change/data.ts b/src/views/mes/pd/project/modules/version-change/data.ts
new file mode 100644
index 0000000..e4756cc
--- /dev/null
+++ b/src/views/mes/pd/project/modules/version-change/data.ts
@@ -0,0 +1,167 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+import { h } from 'vue';
+
+import { DICT_TYPE, MesAutoCodeRuleCode } from '#/packages/constants/src';
+import { getDictOptions } from '#/packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import { isPdMockEnabled } from '#/api/mes/pd/mock/enabled';
+import { mockNextCode } from '#/api/mes/pd/mock/utils';
+
+export type VersionChangeFormType = 'create' | 'detail' | 'update';
+
+export function useVersionChangeGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鍙樻洿鍗曞彿',
+ component: 'Input',
+ componentProps: { placeholder: '璇疯緭鍏ュ彉鏇村崟鍙�' },
+ },
+ {
+ fieldName: 'changeType',
+ label: '鍙樻洿绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_VERSION_CHANGE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍙樻洿绫诲瀷',
+ },
+ },
+ ];
+}
+
+export function useVersionChangeGridColumns(
+ showAction = true,
+): VxeTableGridOptions['columns'] {
+ const columns: VxeTableGridOptions['columns'] = [
+ { field: 'code', title: '鍙樻洿鍗曞彿', minWidth: 130 },
+ {
+ field: 'changeType',
+ title: '鍙樻洿绫诲瀷',
+ minWidth: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_VERSION_CHANGE_TYPE },
+ },
+ },
+ { field: 'fromVersion', title: '鍘熺増鏈�', minWidth: 90 },
+ { field: 'toVersion', title: '鏂扮増鏈�', minWidth: 90 },
+ { field: 'applicantName', title: '鐢宠浜�', minWidth: 100 },
+ {
+ field: 'createTime',
+ title: '鐢宠鏃堕棿',
+ minWidth: 160,
+ formatter: 'formatDateTime',
+ },
+ ];
+
+ if (showAction) {
+ columns.push({
+ field: 'action',
+ title: '鎿嶄綔',
+ width: 180,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ });
+ }
+
+ return columns;
+}
+
+export function useVersionChangeFormSchema(
+ formType: VersionChangeFormType,
+ formApi?: VbenFormApi,
+): VbenFormSchema[] {
+ const readonly = formType === 'detail';
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'projectId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'code',
+ label: '鍙樻洿鍗曞彿',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ彉鏇村崟鍙�' },
+ rules: 'required',
+ suffix:
+ formType === 'detail'
+ ? undefined
+ : () =>
+ h(
+ Button,
+ {
+ type: 'default',
+ onClick: async () => {
+ const code = isPdMockEnabled()
+ ? mockNextCode(
+ 'VC-2026',
+ Math.floor(Date.now() % 10_000),
+ )
+ : await generateAutoCode(
+ MesAutoCodeRuleCode.PD_VERSION_CODE,
+ );
+ await formApi?.setFieldValue('code', code);
+ },
+ },
+ { default: () => '鐢熸垚' },
+ ),
+ },
+ {
+ fieldName: 'changeType',
+ label: '鍙樻洿绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ disabled: readonly,
+ options: getDictOptions(DICT_TYPE.MES_PD_VERSION_CHANGE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍙樻洿绫诲瀷',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'fromVersion',
+ label: '鍘熺増鏈�',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '濡� V1.0' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'toVersion',
+ label: '鏂扮増鏈�',
+ component: 'Input',
+ componentProps: { disabled: readonly, placeholder: '濡� V1.1' },
+ rules: 'required',
+ },
+ {
+ fieldName: 'changeReason',
+ label: '鍙樻洿鍘熷洜',
+ component: 'Textarea',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ彉鏇村師鍥�', rows: 2 },
+ rules: 'required',
+ },
+ {
+ fieldName: 'changeContent',
+ label: '鍙樻洿鍐呭',
+ component: 'Textarea',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ彉鏇村唴瀹�', rows: 4 },
+ rules: 'required',
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: { disabled: readonly, placeholder: '璇疯緭鍏ュ娉�', rows: 2 },
+ },
+ ];
+}
diff --git a/src/views/mes/pd/project/modules/version-change/version-change-form.vue b/src/views/mes/pd/project/modules/version-change/version-change-form.vue
new file mode 100644
index 0000000..a969658
--- /dev/null
+++ b/src/views/mes/pd/project/modules/version-change/version-change-form.vue
@@ -0,0 +1,108 @@
+<script lang="ts" setup>
+import type { VersionChangeFormType } from './data';
+
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createPdVersionChange,
+ getPdVersionChange,
+ updatePdVersionChange,
+} from '#/api/mes/pd/version-change';
+import { $t } from '#/locales';
+
+import { useVersionChangeFormSchema } from './data';
+
+const emit = defineEmits(['success']);
+
+const formType = ref<VersionChangeFormType>('create');
+const isDetail = computed(() => formType.value === 'detail');
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesPdVersionChangeApi.VersionChange;
+ try {
+ if (formType.value === 'create') {
+ await createPdVersionChange(data);
+ } else {
+ await updatePdVersionChange(data);
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ return;
+ }
+ const data = modalApi.getData<{
+ formType: VersionChangeFormType;
+ id?: number;
+ projectId?: number;
+ fromVersion?: string;
+ }>();
+ formType.value = data.formType;
+ formApi.setState({ schema: useVersionChangeFormSchema(data.formType, formApi) });
+ formApi.setDisabled(isDetail.value);
+ modalApi.setState({
+ showConfirmButton: !isDetail.value,
+ title:
+ data.formType === 'detail'
+ ? '鏌ョ湅鐗堟湰鍙樻洿'
+ : data.formType === 'update'
+ ? '缂栬緫鐗堟湰鍙樻洿'
+ : '鏂板鐗堟湰鍙樻洿',
+ });
+ if (data.formType === 'create' && data.projectId) {
+ await formApi.setValues({
+ projectId: data.projectId,
+ fromVersion: data.fromVersion,
+ });
+ return;
+ }
+ if (!data.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ await formApi.setValues(await getPdVersionChange(data.id));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[640px]">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/version-change/version-change-list.vue b/src/views/mes/pd/project/modules/version-change/version-change-list.vue
new file mode 100644
index 0000000..4cb2a67
--- /dev/null
+++ b/src/views/mes/pd/project/modules/version-change/version-change-list.vue
@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import type { FormType } from '../../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdVersionChangeApi } from '#/api/mes/pd/version-change';
+
+import { computed } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ approvePdVersionChange,
+ deletePdVersionChange,
+ getPdVersionChangePage,
+} from '#/api/mes/pd/version-change';
+import { $t } from '#/locales';
+
+import {
+ useVersionChangeGridColumns,
+ useVersionChangeGridFormSchema,
+} from './data';
+import VersionChangeForm from './version-change-form.vue';
+
+const props = withDefaults(
+ defineProps<{
+ formType: FormType;
+ gridHeight?: number | string;
+ projectId: number;
+ projectVersion?: string;
+ selectable?: boolean;
+ }>(),
+ {
+ gridHeight: 360,
+ selectable: false,
+ },
+);
+
+const emit = defineEmits<{
+ viewMaterials: [row: MesPdVersionChangeApi.VersionChange];
+}>();
+
+const isEditable = computed(() =>
+ ['create', 'update'].includes(props.formType),
+);
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: VersionChangeForm,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+}
+
+function handleCreate() {
+ formModalApi
+ .setData({
+ formType: 'create',
+ projectId: props.projectId,
+ fromVersion: props.projectVersion,
+ })
+ .open();
+}
+
+function handleDetail(row: MesPdVersionChangeApi.VersionChange) {
+ if (!isEditable.value) {
+ emit('viewMaterials', row);
+ return;
+ }
+ formModalApi.setData({ formType: 'detail', id: row.id }).open();
+}
+
+function handleEdit(row: MesPdVersionChangeApi.VersionChange) {
+ formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+
+async function handleApprove(row: MesPdVersionChangeApi.VersionChange) {
+ await approvePdVersionChange(row.id!);
+ message.success('鐗堟湰鍙樻洿宸叉壒鍑�');
+ handleRefresh();
+}
+
+async function handleDelete(row: MesPdVersionChangeApi.VersionChange) {
+ await deletePdVersionChange(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.code]));
+ handleRefresh();
+}
+
+function handleSelectRow(row: MesPdVersionChangeApi.VersionChange) {
+ emit('viewMaterials', row);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: { schema: useVersionChangeGridFormSchema() },
+ gridOptions: {
+ columns: useVersionChangeGridColumns(!props.selectable),
+ height: props.gridHeight,
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getPdVersionChangePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ projectId: props.projectId,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isCurrent: props.selectable,
+ isHover: true,
+ },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesPdVersionChangeApi.VersionChange>,
+ gridEvents: props.selectable
+ ? {
+ cellClick: ({ row }: { row: MesPdVersionChangeApi.VersionChange }) => {
+ handleSelectRow(row);
+ },
+ }
+ : undefined,
+});
+</script>
+
+<template>
+ <FormModal @success="handleRefresh" />
+ <Grid :table-title="selectable ? '鐗堟湰鍙樻洿璁板綍' : undefined">
+ <template v-if="isEditable" #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鐗堟湰鍙樻洿']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pd-version-change:create'],
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: isEditable ? '鏌ョ湅' : '鏌ョ湅璧勬枡',
+ type: 'link',
+ onClick: handleDetail.bind(null, row),
+ },
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pd-version-change:update'],
+ ifShow: isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鎵瑰噯',
+ type: 'link',
+ auth: ['mes:pd-version-change:approve'],
+ ifShow: isEditable,
+ onClick: handleApprove.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pd-version-change:delete'],
+ ifShow: isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.code]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/pd/project/modules/workflow-action-form.vue b/src/views/mes/pd/project/modules/workflow-action-form.vue
new file mode 100644
index 0000000..1a62efa
--- /dev/null
+++ b/src/views/mes/pd/project/modules/workflow-action-form.vue
@@ -0,0 +1,126 @@
+<script lang="ts" setup>
+import type { WorkflowActionType } from '../data';
+import type { MesPdProjectApi } from '#/api/mes/pd/project';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '#/packages/effects/common-ui/src';
+import { formatDate } from '#/packages/utils/src';
+
+import { Alert, Descriptions, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ approvePdProject,
+ archivePdProject,
+ getPdProject,
+ submitPdProjectReview,
+ updatePdProject,
+} from '#/api/mes/pd/project';
+
+import { useWorkflowActionFormSchema, WORKFLOW_ACTION_CONFIG } from '../data';
+
+const emit = defineEmits(['success']);
+
+const project = ref<MesPdProjectApi.Project>();
+const actionType = ref<WorkflowActionType>('submit-review');
+
+const actionConfig = computed(() => WORKFLOW_ACTION_CONFIG[actionType.value]);
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (!project.value?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ const { actionRemark } = await formApi.getValues();
+ const config = actionConfig.value;
+ if (actionRemark) {
+ const mergedRemark = [project.value.remark, `${config.remarkTag}${actionRemark}`]
+ .filter(Boolean)
+ .join('\n');
+ await updatePdProject({ ...project.value, remark: mergedRemark });
+ }
+ const projectId = project.value.id;
+ switch (actionType.value) {
+ case 'approve': {
+ await approvePdProject(projectId);
+ break;
+ }
+ case 'archive': {
+ await archivePdProject(projectId);
+ break;
+ }
+ case 'submit-review': {
+ await submitPdProjectReview(projectId);
+ break;
+ }
+ }
+ emit('success');
+ message.success(config.successMessage);
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ project.value = undefined;
+ await formApi.resetForm();
+ return;
+ }
+ const data = modalApi.getData<{ action: WorkflowActionType; id: number }>();
+ actionType.value = data.action;
+ formApi.setState({ schema: useWorkflowActionFormSchema(data.action) });
+ modalApi.setState({ title: WORKFLOW_ACTION_CONFIG[data.action].title });
+ modalApi.lock();
+ try {
+ project.value = await getPdProject(data.id);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal class="w-[640px]">
+ <Alert
+ class="mx-4 mb-4"
+ :message="actionConfig.alertMessage"
+ :description="actionConfig.alertDescription"
+ show-icon
+ :type="actionConfig.alertType"
+ />
+ <Descriptions
+ v-if="project"
+ bordered
+ class="mx-4 mb-4"
+ :column="2"
+ size="small"
+ >
+ <Descriptions.Item label="浠诲姟缂栫爜">{{ project.code }}</Descriptions.Item>
+ <Descriptions.Item label="浠诲姟鍚嶇О">{{ project.name }}</Descriptions.Item>
+ <Descriptions.Item label="浜у搧缂栫爜">{{ project.productCode }}</Descriptions.Item>
+ <Descriptions.Item label="浜у搧鍚嶇О">{{ project.productName }}</Descriptions.Item>
+ <Descriptions.Item label="璁捐鐗堟湰">{{ project.version }}</Descriptions.Item>
+ <Descriptions.Item label="涓昏璁″笀">{{ project.designerName || '-' }}</Descriptions.Item>
+ <Descriptions.Item label="瀹℃牳浜�">{{ project.reviewerName || '-' }}</Descriptions.Item>
+ <Descriptions.Item label="璁″垝瀹屾垚">
+ {{ project.planEndDate ? formatDate(project.planEndDate) : '-' }}
+ </Descriptions.Item>
+ </Descriptions>
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pd/project/modules/workflow-timeline.vue b/src/views/mes/pd/project/modules/workflow-timeline.vue
new file mode 100644
index 0000000..58a6d8a
--- /dev/null
+++ b/src/views/mes/pd/project/modules/workflow-timeline.vue
@@ -0,0 +1,53 @@
+<script lang="ts" setup>
+import type { MesPdWorkflowApi } from '#/api/mes/pd/workflow';
+
+import { onMounted, ref, watch } from 'vue';
+
+import { Steps } from 'ant-design-vue';
+
+import { getPdWorkflowList } from '#/api/mes/pd/workflow';
+
+const props = defineProps<{
+ projectId: number;
+}>();
+
+const loading = ref(false);
+const nodes = ref<MesPdWorkflowApi.WorkflowNode[]>([]);
+const current = ref(0);
+
+async function loadData() {
+ loading.value = true;
+ try {
+ nodes.value = await getPdWorkflowList(props.projectId);
+ const activeIndex = nodes.value.findIndex((item) => item.status !== 1);
+ current.value = activeIndex === -1 ? nodes.value.length : activeIndex;
+ } finally {
+ loading.value = false;
+ }
+}
+
+watch(
+ () => props.projectId,
+ () => loadData(),
+ { immediate: true },
+);
+
+onMounted(loadData);
+</script>
+
+<template>
+ <Steps :current="current" direction="vertical" size="small">
+ <Steps.Step
+ v-for="node in nodes"
+ :key="node.id"
+ :description="node.remark"
+ :sub-title="node.operatorName"
+ :title="node.nodeName"
+ />
+ <Steps.Step
+ v-if="!loading && nodes.length === 0"
+ description="鏆傛棤娴佺▼璁板綍锛屼繚瀛橀」鐩悗灏嗚嚜鍔ㄧ敓鎴�"
+ title="寰呭惎鍔�"
+ />
+ </Steps>
+</template>
diff --git a/src/views/mes/pro/andon/record/data.ts b/src/views/mes/pro/andon/record/data.ts
index a9f1f19..b390630 100644
--- a/src/views/mes/pro/andon/record/data.ts
+++ b/src/views/mes/pro/andon/record/data.ts
@@ -11,7 +11,7 @@
import { getSimpleUserList } from '#/api/system/user';
import { getRangePickerDefaultProps } from '#/utils';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { AndonConfigSelect } from '../config/components';
diff --git a/src/views/mes/pro/card/data.ts b/src/views/mes/pro/card/data.ts
index 011a355..17c3172 100644
--- a/src/views/mes/pro/card/data.ts
+++ b/src/views/mes/pro/card/data.ts
@@ -16,7 +16,7 @@
import { generateAutoCode } from '#/api/mes/md/autocode/record';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
import { UserSelect } from '#/views/system/user/components';
diff --git a/src/views/mes/pro/task/data.ts b/src/views/mes/pro/task/data.ts
index ab0c5de..aeef143 100644
--- a/src/views/mes/pro/task/data.ts
+++ b/src/views/mes/pro/task/data.ts
@@ -12,8 +12,8 @@
import { MdClientSelect } from '#/views/mes/md/client/components';
import { MdItemSelect } from '#/views/mes/md/item/components';
import { MdWorkstationSelect } from '#/views/mes/md/workstation/components';
-import { ProProcessSelect } from '#/views/mes/pro/process/components';
-import { RouteColorPicker } from '#/views/mes/pro/route/components';
+import { ProProcessSelect } from '#/views/mes/process-design/process/components';
+import { RouteColorPicker } from '#/views/mes/process-design/route/components';
import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components';
/** 寰呮帓浜у伐鍗曞垪琛ㄧ殑鎼滅储琛ㄥ崟 */
diff --git a/src/views/mes/process-design/parameter/data.ts b/src/views/mes/process-design/parameter/data.ts
new file mode 100644
index 0000000..e90cfed
--- /dev/null
+++ b/src/views/mes/process-design/parameter/data.ts
@@ -0,0 +1,197 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+
+import { DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { z } from '#/adapter/form';
+
+/** 鍙傛暟绫诲瀷 - 涓嬫媺閫夐」鐨勫�� */
+const PARAM_TYPE_SELECT = 3;
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼宸ヨ壓鍙傛暟鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '鍙傛暟缂栫爜',
+ component: 'Input',
+ componentProps: {
+ maxLength: 64,
+ placeholder: '璇疯緭鍏ュ弬鏁扮紪鐮�',
+ },
+ rules: z.string().min(1, '鍙傛暟缂栫爜涓嶈兘涓虹┖').max(64),
+ },
+ {
+ fieldName: 'name',
+ label: '鍙傛暟鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+ },
+ rules: z.string().min(1, '鍙傛暟鍚嶇О涓嶈兘涓虹┖').max(100),
+ },
+ {
+ fieldName: 'type',
+ label: '鍙傛暟绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_PARAM_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍙傛暟绫诲瀷',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'unit',
+ label: '鍗曚綅',
+ component: 'Input',
+ componentProps: {
+ maxLength: 20,
+ placeholder: '璇疯緭鍏ュ崟浣�',
+ },
+ },
+ {
+ fieldName: 'requiredFlag',
+ label: '鏄惁蹇呭~',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ },
+ {
+ fieldName: 'dictType',
+ label: '瀛楀吀绫诲瀷',
+ component: 'Input',
+ componentProps: {
+ maxLength: 64,
+ placeholder: '璇疯緭鍏ュ瓧鍏哥被鍨嬶紙涓嬫媺閫夐」鏃堕厤缃級',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values?.type === PARAM_TYPE_SELECT,
+ },
+ },
+ {
+ fieldName: 'apiUrl',
+ label: '鎺ュ彛鍦板潃',
+ component: 'Input',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ユ帴鍙e湴鍧�锛堜笅鎷夐�夐」鏃堕厤缃級',
+ },
+ dependencies: {
+ triggerFields: ['type'],
+ show: (values) => values?.type === PARAM_TYPE_SELECT,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '鍙傛暟缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ弬鏁扮紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鍙傛暟鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '鍙傛暟绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_PARAM_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨鍙傛暟绫诲瀷',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdParameterApi.Parameter>['columns'] {
+ return [
+ {
+ type: 'seq',
+ title: '搴忓彿',
+ width: 60,
+ align: 'center',
+ },
+ { field: 'code', title: '鍙傛暟缂栫爜', minWidth: 150 },
+ { field: 'name', title: '鍙傛暟鍚嶇О', minWidth: 180 },
+ {
+ field: 'type',
+ title: '鍙傛暟绫诲瀷',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_PARAM_TYPE },
+ },
+ },
+ { field: 'unit', title: '鍗曚綅', width: 80 },
+ {
+ field: 'requiredFlag',
+ title: '鏄惁蹇呭~',
+ width: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 180 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/md/unitmeasure/index.vue b/src/views/mes/process-design/parameter/index.vue
similarity index 63%
copy from src/views/mes/md/unitmeasure/index.vue
copy to src/views/mes/process-design/parameter/index.vue
index f9f49d4..dfd7e51 100644
--- a/src/views/mes/md/unitmeasure/index.vue
+++ b/src/views/mes/process-design/parameter/index.vue
@@ -1,18 +1,16 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-import { message } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
- deleteUnitMeasure,
- exportUnitMeasure,
- getUnitMeasurePage,
-} from '#/api/mes/md/unitmeasure';
+ deleteParameter,
+ getParameterPage,
+} from '#/api/mes/pro/processdesign/parameter';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -28,30 +26,29 @@
gridApi.query();
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportUnitMeasure(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅.xls', source: data });
-}
-
-/** 鍒涘缓璁¢噺鍗曚綅 */
+/** 鍒涘缓宸ヨ壓鍙傛暟 */
function handleCreate() {
- formModalApi.setData(null).open();
+ formModalApi.setData({ formType: 'create' }).open();
}
-/** 缂栬緫璁¢噺鍗曚綅 */
-function handleEdit(row: MesMdUnitMeasureApi.UnitMeasure) {
- formModalApi.setData(row).open();
+/** 缂栬緫宸ヨ壓鍙傛暟 */
+function handleEdit(row: MesPdParameterApi.Parameter) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
-/** 鍒犻櫎璁¢噺鍗曚綅 */
-async function handleDelete(row: MesMdUnitMeasureApi.UnitMeasure) {
+/** 鏌ョ湅宸ヨ壓鍙傛暟璇︽儏 */
+function handleDetail(row: MesPdParameterApi.Parameter) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟 */
+async function handleDelete(row: MesPdParameterApi.Parameter) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
- await deleteUnitMeasure(row.id!);
+ await deleteParameter(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
@@ -70,7 +67,7 @@
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
- return await getUnitMeasurePage({
+ return await getParameterPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
@@ -79,39 +76,35 @@
},
},
rowConfig: {
- keyField: 'id',
isHover: true,
+ keyField: 'id',
},
toolbarConfig: {
refresh: true,
search: true,
},
- } as VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>,
+ } as VxeTableGridOptions<MesPdParameterApi.Parameter>,
});
</script>
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
- <Grid table-title="璁¢噺鍗曚綅鍒楄〃">
+ <Grid table-title="宸ヨ壓鍙傛暟鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
{
- label: $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']),
+ label: $t('ui.actionTitle.create', ['鍙傛暟']),
type: 'primary',
icon: ACTION_ICON.ADD,
- auth: ['mes:md-unit-measure:create'],
+ auth: ['mes:pd:parameter:create'],
onClick: handleCreate,
- },
- {
- label: $t('ui.actionTitle.export'),
- type: 'primary',
- icon: ACTION_ICON.DOWNLOAD,
- auth: ['mes:md-unit-measure:export'],
- onClick: handleExport,
},
]"
/>
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
</template>
<template #actions="{ row }">
<TableAction
@@ -120,7 +113,7 @@
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
- auth: ['mes:md-unit-measure:update'],
+ auth: ['mes:pd:parameter:update'],
onClick: handleEdit.bind(null, row),
},
{
@@ -128,7 +121,7 @@
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
- auth: ['mes:md-unit-measure:delete'],
+ auth: ['mes:pd:parameter:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
diff --git a/src/views/mes/process-design/parameter/modules/form.vue b/src/views/mes/process-design/parameter/modules/form.vue
new file mode 100644
index 0000000..122a5a8
--- /dev/null
+++ b/src/views/mes/process-design/parameter/modules/form.vue
@@ -0,0 +1,99 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createParameter,
+ getParameter,
+ updateParameter,
+} from '#/api/mes/pro/processdesign/parameter';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesPdParameterApi.Parameter>();
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return $t('ui.actionTitle.detail', ['宸ヨ壓鍙傛暟']);
+ }
+ return formType.value === 'update'
+ ? $t('ui.actionTitle.edit', ['宸ヨ壓鍙傛暟'])
+ : $t('ui.actionTitle.create', ['宸ヨ壓鍙傛暟']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesPdParameterApi.Parameter;
+ try {
+ await (formData.value?.id ? updateParameter(data) : createParameter(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;
+ }
+ formApi.setState({ schema: useFormSchema() });
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getParameter(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/process/components/index.ts b/src/views/mes/process-design/process/components/index.ts
new file mode 100644
index 0000000..4204017
--- /dev/null
+++ b/src/views/mes/process-design/process/components/index.ts
@@ -0,0 +1 @@
+export { default as ProProcessSelect } from './select.vue';
diff --git a/src/views/mes/process-design/process/components/select.vue b/src/views/mes/process-design/process/components/select.vue
new file mode 100644
index 0000000..ba87738
--- /dev/null
+++ b/src/views/mes/process-design/process/components/select.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { MesProProcessApi } from '#/api/mes/pro/process';
+
+import { computed, onMounted, ref, watch } from 'vue';
+
+import { Select, Tag, Tooltip } from 'ant-design-vue';
+
+import { getProcessSimpleList } from '#/api/mes/pro/process';
+
+defineOptions({ name: 'ProProcessSelect', inheritAttrs: false });
+
+const props = withDefaults(
+ defineProps<{
+ allowClear?: boolean;
+ disabled?: boolean;
+ modelValue?: number;
+ placeholder?: string;
+ }>(),
+ {
+ allowClear: true,
+ disabled: false,
+ modelValue: undefined,
+ placeholder: '璇烽�夋嫨宸ュ簭',
+ },
+);
+
+const emit = defineEmits<{
+ change: [item: MesProProcessApi.Process | undefined];
+ 'update:modelValue': [value: number | undefined];
+}>();
+
+const allList = ref<MesProProcessApi.Process[]>([]);
+const selectedItem = ref<MesProProcessApi.Process>();
+
+const selectValue = computed({
+ get: () => props.modelValue,
+ set: (value: number | undefined) => {
+ emit('update:modelValue', value);
+ },
+});
+
+/** 鍓嶇杩囨护锛氭寜宸ュ簭鍚嶇О鎴栫紪鐮佹ā绯婂尮閰� */
+function handleFilter(input: string, option: any) {
+ const keyword = input.toLowerCase();
+ const item = option?.item as MesProProcessApi.Process | undefined;
+ return Boolean(
+ item?.name?.toLowerCase().includes(keyword) ||
+ item?.code?.toLowerCase().includes(keyword),
+ );
+}
+
+/** 鏍规嵁褰撳墠鍊煎悓姝� tooltip 灞曠ず鐨勫伐搴忚鎯� */
+function syncSelectedItem(value: number | undefined) {
+ selectedItem.value =
+ value === undefined
+ ? undefined
+ : allList.value.find((item) => item.id === value);
+}
+
+/** 闄� v-model 澶栵紝棰濆鎶涘嚭瀹屾暣宸ュ簭瀵硅薄缁欎笟鍔¤〃鍗曚娇鐢� */
+function handleChange(value: any) {
+ const nextValue = value === undefined ? undefined : Number(value);
+ syncSelectedItem(nextValue);
+ emit('change', selectedItem.value);
+}
+
+watch(
+ () => props.modelValue,
+ (value) => {
+ syncSelectedItem(value);
+ },
+);
+
+onMounted(async () => {
+ allList.value = await getProcessSimpleList();
+ syncSelectedItem(props.modelValue);
+});
+</script>
+
+<template>
+ <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>宸ヨ壓瑕佹眰锛歿{ selectedItem.attention || '-' }}</div>
+ <div>澶囨敞锛歿{ selectedItem.remark || '-' }}</div>
+ </div>
+ </template>
+ <Select
+ v-bind="$attrs"
+ v-model:value="selectValue"
+ :allow-clear="allowClear"
+ :disabled="disabled"
+ :filter-option="handleFilter"
+ :placeholder="placeholder"
+ class="w-full"
+ show-search
+ @change="handleChange"
+ >
+ <Select.Option
+ v-for="item in allList"
+ :key="item.id"
+ :item="item"
+ :value="item.id"
+ >
+ <div class="flex items-center gap-2">
+ <span>{{ item.name }}</span>
+ <Tag v-if="item.code" color="default">{{ item.code }}</Tag>
+ </div>
+ </Select.Option>
+ </Select>
+ </Tooltip>
+</template>
diff --git a/src/views/mes/process-design/process/data.ts b/src/views/mes/process-design/process/data.ts
new file mode 100644
index 0000000..d6ce88a
--- /dev/null
+++ b/src/views/mes/process-design/process/data.ts
@@ -0,0 +1,274 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+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 { h } from 'vue';
+
+import {
+ CommonStatusEnum,
+ DICT_TYPE,
+ MesAutoCodeRuleCode,
+} from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼鐢熶骇宸ュ簭鐨勮〃鍗� */
+export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'code',
+ label: '宸ュ簭缂栫爜',
+ component: 'Input',
+ componentProps: {
+ maxLength: 64,
+ placeholder: '璇疯緭鍏ュ伐搴忕紪鐮�',
+ },
+ rules: z.string().min(1, '宸ュ簭缂栫爜涓嶈兘涓虹┖').max(64),
+ suffix: () =>
+ h(
+ Button,
+ {
+ type: 'default',
+ onClick: async () => {
+ const code = await generateAutoCode(
+ MesAutoCodeRuleCode.PRO_PROCESS_CODE,
+ );
+ await formApi?.setFieldValue('code', code);
+ },
+ },
+ { default: () => '鐢熸垚' },
+ ),
+ },
+ {
+ fieldName: 'name',
+ label: '宸ュ簭鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ rules: z.string().min(1, '宸ュ簭鍚嶇О涓嶈兘涓虹┖').max(100),
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'RadioGroup',
+ componentProps: {
+ buttonStyle: 'solid',
+ optionType: 'button',
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ },
+ rules: z.number().default(CommonStatusEnum.ENABLE),
+ },
+ {
+ fieldName: 'attention',
+ label: '宸ュ簭璇存槑',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ maxLength: 500,
+ placeholder: '璇疯緭鍏ュ伐搴忚鏄�',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-3',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '宸ュ簭缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ伐搴忕紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '宸ュ簭鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ伐搴忓悕绉�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesProProcessApi.Process>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '宸ュ簭缂栫爜',
+ minWidth: 150,
+ slots: {
+ default: 'code',
+ },
+ },
+ { field: 'name', title: '宸ュ簭鍚嶇О', minWidth: 180 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.COMMON_STATUS },
+ },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 180 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
+
+/** 宸ュ簭鍐呭锛堟搷浣滄楠わ級琛ㄥ崟 */
+export function useContentFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'processId',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'sort',
+ label: '搴忓彿',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ max: 999,
+ min: 1,
+ precision: 0,
+ },
+ rules: z.number().default(1),
+ },
+ {
+ fieldName: 'content',
+ label: '姝ラ璇存槑',
+ component: 'Textarea',
+ componentProps: {
+ maxLength: 500,
+ placeholder: '璇疯緭鍏ユ楠よ鏄�',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'device',
+ label: '杈呭姪璁惧',
+ component: 'Input',
+ componentProps: {
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ヨ緟鍔╄澶�',
+ },
+ },
+ {
+ fieldName: 'material',
+ label: '杈呭姪鏉愭枡',
+ component: 'Input',
+ componentProps: {
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ヨ緟鍔╂潗鏂�',
+ },
+ },
+ {
+ fieldName: 'docUrl',
+ label: '鏉愭枡鏂囨。 URL',
+ component: 'Input',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ユ潗鏂欐枃妗� URL',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 宸ュ簭鍐呭鍒楄〃鐨勫瓧娈� */
+export function useContentGridColumns(): VxeTableGridOptions<MesProProcessContentApi.ProcessContent>['columns'] {
+ return [
+ { field: 'sort', title: '搴忓彿', width: 80, align: 'center' },
+ { field: 'content', title: '姝ラ璇存槑', minWidth: 220 },
+ { field: 'device', title: '杈呭姪璁惧', width: 150 },
+ { field: 'material', title: '杈呭姪鏉愭枡', width: 150 },
+ { field: 'docUrl', title: '鏉愭枡鏂囨。', minWidth: 180 },
+ { field: 'remark', title: '澶囨敞', minWidth: 160 },
+ {
+ title: '鎿嶄綔',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/md/unitmeasure/index.vue b/src/views/mes/process-design/process/index.vue
similarity index 66%
rename from src/views/mes/md/unitmeasure/index.vue
rename to src/views/mes/process-design/process/index.vue
index f9f49d4..8662934 100644
--- a/src/views/mes/md/unitmeasure/index.vue
+++ b/src/views/mes/process-design/process/index.vue
@@ -1,18 +1,18 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
+import type { MesProProcessApi } from '#/api/mes/pro/process';
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-import { message } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
- deleteUnitMeasure,
- exportUnitMeasure,
- getUnitMeasurePage,
-} from '#/api/mes/md/unitmeasure';
+ deleteProcess,
+ exportProcess,
+ getProcessPage,
+} from '#/api/mes/pro/process';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -28,35 +28,40 @@
gridApi.query();
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportUnitMeasure(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅.xls', source: data });
-}
-
-/** 鍒涘缓璁¢噺鍗曚綅 */
+/** 鍒涘缓鐢熶骇宸ュ簭 */
function handleCreate() {
- formModalApi.setData(null).open();
+ formModalApi.setData({ formType: 'create' }).open();
}
-/** 缂栬緫璁¢噺鍗曚綅 */
-function handleEdit(row: MesMdUnitMeasureApi.UnitMeasure) {
- formModalApi.setData(row).open();
+/** 缂栬緫鐢熶骇宸ュ簭 */
+function handleEdit(row: MesProProcessApi.Process) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
-/** 鍒犻櫎璁¢噺鍗曚綅 */
-async function handleDelete(row: MesMdUnitMeasureApi.UnitMeasure) {
+/** 鏌ョ湅鐢熶骇宸ュ簭璇︽儏 */
+function handleDetail(row: MesProProcessApi.Process) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎鐢熶骇宸ュ簭 */
+async function handleDelete(row: MesProProcessApi.Process) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
- await deleteUnitMeasure(row.id!);
+ await deleteProcess(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
hideLoading();
}
+}
+
+/** 瀵煎嚭鐢熶骇宸ュ簭 */
+async function handleExport() {
+ const data = await exportProcess(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '鐢熶骇宸ュ簭.xls', source: data });
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -70,7 +75,7 @@
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
- return await getUnitMeasurePage({
+ return await getProcessPage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
@@ -79,39 +84,42 @@
},
},
rowConfig: {
- keyField: 'id',
isHover: true,
+ keyField: 'id',
},
toolbarConfig: {
refresh: true,
search: true,
},
- } as VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>,
+ } as VxeTableGridOptions<MesProProcessApi.Process>,
});
</script>
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
- <Grid table-title="璁¢噺鍗曚綅鍒楄〃">
+ <Grid table-title="鐢熶骇宸ュ簭鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
{
- label: $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']),
+ label: $t('ui.actionTitle.create', ['宸ュ簭']),
type: 'primary',
icon: ACTION_ICON.ADD,
- auth: ['mes:md-unit-measure:create'],
+ auth: ['mes:pro-process:create'],
onClick: handleCreate,
},
{
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
- auth: ['mes:md-unit-measure:export'],
+ auth: ['mes:pro-process:export'],
onClick: handleExport,
},
]"
/>
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
</template>
<template #actions="{ row }">
<TableAction
@@ -120,7 +128,7 @@
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
- auth: ['mes:md-unit-measure:update'],
+ auth: ['mes:pro-process:update'],
onClick: handleEdit.bind(null, row),
},
{
@@ -128,7 +136,7 @@
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
- auth: ['mes:md-unit-measure:delete'],
+ auth: ['mes:pro-process:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
diff --git a/src/views/mes/process-design/process/modules/content-form.vue b/src/views/mes/process-design/process/modules/content-form.vue
new file mode 100644
index 0000000..a3d79dd
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/content-form.vue
@@ -0,0 +1,98 @@
+<script lang="ts" setup>
+import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createProcessContent,
+ getProcessContent,
+ updateProcessContent,
+} from '#/api/mes/pro/process/content';
+import { $t } from '#/locales';
+
+import { useContentFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProProcessContentApi.ProcessContent>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['宸ュ簭姝ラ'])
+ : $t('ui.actionTitle.create', ['宸ュ簭姝ラ']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-2',
+ labelWidth: 120,
+ },
+ layout: 'horizontal',
+ schema: useContentFormSchema(),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data =
+ (await formApi.getValues()) as MesProProcessContentApi.ProcessContent;
+ try {
+ await (formData.value?.id
+ ? updateProcessContent(data)
+ : createProcessContent(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<{
+ id?: number;
+ maxSort?: number;
+ processId: number;
+ }>();
+ if (!data?.id) {
+ // 鏂板鏃讹紝榛樿搴忓彿 = maxSort + 1
+ await formApi.setValues({
+ processId: data?.processId,
+ sort: (data?.maxSort || 0) + 1,
+ });
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getProcessContent(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/process/modules/content-list.vue b/src/views/mes/process-design/process/modules/content-list.vue
new file mode 100644
index 0000000..4ee6c92
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/content-list.vue
@@ -0,0 +1,132 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProProcessContentApi } from '#/api/mes/pro/process/content';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteProcessContent,
+ getProcessContentListByProcessId,
+} from '#/api/mes/pro/process/content';
+import { $t } from '#/locales';
+
+import { useContentGridColumns } from '../data';
+import ContentForm from './content-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ processId: number;
+}>();
+
+const isEditable = ref(props.formType !== 'detail'); // 鏄惁鍙紪杈�
+const list = ref<MesProProcessContentApi.ProcessContent[]>([]);
+
+const [ContentFormModal, contentFormModalApi] = useVbenModal({
+ connectedComponent: ContentForm,
+ destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ autoResize: true,
+ border: true,
+ columns: useContentGridColumns(),
+ data: list.value,
+ minHeight: 240,
+ pagerConfig: { enabled: false },
+ rowConfig: {
+ isHover: true,
+ keyField: 'id',
+ },
+ showOverflow: true,
+ toolbarConfig: { enabled: false },
+ } as VxeTableGridOptions<MesProProcessContentApi.ProcessContent>,
+});
+
+/** 鍔犺浇宸ュ簭鍐呭鍒楄〃 */
+async function getList() {
+ gridApi.setLoading(true);
+ try {
+ list.value = await getProcessContentListByProcessId(props.processId);
+ gridApi.setGridOptions({ data: list.value });
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+/** 鏂板宸ュ簭姝ラ */
+function handleCreate() {
+ const maxSort = Math.max(0, ...list.value.map((item) => item.sort || 0));
+ contentFormModalApi.setData({ maxSort, processId: props.processId }).open();
+}
+
+/** 缂栬緫宸ュ簭姝ラ */
+function handleEdit(row: MesProProcessContentApi.ProcessContent) {
+ contentFormModalApi
+ .setData({ id: row.id, processId: props.processId })
+ .open();
+}
+
+/** 鍒犻櫎宸ュ簭姝ラ */
+async function handleDelete(row: MesProProcessContentApi.ProcessContent) {
+ await deleteProcessContent(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['宸ュ簭姝ラ']));
+ await getList();
+}
+
+watch(
+ () => props.processId,
+ (value) => {
+ if (value) {
+ getList();
+ }
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <ContentFormModal @success="getList" />
+ <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['姝ラ']),
+ type: 'primary',
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </div>
+ <Grid class="w-full" table-title="宸ュ簭鎿嶄綔姝ラ">
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ ifShow: () => isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ ifShow: () => isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', ['宸ュ簭姝ラ']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/process-design/process/modules/form.vue b/src/views/mes/process-design/process/modules/form.vue
new file mode 100644
index 0000000..f88df1e
--- /dev/null
+++ b/src/views/mes/process-design/process/modules/form.vue
@@ -0,0 +1,114 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesProProcessApi } from '#/api/mes/pro/process';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Divider, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createProcess,
+ getProcess,
+ updateProcess,
+} from '#/api/mes/pro/process';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import ContentList from './content-list.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+const formData = ref<MesProProcessApi.Process>();
+
+const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return $t('ui.actionTitle.detail', ['鐢熶骇宸ュ簭']);
+ }
+ return formType.value === 'update'
+ ? $t('ui.actionTitle.edit', ['鐢熶骇宸ュ簭'])
+ : $t('ui.actionTitle.create', ['鐢熶骇宸ュ簭']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-3',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-3',
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = (await formApi.getValues()) as MesProProcessApi.Process;
+ try {
+ await (formData.value?.id ? updateProcess(data) : createProcess(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;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ // 鍔犺浇鏁版嵁
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getProcess(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+
+/** 鐢ㄤ簬宸ュ簭鍐呭瀛愯〃鐨勫伐搴忕紪鍙� */
+const processId = computed(() => formData.value?.id);
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀哄伐搴忔搷浣滄楠ゅ瓙琛紝鏂板妯″紡涓嬮殣钘� -->
+ <template v-if="processId">
+ <Divider class="!my-3" orientation="left">鎿嶄綔姝ラ</Divider>
+ <div class="mx-4">
+ <ContentList :form-type="formType" :process-id="processId" />
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/components/color-picker.vue b/src/views/mes/process-design/route/components/color-picker.vue
new file mode 100644
index 0000000..f3edab9
--- /dev/null
+++ b/src/views/mes/process-design/route/components/color-picker.vue
@@ -0,0 +1,66 @@
+<script lang="ts" setup>
+import { computed } from 'vue';
+
+/** 鐢樼壒鍥鹃鑹查�夋嫨鍣紙ant-design-vue 4.x 鏆傛湭鎻愪緵 ColorPicker锛屽皝瑁呭師鐢� input[type=color]锛� */
+defineOptions({ name: 'RouteColorPicker' });
+
+const props = withDefaults(
+ defineProps<{
+ disabled?: boolean;
+ modelValue?: string;
+ }>(),
+ {
+ disabled: false,
+ modelValue: '',
+ },
+);
+
+const emit = defineEmits<{
+ change: [value: string];
+ 'update:modelValue': [value: string];
+}>();
+
+/** 鐢ㄤ簬 input[type=color] 鐨勫睍绀哄�硷紝蹇呴』鏄悎娉� hex锛涢潪娉曟椂鍥為��鍒� #000000 浣嗕笉淇敼 modelValue */
+const swatchValue = computed(() =>
+ /^#[0-9a-f]{6}$/i.test(props.modelValue ?? '')
+ ? (props.modelValue as string)
+ : '#000000',
+);
+
+/** 棰滆壊鍙樺寲鏃跺悓姝� modelValue */
+function handleColorChange(event: Event) {
+ const value = (event.target as HTMLInputElement).value;
+ emit('update:modelValue', value);
+ emit('change', value);
+}
+</script>
+
+<template>
+ <div class="flex items-center gap-2">
+ <input
+ class="route-color-picker__swatch"
+ :disabled="disabled"
+ type="color"
+ :value="swatchValue"
+ @change="handleColorChange"
+ @input="handleColorChange"
+ />
+ <span v-if="modelValue">{{ modelValue }}</span>
+ </div>
+</template>
+
+<style scoped>
+.route-color-picker__swatch {
+ inline-size: 36px;
+ block-size: 28px;
+ padding: 2px;
+ cursor: pointer;
+ border: 1px solid var(--ant-color-border, #d9d9d9);
+ border-radius: 4px;
+}
+
+.route-color-picker__swatch:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+}
+</style>
diff --git a/src/views/mes/process-design/route/components/index.ts b/src/views/mes/process-design/route/components/index.ts
new file mode 100644
index 0000000..4a78a4c
--- /dev/null
+++ b/src/views/mes/process-design/route/components/index.ts
@@ -0,0 +1 @@
+export { default as RouteColorPicker } from './color-picker.vue';
diff --git a/src/views/mes/process-design/route/data.ts b/src/views/mes/process-design/route/data.ts
new file mode 100644
index 0000000..a6c6810
--- /dev/null
+++ b/src/views/mes/process-design/route/data.ts
@@ -0,0 +1,517 @@
+import type { VbenFormApi, VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteApi } from '#/api/mes/pro/route';
+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 { h, markRaw } from 'vue';
+
+import {
+ CommonStatusEnum,
+ DICT_TYPE,
+ MesAutoCodeRuleCode,
+} from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { Button } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { generateAutoCode } from '#/api/mes/md/autocode/record';
+import {
+ MdItemSelect,
+ MdProductBomSelect,
+} from '#/views/mes/md/item/components';
+
+import { RouteColorPicker } from './components';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 宸ヨ壓璺嚎琛ㄥ崟 */
+export function useFormSchema(formApi?: VbenFormApi): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'code',
+ label: '璺嚎缂栫爜',
+ component: 'Input',
+ componentProps: {
+ maxLength: 64,
+ placeholder: '璇疯緭鍏ュ伐鑹鸿矾绾跨紪鐮�',
+ },
+ rules: z.string().min(1, '璺嚎缂栫爜涓嶈兘涓虹┖').max(64),
+ suffix: () =>
+ h(
+ Button,
+ {
+ type: 'default',
+ onClick: async () => {
+ const code = await generateAutoCode(
+ MesAutoCodeRuleCode.PRO_ROUTE_CODE,
+ );
+ await formApi?.setFieldValue('code', code);
+ },
+ },
+ { default: () => '鐢熸垚' },
+ ),
+ },
+ {
+ fieldName: 'name',
+ label: '璺嚎鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ュ伐鑹鸿矾绾垮悕绉�',
+ },
+ rules: z.string().min(1, '璺嚎鍚嶇О涓嶈兘涓虹┖').max(100),
+ },
+ {
+ fieldName: 'description',
+ label: '璺嚎璇存槑',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxLength: 500,
+ placeholder: '璇疯緭鍏ュ伐鑹鸿矾绾胯鏄�',
+ rows: 3,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '璺嚎缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヨ矾绾跨紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '璺嚎鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ヨ矾绾垮悕绉�',
+ },
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
+ placeholder: '璇烽�夋嫨鐘舵��',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃瀛楁 */
+export function useGridColumns(
+ onStatusChange?: (
+ newStatus: number,
+ row: MesProRouteApi.Route,
+ ) => PromiseLike<boolean | undefined>,
+ statusEditable = true,
+): VxeTableGridOptions<MesProRouteApi.Route>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '璺嚎缂栫爜',
+ minWidth: 160,
+ slots: { default: 'code' },
+ },
+ { field: 'name', title: '璺嚎鍚嶇О', minWidth: 180 },
+ { field: 'description', title: '璺嚎璇存槑', minWidth: 200 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 110,
+ align: 'center',
+ cellRender: {
+ attrs: { beforeChange: onStatusChange },
+ name: 'CellSwitch',
+ props: {
+ checkedValue: CommonStatusEnum.ENABLE,
+ disabled: !statusEditable,
+ unCheckedValue: CommonStatusEnum.DISABLE,
+ },
+ },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 160 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 160,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 宸ヨ壓璺嚎宸ュ簭鏄庣粏琛ㄥ崟 */
+export function useRouteProcessFormSchema(
+ processOptions: Array<{ label: string; value: number }>,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'routeId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'sort',
+ label: '搴忓彿',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 1,
+ precision: 0,
+ },
+ rules: z.number().default(1),
+ },
+ {
+ fieldName: 'processId',
+ label: '宸ュ簭',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: processOptions,
+ placeholder: '璇烽�夋嫨宸ュ簭',
+ showSearch: true,
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'linkType',
+ label: '涓庝笅閬撳伐搴忓叧绯�',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PRO_LINK_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨',
+ },
+ rules: z.number().default(3),
+ },
+ {
+ fieldName: 'colorCode',
+ label: '鐢樼壒鍥鹃鑹�',
+ component: RouteColorPicker,
+ },
+ {
+ fieldName: 'keyFlag',
+ label: '鏄惁鍏抽敭宸ュ簭',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ },
+ {
+ fieldName: 'checkFlag',
+ label: '鏄惁璐ㄦ纭',
+ component: 'Switch',
+ componentProps: {
+ checkedChildren: '鏄�',
+ unCheckedChildren: '鍚�',
+ },
+ rules: z.boolean().default(false),
+ },
+ {
+ fieldName: 'prepareTime',
+ label: '鍑嗗鏃堕棿(鍒�)',
+ component: 'InputNumber',
+ 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,
+ },
+ rules: z.number().default(0),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 宸ヨ壓璺嚎宸ュ簭鍒楄〃瀛楁 */
+export function useRouteProcessGridColumns(): VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>['columns'] {
+ return [
+ { field: 'sort', title: '搴忓彿', width: 70, align: 'center', fixed: 'left' },
+ { field: 'processCode', title: '宸ュ簭缂栫爜', width: 140, fixed: 'left' },
+ { field: 'processName', title: '宸ュ簭鍚嶇О', width: 140, fixed: 'left' },
+ { field: 'nextProcessName', title: '涓嬩竴閬撳伐搴�', width: 140 },
+ {
+ field: 'linkType',
+ title: '涓庝笅涓�閬撳伐搴忓叧绯�',
+ width: 160,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_LINK_TYPE },
+ },
+ },
+ {
+ field: 'keyFlag',
+ title: '鍏抽敭宸ュ簭',
+ width: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ {
+ field: 'checkFlag',
+ title: '璐ㄦ纭',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ { field: 'prepareTime', title: '鍑嗗鏃堕棿(鍒�)', width: 110 },
+ { field: 'waitTime', title: '绛夊緟鏃堕棿(鍒�)', width: 110 },
+ {
+ field: 'colorCode',
+ title: '鐢樼壒鍥鹃鑹�',
+ width: 130,
+ slots: { default: 'colorCode' },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 160 },
+ {
+ title: '鎿嶄綔',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧鍒楄〃瀛楁 */
+export function useRouteProductGridColumns(): VxeTableGridOptions<MesProRouteProductApi.RouteProduct>['columns'] {
+ return [
+ { field: 'itemCode', title: '浜у搧鐗╂枡缂栫爜', width: 150 },
+ { field: 'itemName', title: '浜у搧鐗╂枡鍚嶇О', width: 150 },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', width: 150 },
+ { field: 'unitName', title: '鍗曚綅', width: 80 },
+ { field: 'quantity', title: '鐢熶骇鏁伴噺', width: 100 },
+ {
+ field: 'productionTime',
+ title: '鐢熶骇鐢ㄦ椂',
+ width: 130,
+ slots: { default: 'productionTime' },
+ },
+ { field: 'remark', title: '澶囨敞', minWidth: 160 },
+ {
+ title: '鎿嶄綔',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧 BOM 鍒楄〃瀛楁 */
+export function useRouteProductBomGridColumns(): VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>['columns'] {
+ return [
+ { field: 'itemCode', title: 'BOM 鐗╂枡缂栫爜', width: 150 },
+ { field: 'itemName', title: 'BOM 鐗╂枡鍚嶇О', width: 150 },
+ { field: 'specification', title: '瑙勬牸鍨嬪彿', width: 150 },
+ { field: 'unitName', title: '鍗曚綅', width: 80 },
+ { field: 'quantity', title: '鐢ㄦ枡姣斾緥', width: 100 },
+ { field: 'remark', title: '澶囨敞', minWidth: 160 },
+ {
+ title: '鎿嶄綔',
+ width: 130,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧琛ㄥ崟 */
+export function useRouteProductFormSchema(
+ onItemChange?: (item: any) => void,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'routeId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ // 浜у搧鐗╂枡锛氫娇鐢ㄤ笟鍔¤嚜瀹氫箟閫夋嫨鍣紝change 鍥炲~缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅
+ {
+ fieldName: 'itemId',
+ label: '浜у搧',
+ component: markRaw(MdItemSelect),
+ componentProps: {
+ onChange: onItemChange,
+ },
+ formItemClass: 'col-span-2',
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'quantity',
+ label: '鐢熶骇鏁伴噺',
+ component: 'InputNumber',
+ 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,
+ },
+ rules: z.number().default(1),
+ },
+ {
+ fieldName: 'timeUnitType',
+ label: '鏃堕棿鍗曚綅',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_TIME_UNIT_TYPE),
+ placeholder: '璇烽�夋嫨',
+ },
+ rules: z.string().default('MINUTE'),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+/** 宸ヨ壓璺嚎浜у搧 BOM 琛ㄥ崟 */
+export function useRouteProductBomFormSchema(
+ itemId: () => number,
+ onBomChange?: (bom: any) => void,
+): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'routeId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'processId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'productId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ // BOM 鐗╂枡锛氫緷璧栦骇鍝佺墿鏂欙紝浣跨敤涓氬姟鑷畾涔夐�夋嫨鍣�
+ {
+ fieldName: 'itemId',
+ label: 'BOM 鐗╂枡',
+ component: markRaw(MdProductBomSelect),
+ componentProps: () => ({
+ itemId: itemId(),
+ onChange: onBomChange,
+ placeholder: '璇烽�夋嫨 BOM 鐗╂枡',
+ }),
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'quantity',
+ label: '鐢ㄦ枡姣斾緥',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ },
+ rules: z.number().default(1),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/process-design/route/index.vue b/src/views/mes/process-design/route/index.vue
new file mode 100644
index 0000000..2cb7372
--- /dev/null
+++ b/src/views/mes/process-design/route/index.vue
@@ -0,0 +1,180 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteApi } from '#/api/mes/pro/route';
+
+import { useAccess } from '../../../../packages/effects/access/src';
+import { confirm, Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
+import { CommonStatusEnum, DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictLabel } from '../../../../packages/effects/hooks/src';
+import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
+
+import { Button, message, Tooltip } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteRoute,
+ exportRoute,
+ getRoutePage,
+ updateRouteStatus,
+} from '#/api/mes/pro/route';
+import { $t } from '#/locales';
+
+import { useGridColumns, useGridFormSchema } from './data';
+import Form from './modules/form.vue';
+
+const { hasAccessByCodes } = useAccess();
+const statusEditable = hasAccessByCodes(['mes:pro-route:update']); // 鏄惁鍙垏鎹㈢姸鎬�
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+/** 鍒锋柊琛ㄦ牸 */
+function handleRefresh() {
+ gridApi.query();
+}
+
+/** 鍒涘缓宸ヨ壓璺嚎 */
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+/** 缂栬緫宸ヨ壓璺嚎锛堜粎鍋滅敤鐘舵�佸彲缂栬緫锛� */
+function handleEdit(row: MesProRouteApi.Route) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
+}
+
+/** 璇︽儏鏌ョ湅 */
+function handleDetail(row: MesProRouteApi.Route) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒囨崲鐘舵�� */
+async function handleStatusChange(
+ newStatus: number,
+ row: MesProRouteApi.Route,
+): Promise<boolean | undefined> {
+ try {
+ await confirm(
+ `纭瑕佸皢"${row.name}"宸ヨ壓璺嚎鍒囨崲涓恒��${getDictLabel(DICT_TYPE.COMMON_STATUS, newStatus)}銆戝悧锛焋,
+ );
+ } catch {
+ return false;
+ }
+ await updateRouteStatus(row.id!, newStatus);
+ message.success($t('ui.actionMessage.operationSuccess'));
+ return true;
+}
+
+/** 鍒犻櫎锛堜粎鍋滅敤鐘舵�佸彲鍒犻櫎锛� */
+async function handleDelete(row: MesProRouteApi.Route) {
+ const hideLoading = message.loading({
+ content: $t('ui.actionMessage.deleting', [row.name]),
+ duration: 0,
+ });
+ try {
+ await deleteRoute(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
+ handleRefresh();
+ } finally {
+ hideLoading();
+ }
+}
+
+/** 瀵煎嚭 */
+async function handleExport() {
+ const data = await exportRoute(await gridApi.formApi.getValues());
+ downloadFileFromBlobPart({ fileName: '宸ヨ壓璺嚎.xls', source: data });
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(handleStatusChange, statusEditable),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getRoutePage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ isHover: true,
+ keyField: 'id',
+ },
+ toolbarConfig: { refresh: true, search: true },
+ } as VxeTableGridOptions<MesProRouteApi.Route>,
+});
+</script>
+
+<template>
+ <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Grid table-title="宸ヨ壓璺嚎鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['宸ヨ壓璺嚎']),
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-route:create'],
+ onClick: handleCreate,
+ },
+ {
+ label: $t('ui.actionTitle.export'),
+ type: 'primary',
+ icon: ACTION_ICON.DOWNLOAD,
+ auth: ['mes:pro-route:export'],
+ onClick: handleExport,
+ },
+ ]"
+ />
+ </template>
+ <template #code="{ row }">
+ <Button type="link" @click="handleDetail(row)">{{ row.code }}</Button>
+ </template>
+ <template #actions="{ row }">
+ <Tooltip
+ :open="row.status === CommonStatusEnum.DISABLE ? false : undefined"
+ title="浠呭仠鐢ㄧ姸鎬侊紝鎵嶅彲浠ユ搷浣�"
+ >
+ <span class="inline-block">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['mes:pro-route:update'],
+ disabled: row.status !== CommonStatusEnum.DISABLE,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['mes:pro-route:delete'],
+ disabled: row.status !== CommonStatusEnum.DISABLE,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', [row.name]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </span>
+ </Tooltip>
+ </template>
+ </Grid>
+ </Page>
+</template>
diff --git a/src/views/mes/process-design/route/modules/bom-form.vue b/src/views/mes/process-design/route/modules/bom-form.vue
new file mode 100644
index 0000000..5c9b0e2
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/bom-form.vue
@@ -0,0 +1,118 @@
+<script lang="ts" setup>
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createRouteProductBom,
+ getRouteProductBom,
+ updateRouteProductBom,
+} from '#/api/mes/pro/route/productbom';
+import { $t } from '#/locales';
+
+import { useRouteProductBomFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProRouteProductBomApi.RouteProductBom>();
+const productId = ref<number>(0); // 褰撳墠浜у搧鐗╂枡缂栧彿锛屼緵 BOM 閫夋嫨鍣ㄧ瓫閫夊彲鐢� BOM
+const getTitle = computed(() =>
+ formData.value?.id
+ ? $t('ui.actionTitle.edit', ['BOM 鐗╂枡'])
+ : $t('ui.actionTitle.create', ['BOM 鐗╂枡']),
+);
+
+/** MdProductBomSelect change 鍥炶皟锛氭妸 BOM 鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅/榛樿鐢ㄩ噺鍥炲~鍒� form */
+async function handleBomChange(bom?: any) {
+ if (!bom) {
+ return;
+ }
+ await formApi.setValues({
+ itemCode: bom.bomItemCode,
+ itemName: bom.bomItemName,
+ quantity: bom.quantity ?? 1,
+ specification: bom.specification,
+ unitName: bom.unitName,
+ });
+}
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 110,
+ },
+ layout: 'horizontal',
+ schema: useRouteProductBomFormSchema(() => productId.value, handleBomChange),
+ showDefaultActions: false,
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data =
+ (await formApi.getValues()) as MesProRouteProductBomApi.RouteProductBom;
+ try {
+ await (formData.value?.id
+ ? updateRouteProductBom(data)
+ : createRouteProductBom(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<{
+ id?: number;
+ processId: number;
+ productId: number;
+ routeId: number;
+ }>();
+ if (!data) {
+ return;
+ }
+ productId.value = data.productId;
+ if (!data.id) {
+ // 鏂板鏃讹紝缁� routeId/processId/productId 鍏滃簳榛樿鍊�
+ await formApi.setValues({
+ processId: data.processId,
+ productId: data.productId,
+ quantity: 1,
+ routeId: data.routeId,
+ });
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getRouteProductBom(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-1/3">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/form.vue b/src/views/mes/process-design/route/modules/form.vue
new file mode 100644
index 0000000..5e7f807
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/form.vue
@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesProRouteApi } from '#/api/mes/pro/route';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message, TabPane, Tabs } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { createRoute, getRoute, updateRoute } from '#/api/mes/pro/route';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import ProcessList from './process-list.vue';
+import ProductList from './product-list.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create'); // 琛ㄥ崟妯″紡
+const subTab = ref('process'); // 褰撳墠婵�娲荤殑瀛愯〃 Tab
+const formData = ref<MesProRouteApi.Route>();
+
+const isDetail = computed(() => formType.value === 'detail'); // 鏄惁鏌ョ湅妯″紡
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return $t('ui.actionTitle.detail', ['宸ヨ壓璺嚎']);
+ }
+ return formType.value === 'update'
+ ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎'])
+ : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data = (await formApi.getValues()) as MesProRouteApi.Route;
+ try {
+ if (formType.value === 'create') {
+ // 鏂板鎴愬姛鍚庡垏鍒扮紪杈戞ā寮忥紝鏂逛究缁х画缁存姢缁勬垚宸ュ簭銆佸叧鑱斾骇鍝�
+ const id = await createRoute(data);
+ formData.value = { ...data, id };
+ await formApi.setFieldValue('id', id);
+ formType.value = 'update';
+ } else {
+ await updateRoute(data);
+ formData.value = { ...formData.value, ...data };
+ }
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+ formApi.setState({ schema: useFormSchema(formApi) });
+ subTab.value = 'process';
+ // 鍔犺浇鏁版嵁
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getRoute(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <Form class="mx-4" />
+ <!-- 缂栬緫/璇︽儏妯″紡涓嬪睍绀哄瓙琛� Tab锛屾柊澧炴ā寮忎笅闅愯棌 -->
+ <Tabs
+ v-if="formType !== 'create' && formData?.id"
+ v-model:active-key="subTab"
+ class="mx-4 mt-4"
+ >
+ <TabPane key="process" tab="缁勬垚宸ュ簭">
+ <ProcessList :form-type="formType" :route-id="formData.id" />
+ </TabPane>
+ <TabPane key="product" tab="鍏宠仈浜у搧">
+ <ProductList :form-type="formType" :route-id="formData.id" />
+ </TabPane>
+ </Tabs>
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/process-form.vue b/src/views/mes/process-design/route/modules/process-form.vue
new file mode 100644
index 0000000..5ef7593
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/process-form.vue
@@ -0,0 +1,121 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '#/adapter/form';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getProcessSimpleList } from '#/api/mes/pro/process';
+import {
+ createRouteProcess,
+ getRouteProcess,
+ updateRouteProcess,
+} from '#/api/mes/pro/route/process';
+import { $t } from '#/locales';
+
+import { useRouteProcessFormSchema } from '../data';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProRouteProcessApi.RouteProcess>();
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎宸ュ簭'])
+ : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎宸ュ簭']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+/** 鍔犺浇宸ュ簭閫夐」鍚庡啀鐢熸垚 schema锛堥伩鍏嶄笅鎷夌┖閫夐」锛� */
+async function loadSchema(): Promise<VbenFormSchema[]> {
+ const list = await getProcessSimpleList();
+ const options = (list || []).map((item) => ({
+ label: item.name ?? '',
+ value: item.id!,
+ }));
+ return useRouteProcessFormSchema(options);
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data =
+ (await formApi.getValues()) as MesProRouteProcessApi.RouteProcess;
+ try {
+ await (formData.value?.id
+ ? updateRouteProcess(data)
+ : createRouteProcess(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;
+ }
+ modalApi.lock();
+ try {
+ // 宸ュ簭涓嬫媺渚濊禆杩滅▼鏁版嵁锛宻chema 鍦ㄥ脊绐楁墦寮�鏃跺啀鐢熸垚
+ const schema = await loadSchema();
+ formApi.setState({ schema });
+ } finally {
+ modalApi.unlock();
+ }
+ // 鍔犺浇鏁版嵁
+ const data = modalApi.getData<{
+ id?: number;
+ maxSort?: number;
+ routeId: number;
+ }>();
+ if (!data) {
+ return;
+ }
+ if (!data.id) {
+ // 鏂板鏃讹紝榛樿搴忓彿 = maxSort + 1锛屽苟缁欎竴涓粯璁ょ敇鐗瑰浘棰滆壊
+ await formApi.setValues({
+ colorCode: '#00AEF3',
+ routeId: data.routeId,
+ sort: (data.maxSort || 0) + 1,
+ });
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getRouteProcess(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/process-list.vue b/src/views/mes/process-design/route/modules/process-list.vue
new file mode 100644
index 0000000..70a992d
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/process-list.vue
@@ -0,0 +1,136 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteRouteProcess,
+ getRouteProcessListByRoute,
+} from '#/api/mes/pro/route/process';
+import { $t } from '#/locales';
+
+import { useRouteProcessGridColumns } from '../data';
+import ProcessForm from './process-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ routeId: number;
+}>();
+
+const isEditable = ref(props.formType !== 'detail'); // 鏄惁鍙紪杈�
+const list = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ヨ壓璺嚎宸ュ簭鍒楄〃
+
+const [ProcessFormModal, processFormModalApi] = useVbenModal({
+ connectedComponent: ProcessForm,
+ destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ autoResize: true,
+ border: true,
+ columns: useRouteProcessGridColumns(),
+ data: list.value,
+ minHeight: 240,
+ pagerConfig: { enabled: false },
+ rowConfig: { isHover: true, keyField: 'id' },
+ showOverflow: true,
+ toolbarConfig: { enabled: false },
+ } as VxeTableGridOptions<MesProRouteProcessApi.RouteProcess>,
+});
+
+/** 鍔犺浇宸ヨ壓璺嚎宸ュ簭鍒楄〃 */
+async function getList() {
+ gridApi.setLoading(true);
+ try {
+ list.value = await getRouteProcessListByRoute(props.routeId);
+ gridApi.setGridOptions({ data: list.value });
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+/** 鏂板宸ヨ壓璺嚎宸ュ簭 */
+function handleCreate() {
+ const maxSort = Math.max(0, ...list.value.map((item) => item.sort || 0));
+ processFormModalApi.setData({ maxSort, routeId: props.routeId }).open();
+}
+
+/** 缂栬緫宸ヨ壓璺嚎宸ュ簭 */
+function handleEdit(row: MesProRouteProcessApi.RouteProcess) {
+ processFormModalApi.setData({ id: row.id, routeId: props.routeId }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓璺嚎宸ュ簭 */
+async function handleDelete(row: MesProRouteProcessApi.RouteProcess) {
+ await deleteRouteProcess(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['宸ヨ壓璺嚎宸ュ簭']));
+ await getList();
+}
+
+watch(
+ () => props.routeId,
+ (value) => {
+ if (value) {
+ getList();
+ }
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <ProcessFormModal @success="getList" />
+ <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['宸ュ簭']),
+ type: 'primary',
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </div>
+ <Grid class="w-full" table-title="缁勬垚宸ュ簭">
+ <template #colorCode="{ row }">
+ <div v-if="row.colorCode" class="flex items-center justify-center gap-1">
+ <div
+ class="h-4 w-4 rounded"
+ :style="{ backgroundColor: row.colorCode }"
+ ></div>
+ <span>{{ row.colorCode }}</span>
+ </div>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ ifShow: () => isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ ifShow: () => isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', ['宸ヨ壓璺嚎宸ュ簭']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/process-design/route/modules/product-bom-list.vue b/src/views/mes/process-design/route/modules/product-bom-list.vue
new file mode 100644
index 0000000..d4d8995
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/product-bom-list.vue
@@ -0,0 +1,171 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
+import type { MesProRouteProductBomApi } from '#/api/mes/pro/route/productbom';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { message, TabPane, Tabs } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getRouteProcessListByRoute } from '#/api/mes/pro/route/process';
+import {
+ deleteRouteProductBom,
+ getRouteProductBomList,
+} from '#/api/mes/pro/route/productbom';
+import { $t } from '#/locales';
+
+import { useRouteProductBomGridColumns } from '../data';
+import BomForm from './bom-form.vue';
+
+const props = defineProps<{
+ productId: number;
+ productName?: string;
+ routeId: number;
+}>();
+
+const processList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ュ簭鍒楄〃锛堢敤浜� Tab锛�
+const activeProcessId = ref<number>(); // 褰撳墠閫変腑鐨勫伐搴� Tab
+const list = ref<MesProRouteProductBomApi.RouteProductBom[]>([]); // 褰撳墠宸ュ簭涓嬬殑 BOM 鍒楄〃
+
+const [BomFormModal, bomFormModalApi] = useVbenModal({
+ connectedComponent: BomForm,
+ destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ autoResize: true,
+ border: true,
+ columns: useRouteProductBomGridColumns(),
+ data: list.value,
+ minHeight: 200,
+ pagerConfig: { enabled: false },
+ rowConfig: { isHover: true, keyField: 'id' },
+ showOverflow: true,
+ toolbarConfig: { enabled: false },
+ } as VxeTableGridOptions<MesProRouteProductBomApi.RouteProductBom>,
+});
+
+/** 鍔犺浇璺嚎涓嬬殑宸ュ簭鍒楄〃锛岀敤浜庡伐搴� Tab */
+async function loadProcessList() {
+ processList.value = (await getRouteProcessListByRoute(props.routeId)) || [];
+ if (processList.value.length > 0) {
+ activeProcessId.value = processList.value[0]!.processId;
+ await getList();
+ } else {
+ activeProcessId.value = undefined;
+ list.value = [];
+ gridApi.setGridOptions({ data: list.value });
+ }
+}
+
+/** 鍔犺浇褰撳墠宸ュ簭涓嬬殑 BOM 鍒楄〃 */
+async function getList() {
+ if (!activeProcessId.value) {
+ return;
+ }
+ gridApi.setLoading(true);
+ try {
+ list.value = await getRouteProductBomList({
+ processId: activeProcessId.value,
+ productId: props.productId,
+ routeId: props.routeId,
+ });
+ gridApi.setGridOptions({ data: list.value });
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+/** 鏂板 BOM */
+function handleCreate() {
+ if (!activeProcessId.value) {
+ message.warning('璇峰厛閫夋嫨宸ュ簭');
+ return;
+ }
+ bomFormModalApi
+ .setData({
+ processId: activeProcessId.value,
+ productId: props.productId,
+ routeId: props.routeId,
+ })
+ .open();
+}
+
+/** 缂栬緫 BOM */
+function handleEdit(row: MesProRouteProductBomApi.RouteProductBom) {
+ bomFormModalApi
+ .setData({
+ id: row.id,
+ processId: activeProcessId.value!,
+ productId: props.productId,
+ routeId: props.routeId,
+ })
+ .open();
+}
+
+/** 鍒犻櫎 BOM */
+async function handleDelete(row: MesProRouteProductBomApi.RouteProductBom) {
+ await deleteRouteProductBom(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['BOM 鐗╂枡']));
+ await getList();
+}
+
+watch(
+ () => [props.routeId, props.productId],
+ () => {
+ if (props.routeId && props.productId) {
+ loadProcessList();
+ }
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <BomFormModal @success="getList" />
+ <Tabs v-model:active-key="activeProcessId" @change="getList">
+ <TabPane
+ v-for="item in processList"
+ :key="item.processId"
+ :tab="item.processName"
+ />
+ </Tabs>
+ <div class="mb-3 flex items-center justify-start">
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['BOM 鐗╂枡']),
+ type: 'primary',
+ disabled: !activeProcessId,
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </div>
+ <Grid class="w-full" table-title="浜у搧 BOM">
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', ['BOM 鐗╂枡']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/process-design/route/modules/product-form.vue b/src/views/mes/process-design/route/modules/product-form.vue
new file mode 100644
index 0000000..2d0db55
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/product-form.vue
@@ -0,0 +1,125 @@
+<script lang="ts" setup>
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Divider, message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createRouteProduct,
+ getRouteProduct,
+ updateRouteProduct,
+} from '#/api/mes/pro/route/product';
+import { $t } from '#/locales';
+
+import { useRouteProductFormSchema } from '../data';
+import ProductBomList from './product-bom-list.vue';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProRouteProductApi.RouteProduct>();
+const getTitle = computed(() =>
+ formData.value?.id
+ ? $t('ui.actionTitle.edit', ['宸ヨ壓璺嚎浜у搧'])
+ : $t('ui.actionTitle.create', ['宸ヨ壓璺嚎浜у搧']),
+);
+
+/** MdItemSelect change 鍥炶皟锛氭妸鐗╂枡缂栫爜/鍚嶇О/瑙勬牸/鍗曚綅鍥炲~鍒� form */
+async function handleItemChange(item?: any) {
+ if (!item) {
+ return;
+ }
+ await formApi.setValues({
+ itemCode: item.code,
+ itemName: item.name,
+ specification: item.specification,
+ unitName: item.unitMeasureName,
+ });
+}
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: useRouteProductFormSchema(handleItemChange),
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ modalApi.lock();
+ // 鎻愪氦琛ㄥ崟
+ const data =
+ (await formApi.getValues()) as MesProRouteProductApi.RouteProduct;
+ try {
+ if (formData.value?.id) {
+ await updateRouteProduct(data);
+ // 鐢ㄦ渶鏂拌〃鍗曞�煎悓姝� formData锛岀‘淇濅骇鍝� BOM 瀛愯〃缁戝畾鐨� itemId 涓庤〃鍗曚竴鑷�
+ formData.value = { ...formData.value, ...data };
+ } else {
+ // 鏂板鎴愬姛鍚庡垏鎹㈠埌缂栬緫妯″紡锛屾柟渚跨户缁淮鎶や骇鍝� BOM
+ const id = await createRouteProduct(data);
+ formData.value = { ...data, id };
+ await formApi.setFieldValue('id', id);
+ }
+ 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<{
+ id?: number;
+ routeId: number;
+ }>();
+ if (!data) {
+ return;
+ }
+ if (!data.id) {
+ await formApi.setValues({ routeId: data.routeId });
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getRouteProduct(data.id);
+ // 璁剧疆鍒� values
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ <!-- 缂栬緫妯″紡涓嬪睍绀轰骇鍝� BOM 瀛愯〃锛屾柊澧炴ā寮忎笅闅愯棌 -->
+ <template v-if="formData?.id && formData?.itemId">
+ <Divider class="!my-3" orientation="left">浜у搧 BOM 閰嶇疆</Divider>
+ <div class="mx-4">
+ <ProductBomList
+ :route-id="formData.routeId!"
+ :product-id="formData.itemId"
+ :product-name="formData.itemName"
+ />
+ </div>
+ </template>
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/route/modules/product-list.vue b/src/views/mes/process-design/route/modules/product-list.vue
new file mode 100644
index 0000000..e21a6d5
--- /dev/null
+++ b/src/views/mes/process-design/route/modules/product-list.vue
@@ -0,0 +1,134 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProRouteProductApi } from '#/api/mes/pro/route/product';
+
+import { ref, watch } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+import { DICT_TYPE } from '../../../../../packages/constants/src';
+import { getDictLabel } from '../../../../../packages/effects/hooks/src';
+
+import { message } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteRouteProduct,
+ getRouteProductListByRoute,
+} from '#/api/mes/pro/route/product';
+import { $t } from '#/locales';
+
+import { useRouteProductGridColumns } from '../data';
+import ProductForm from './product-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ routeId: number;
+}>();
+
+const isEditable = ref(props.formType !== 'detail'); // 鏄惁鍙紪杈�
+const list = ref<MesProRouteProductApi.RouteProduct[]>([]); // 宸ヨ壓璺嚎浜у搧鍒楄〃
+
+const [ProductFormModal, productFormModalApi] = useVbenModal({
+ connectedComponent: ProductForm,
+ destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ autoResize: true,
+ border: true,
+ columns: useRouteProductGridColumns(),
+ data: list.value,
+ minHeight: 240,
+ pagerConfig: { enabled: false },
+ rowConfig: { isHover: true, keyField: 'id' },
+ showOverflow: true,
+ toolbarConfig: { enabled: false },
+ } as VxeTableGridOptions<MesProRouteProductApi.RouteProduct>,
+});
+
+/** 鍔犺浇宸ヨ壓璺嚎浜у搧鍒楄〃 */
+async function getList() {
+ gridApi.setLoading(true);
+ try {
+ list.value = await getRouteProductListByRoute(props.routeId);
+ gridApi.setGridOptions({ data: list.value });
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+/** 鏂板宸ヨ壓璺嚎浜у搧 */
+function handleCreate() {
+ productFormModalApi.setData({ routeId: props.routeId }).open();
+}
+
+/** 缂栬緫宸ヨ壓璺嚎浜у搧 */
+function handleEdit(row: MesProRouteProductApi.RouteProduct) {
+ productFormModalApi.setData({ id: row.id, routeId: props.routeId }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓璺嚎浜у搧 */
+async function handleDelete(row: MesProRouteProductApi.RouteProduct) {
+ await deleteRouteProduct(row.id!);
+ message.success($t('ui.actionMessage.deleteSuccess', ['宸ヨ壓璺嚎浜у搧']));
+ await getList();
+}
+
+watch(
+ () => props.routeId,
+ (value) => {
+ if (value) {
+ getList();
+ }
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <ProductFormModal @success="getList" />
+ <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+ <TableAction
+ :actions="[
+ {
+ label: $t('ui.actionTitle.create', ['鍏宠仈浜у搧']),
+ type: 'primary',
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </div>
+ <Grid class="w-full" table-title="鍏宠仈浜у搧">
+ <template #productionTime="{ row }">
+ <span v-if="row.productionTime">
+ {{ row.productionTime }}
+ {{ getDictLabel(DICT_TYPE.MES_TIME_UNIT_TYPE, row.timeUnitType) }}
+ </span>
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: $t('common.edit'),
+ type: 'link',
+ ifShow: () => isEditable,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: $t('common.delete'),
+ type: 'link',
+ danger: true,
+ ifShow: () => isEditable,
+ popConfirm: {
+ title: $t('ui.actionMessage.deleteConfirm', ['宸ヨ壓璺嚎浜у搧']),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
diff --git a/src/views/mes/process-design/template/data.ts b/src/views/mes/process-design/template/data.ts
new file mode 100644
index 0000000..0e2f655
--- /dev/null
+++ b/src/views/mes/process-design/template/data.ts
@@ -0,0 +1,128 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
+
+import { DICT_TYPE } from '../../../../packages/constants/src';
+import { getDictOptions } from '../../../../packages/effects/hooks/src';
+
+import { z } from '#/adapter/form';
+
+/** 琛ㄥ崟绫诲瀷 */
+export type FormType = 'create' | 'detail' | 'update';
+
+/** 鏂板/淇敼宸ヨ壓鍙傛暟妯℃澘鐨勮〃鍗� */
+export function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '妯℃澘鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ maxLength: 100,
+ placeholder: '璇疯緭鍏ユā鏉垮悕绉�',
+ },
+ rules: z.string().min(1, '妯℃澘鍚嶇О涓嶈兘涓虹┖').max(100),
+ },
+ {
+ fieldName: 'type',
+ label: '妯℃澘绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_TEMPLATE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'parameterIds',
+ label: '鍏宠仈鍙傛暟',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ maxLength: 250,
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 3,
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勬悳绱㈣〃鍗� */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'name',
+ label: '妯℃澘鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ユā鏉垮悕绉�',
+ },
+ },
+ {
+ fieldName: 'type',
+ label: '妯℃澘绫诲瀷',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: getDictOptions(DICT_TYPE.MES_PD_TEMPLATE_TYPE, 'number'),
+ placeholder: '璇烽�夋嫨妯℃澘绫诲瀷',
+ },
+ },
+ ];
+}
+
+/** 鍒楄〃鐨勫瓧娈� */
+export function useGridColumns(): VxeTableGridOptions<MesPdTemplateApi.Template>['columns'] {
+ return [
+ {
+ type: 'seq',
+ title: '搴忓彿',
+ width: 60,
+ align: 'center',
+ },
+ { field: 'name', title: '妯℃澘鍚嶇О', minWidth: 180 },
+ {
+ field: 'type',
+ title: '妯℃澘绫诲瀷',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_TEMPLATE_TYPE },
+ },
+ },
+ { field: 'createUser', title: '鍒涘缓浜�', width: 120 },
+ {
+ field: 'createTime',
+ title: '鍒涘缓鏃堕棿',
+ width: 180,
+ formatter: 'formatDateTime',
+ },
+ {
+ title: '鎿嶄綔',
+ width: 150,
+ fixed: 'right',
+ slots: {
+ default: 'actions',
+ },
+ },
+ ];
+}
diff --git a/src/views/mes/md/unitmeasure/index.vue b/src/views/mes/process-design/template/index.vue
similarity index 63%
copy from src/views/mes/md/unitmeasure/index.vue
copy to src/views/mes/process-design/template/index.vue
index f9f49d4..0c6faa2 100644
--- a/src/views/mes/md/unitmeasure/index.vue
+++ b/src/views/mes/process-design/template/index.vue
@@ -1,18 +1,16 @@
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
-import type { MesMdUnitMeasureApi } from '#/api/mes/md/unitmeasure';
+import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
import { Page, useVbenModal } from '../../../../packages/effects/common-ui/src';
-import { downloadFileFromBlobPart } from '../../../../packages/utils/src';
-import { message } from 'ant-design-vue';
+import { Button, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
- deleteUnitMeasure,
- exportUnitMeasure,
- getUnitMeasurePage,
-} from '#/api/mes/md/unitmeasure';
+ deleteTemplate,
+ getTemplatePage,
+} from '#/api/mes/pro/processdesign/template';
import { $t } from '#/locales';
import { useGridColumns, useGridFormSchema } from './data';
@@ -28,30 +26,29 @@
gridApi.query();
}
-/** 瀵煎嚭琛ㄦ牸 */
-async function handleExport() {
- const data = await exportUnitMeasure(await gridApi.formApi.getValues());
- downloadFileFromBlobPart({ fileName: '璁¢噺鍗曚綅.xls', source: data });
-}
-
-/** 鍒涘缓璁¢噺鍗曚綅 */
+/** 鍒涘缓宸ヨ壓鍙傛暟妯℃澘 */
function handleCreate() {
- formModalApi.setData(null).open();
+ formModalApi.setData({ formType: 'create' }).open();
}
-/** 缂栬緫璁¢噺鍗曚綅 */
-function handleEdit(row: MesMdUnitMeasureApi.UnitMeasure) {
- formModalApi.setData(row).open();
+/** 缂栬緫宸ヨ壓鍙傛暟妯℃澘 */
+function handleEdit(row: MesPdTemplateApi.Template) {
+ formModalApi.setData({ id: row.id, formType: 'update' }).open();
}
-/** 鍒犻櫎璁¢噺鍗曚綅 */
-async function handleDelete(row: MesMdUnitMeasureApi.UnitMeasure) {
+/** 鏌ョ湅宸ヨ壓鍙傛暟妯℃澘璇︽儏 */
+function handleDetail(row: MesPdTemplateApi.Template) {
+ formModalApi.setData({ id: row.id, formType: 'detail' }).open();
+}
+
+/** 鍒犻櫎宸ヨ壓鍙傛暟妯℃澘 */
+async function handleDelete(row: MesPdTemplateApi.Template) {
const hideLoading = message.loading({
content: $t('ui.actionMessage.deleting', [row.name]),
duration: 0,
});
try {
- await deleteUnitMeasure(row.id!);
+ await deleteTemplate(row.id!);
message.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
@@ -70,7 +67,7 @@
proxyConfig: {
ajax: {
query: async ({ page }, formValues) => {
- return await getUnitMeasurePage({
+ return await getTemplatePage({
pageNo: page.currentPage,
pageSize: page.pageSize,
...formValues,
@@ -79,39 +76,35 @@
},
},
rowConfig: {
- keyField: 'id',
isHover: true,
+ keyField: 'id',
},
toolbarConfig: {
refresh: true,
search: true,
},
- } as VxeTableGridOptions<MesMdUnitMeasureApi.UnitMeasure>,
+ } as VxeTableGridOptions<MesPdTemplateApi.Template>,
});
</script>
<template>
<Page auto-content-height><FormModal @success="handleRefresh" />
- <Grid table-title="璁¢噺鍗曚綅鍒楄〃">
+ <Grid table-title="宸ヨ壓鍙傛暟妯℃澘鍒楄〃">
<template #toolbar-tools>
<TableAction
:actions="[
{
- label: $t('ui.actionTitle.create', ['璁¢噺鍗曚綅']),
+ label: $t('ui.actionTitle.create', ['妯℃澘']),
type: 'primary',
icon: ACTION_ICON.ADD,
- auth: ['mes:md-unit-measure:create'],
+ auth: ['mes:pd:template:create'],
onClick: handleCreate,
- },
- {
- label: $t('ui.actionTitle.export'),
- type: 'primary',
- icon: ACTION_ICON.DOWNLOAD,
- auth: ['mes:md-unit-measure:export'],
- onClick: handleExport,
},
]"
/>
+ </template>
+ <template #name="{ row }">
+ <Button type="link" @click="handleDetail(row)">{{ row.name }}</Button>
</template>
<template #actions="{ row }">
<TableAction
@@ -120,7 +113,7 @@
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
- auth: ['mes:md-unit-measure:update'],
+ auth: ['mes:pd:template:update'],
onClick: handleEdit.bind(null, row),
},
{
@@ -128,7 +121,7 @@
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
- auth: ['mes:md-unit-measure:delete'],
+ auth: ['mes:pd:template:delete'],
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.name]),
confirm: handleDelete.bind(null, row),
diff --git a/src/views/mes/process-design/template/modules/form.vue b/src/views/mes/process-design/template/modules/form.vue
new file mode 100644
index 0000000..8d824f3
--- /dev/null
+++ b/src/views/mes/process-design/template/modules/form.vue
@@ -0,0 +1,173 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '../../../../../packages/effects/common-ui/src';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import {
+ createTemplate,
+ getTemplate,
+ updateTemplate,
+} from '#/api/mes/pro/processdesign/template';
+import { $t } from '#/locales';
+
+import { useFormSchema } from '../data';
+import ParameterSelectDialog from './parameter-select-dialog.vue';
+
+const emit = defineEmits(['success']);
+const formType = ref<FormType>('create');
+const formData = ref<MesPdTemplateApi.Template>();
+
+/** 宸查�夊弬鏁板垪琛紙鐢ㄤ簬椤甸潰灞曠ず锛� */
+const selectedParameters = ref<MesPdParameterApi.Parameter[]>([]);
+const parameterSelectDialogRef = ref<InstanceType<typeof ParameterSelectDialog>>();
+
+const isDetail = computed(() => formType.value === 'detail');
+const getTitle = computed(() => {
+ if (formType.value === 'detail') {
+ return $t('ui.actionTitle.detail', ['宸ヨ壓鍙傛暟妯℃澘']);
+ }
+ return formType.value === 'update'
+ ? $t('ui.actionTitle.edit', ['宸ヨ壓鍙傛暟妯℃澘'])
+ : $t('ui.actionTitle.create', ['宸ヨ壓鍙傛暟妯℃澘']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: {
+ class: 'w-full',
+ },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [],
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+/** 鎵撳紑宸ヨ壓鍙傛暟閫夋嫨寮圭獥 */
+function handleSelectParameters() {
+ parameterSelectDialogRef.value?.open(
+ selectedParameters.value
+ .map((p) => p.id)
+ .filter((id): id is number => id !== undefined),
+ );
+}
+
+/** 鎺ユ敹閫変腑鐨勫弬鏁� */
+function onParametersSelected(rows: MesPdParameterApi.Parameter[]) {
+ selectedParameters.value = rows;
+}
+
+/** 绉婚櫎宸查�夊弬鏁� */
+function removeParameter(param: MesPdParameterApi.Parameter) {
+ selectedParameters.value = selectedParameters.value.filter(
+ (p) => p.id !== param.id,
+ );
+}
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ if (isDetail.value) {
+ await modalApi.close();
+ return;
+ }
+ const { valid } = await formApi.validate();
+ if (!valid) {
+ return;
+ }
+ if (selectedParameters.value.length === 0) {
+ message.warning('璇疯嚦灏戦�夋嫨涓�涓伐鑹哄弬鏁�');
+ return;
+ }
+ modalApi.lock();
+ const data = (await formApi.getValues()) as MesPdTemplateApi.Template;
+ data.parameterIds = selectedParameters.value
+ .map((p) => p.id)
+ .filter((id): id is number => id !== undefined);
+ try {
+ await (formData.value?.id ? updateTemplate(data) : createTemplate(data));
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ selectedParameters.value = [];
+ return;
+ }
+ formApi.setState({ schema: useFormSchema() });
+ const data = modalApi.getData<{ formType: FormType; id?: number }>();
+ formType.value = data.formType;
+ formApi.setDisabled(formType.value === 'detail');
+ modalApi.setState({ showConfirmButton: formType.value !== 'detail' });
+ if (!data?.id) {
+ return;
+ }
+ modalApi.lock();
+ try {
+ formData.value = await getTemplate(data.id);
+ await formApi.setValues(formData.value);
+ // 鍥炴樉宸查�夊弬鏁帮紙浠庤鎯呮帴鍙h繑鍥炵殑鍙傛暟鍒楄〃涓幏鍙栵級
+ if (formData.value.parameterIds) {
+ // parameterIds 宸茬敱鍚庣杩斿洖锛宻electedParameters 鍦ㄨ鎯呮ā寮忎笅灞曠ず
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-4/5">
+ <Form class="mx-4" />
+ <!-- 鍏宠仈鍙傛暟閫夋嫨鍖哄煙 -->
+ <div class="mx-4 mt-4">
+ <div class="mb-2 flex items-center justify-between">
+ <span class="text-sm font-medium">鍏宠仈宸ヨ壓鍙傛暟</span>
+ <Button
+ v-if="!isDetail"
+ type="primary"
+ size="small"
+ @click="handleSelectParameters"
+ >
+ 閫夋嫨鍙傛暟
+ </Button>
+ </div>
+ <div
+ v-if="selectedParameters.length > 0"
+ class="min-h-10 rounded border border-gray-200 p-2"
+ >
+ <Tag
+ v-for="param in selectedParameters"
+ :key="param.id"
+ :closable="!isDetail"
+ class="mb-1 mr-1"
+ @close="removeParameter(param)"
+ >
+ {{ param.name }}({{ param.code }})
+ </Tag>
+ </div>
+ <div v-else class="rounded border border-dashed border-gray-300 p-4 text-center text-gray-400">
+ 鏆傛湭閫夋嫨鍙傛暟
+ </div>
+ </div>
+ <ParameterSelectDialog
+ ref="parameterSelectDialogRef"
+ @selected="onParametersSelected"
+ />
+ </Modal>
+</template>
diff --git a/src/views/mes/process-design/template/modules/parameter-select-dialog.vue b/src/views/mes/process-design/template/modules/parameter-select-dialog.vue
new file mode 100644
index 0000000..b4f87c6
--- /dev/null
+++ b/src/views/mes/process-design/template/modules/parameter-select-dialog.vue
@@ -0,0 +1,207 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesPdParameterApi } from '#/api/mes/pro/processdesign/parameter';
+
+import { nextTick, ref } from 'vue';
+
+import { DICT_TYPE } from '../../../../../packages/constants/src';
+
+import { Button, message, Modal } from 'ant-design-vue';
+
+import { useVbenVxeGrid } from '#/adapter/vxe-table';
+import { getParameterPage } from '#/api/mes/pro/processdesign/parameter';
+
+defineOptions({ name: 'PdParameterSelectDialog' });
+
+const emit = defineEmits<{
+ selected: [rows: MesPdParameterApi.Parameter[]];
+}>();
+
+const open = ref(false);
+const selectedRows = ref<MesPdParameterApi.Parameter[]>([]);
+const preSelectedIds = ref<number[]>([]);
+
+/** 鑾峰彇澶氶�夎褰曪紝鍖呭惈 VXE reserve 璺ㄩ〉璁板綍 */
+function getMultipleSelectedRows() {
+ const selectedMap = new Map<number, MesPdParameterApi.Parameter>();
+ const records = [
+ ...(gridApi.grid.getCheckboxReserveRecords?.() ?? []),
+ ...(gridApi.grid.getCheckboxRecords?.() ?? []),
+ ] as MesPdParameterApi.Parameter[];
+ records.forEach((row) => {
+ if (row.id) {
+ selectedMap.set(row.id, row);
+ }
+ });
+ return [...selectedMap.values()];
+}
+
+/** 澶勭悊鍕鹃�夊彉鍖� */
+function handleCheckboxSelectChange() {
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶氶�夋ā寮忎笅鍒囨崲琛屽嬀閫� */
+async function toggleMultipleRow(row: MesPdParameterApi.Parameter) {
+ const selected = gridApi.grid.isCheckedByCheckboxRow(row);
+ await gridApi.grid.setCheckboxRow(row, !selected);
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 澶勭悊琛屽弻鍑� */
+async function handleCellDblclick({ row }: { row: MesPdParameterApi.Parameter }) {
+ await toggleMultipleRow(row);
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: [
+ {
+ fieldName: 'code',
+ label: '鍙傛暟缂栫爜',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ弬鏁扮紪鐮�',
+ },
+ },
+ {
+ fieldName: 'name',
+ label: '鍙傛暟鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ allowClear: true,
+ placeholder: '璇疯緭鍏ュ弬鏁板悕绉�',
+ },
+ },
+ ],
+ },
+ gridOptions: {
+ columns: [
+ { type: 'checkbox', width: 50 },
+ { field: 'code', title: '鍙傛暟缂栫爜', minWidth: 150 },
+ { field: 'name', title: '鍙傛暟鍚嶇О', minWidth: 180 },
+ {
+ field: 'type',
+ title: '鍙傛暟绫诲瀷',
+ width: 120,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PD_PARAM_TYPE },
+ },
+ },
+ { field: 'unit', title: '鍗曚綅', width: 80 },
+ {
+ field: 'requiredFlag',
+ title: '鏄惁蹇呭~',
+ width: 90,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
+ },
+ },
+ ],
+ height: 520,
+ keepSource: true,
+ checkboxConfig: {
+ highlight: true,
+ range: true,
+ reserve: true,
+ },
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getParameterPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesPdParameterApi.Parameter>,
+ gridEvents: {
+ cellDblclick: handleCellDblclick,
+ checkboxAll: handleCheckboxSelectChange,
+ checkboxChange: handleCheckboxSelectChange,
+ },
+});
+
+/** 鍥炴樉棰勯�夊弬鏁� */
+async function applyPreSelection() {
+ if (preSelectedIds.value.length === 0) {
+ return;
+ }
+ const rows = gridApi.grid.getTableData().fullData as MesPdParameterApi.Parameter[];
+ for (const row of rows) {
+ if (row.id === undefined || row.id === null || !preSelectedIds.value.includes(row.id)) {
+ continue;
+ }
+ await gridApi.grid.setCheckboxRow(row, true);
+ }
+ selectedRows.value = getMultipleSelectedRows();
+}
+
+/** 閲嶇疆鏌ヨ鍜岄�夋嫨鐘舵�� */
+async function resetQueryState() {
+ selectedRows.value = [];
+ await gridApi.grid.clearCheckboxRow();
+ await gridApi.grid.clearCheckboxReserve();
+ await gridApi.formApi.resetForm();
+}
+
+/** 鎵撳紑鍙傛暟閫夋嫨寮圭獥 */
+async function openModal(selectedIds?: number[]) {
+ open.value = true;
+ preSelectedIds.value = selectedIds || [];
+ await nextTick();
+ await resetQueryState();
+ await gridApi.query();
+ await nextTick();
+ await applyPreSelection();
+}
+
+/** 鍏抽棴鍙傛暟閫夋嫨寮圭獥 */
+async function closeModal() {
+ open.value = false;
+ await resetQueryState();
+}
+
+/** 纭閫夋嫨鍙傛暟 */
+function handleConfirm() {
+ const rows = getMultipleSelectedRows();
+ if (rows.length === 0) {
+ message.warning('璇疯嚦灏戦�夋嫨涓�鏉℃暟鎹�');
+ return;
+ }
+ emit('selected', rows);
+ 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>
diff --git a/vite.config.ts b/vite.config.ts
index 44d7717..4f673c6 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,4 +1,4 @@
-import { defineConfig } from './internal/vite-config/src';
+import { defineConfig } from "./internal/vite-config/src";
export default defineConfig(async () => {
return {
@@ -10,14 +10,14 @@
server: {
allowedHosts: true,
proxy: {
- '/admin-api': {
+ "/admin-api": {
changeOrigin: true,
- rewrite: (path) => path.replace(/^\/admin-api/, ''),
- target: 'http://192.168.0.226:48080/admin-api',
+ rewrite: (path) => path.replace(/^\/admin-api/, ""),
+ target: "http://192.168.0.244:48080/admin-api",
ws: true,
},
},
},
},
};
-});
\ No newline at end of file
+});
--
Gitblit v1.9.3