spring
2026-07-03 081f242ea3c019f4eddd0fe342c5cf6189da1468
fix: 产品设计
已复制2个文件
已添加63个文件
已重命名1个文件
已修改16个文件
已删除13个文件
11358 ■■■■ 文件已修改
.claude/settings.local.json 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.env.development 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
CLAUDE.md 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FRONTEND_DEVELOPMENT.md 311 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
mdm_frontend_change.md 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/archive/index.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/bom/index.ts 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/document/index.ts 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/drawing-review/index.ts 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/mock/enabled.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/mock/seed.ts 386 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/mock/store.ts 554 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/mock/utils.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/project/index.ts 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/task/index.ts 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/version-change/index.ts 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pd/workflow/index.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/processdesign/parameter/index.ts 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/mes/pro/processdesign/template/index.ts 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/biz-mes-enum.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/packages/constants/src/dict-enum.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/routes/modules/mes.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/client/data.ts 387 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/client/index.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/client/modules/form.vue 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/client/modules/import-form.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/client/modules/product-sales-line-list.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/client/modules/product-sales-list.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/item/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/unitmeasure/data.ts 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/unitmeasure/modules/form.vue 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/data.ts 381 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/index.vue 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/modules/form.vue 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/modules/import-form.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/modules/item-receipt-detail.vue 162 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/modules/item-receipt-line-list.vue 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/vendor/modules/item-receipt-list.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/md/workstation/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/archive/data.ts 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/archive/index.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/archive/modules/detail.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/data.ts 535 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/detail/data.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/detail/index.vue 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/index.vue 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/bom-form.vue 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/bom-list.vue 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/create-form.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/document-form.vue 142 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/document-list.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/drawing-review/data.ts 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/drawing-review/review-form.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/drawing-review/review-list.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/form.vue 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/material-panel.vue 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/publish-form.vue 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/task-draft-banner.vue 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/task/data.ts 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/task/task-form.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/task/task-list.vue 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/version-change/data.ts 167 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/version-change/version-change-form.vue 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/version-change/version-change-list.vue 186 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/workflow-action-form.vue 126 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pd/project/modules/workflow-timeline.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/andon/record/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/card/data.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/pro/task/data.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/parameter/data.ts 197 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/parameter/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/parameter/modules/form.vue 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/components/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/components/select.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/data.ts 274 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/index.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/modules/content-form.vue 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/modules/content-list.vue 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/process/modules/form.vue 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/components/color-picker.vue 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/components/index.ts 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/data.ts 517 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/index.vue 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/bom-form.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/form.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/process-form.vue 121 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/process-list.vue 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/product-bom-list.vue 171 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/product-form.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/route/modules/product-list.vue 134 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/template/data.ts 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/template/index.vue 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/template/modules/form.vue 173 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/mes/process-design/template/modules/parameter-select-dialog.vue 207 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
vite.config.ts 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.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\\\\', ''\\) })"
    ]
  }
}
.env.development
@@ -19,3 +19,6 @@
VITE_APP_DEFAULT_USERNAME=admin
# é»˜è®¤ç™»å½•密码
VITE_APP_DEFAULT_PASSWORD=admin123
# äº§å“è®¾è®¡æ¨¡å—本地 Mock(无后端时演示)
VITE_PD_MOCK=true
CLAUDE.md
@@ -92,3 +92,14 @@
2. ä½¿ç”¨ VeeValidate + Zod è¿›è¡Œè¡¨å•验证
3. ä½¿ç”¨ `v-dompurify-html` å¤„理用户输入的 HTML
4. æ•æ„Ÿæ•°æ®ä½¿ç”¨ `secure-ls` åŠ å¯†å­˜å‚¨
## MES æ¨¡å—开发
> **重要**:开发 MES æ¨¡å—前,必须先阅读 `MES_DEVELOPMENT.md`,该文件包含:
> - é¡µé¢æ–‡ä»¶ç»“构规范(data.ts / index.vue / form.vue)
> - API æŽ¥å£ç¼–写模板
> - å¯¼å…¥è·¯å¾„层级规则(index.vue ç”¨ 4 å±‚ `..`,form.vue ç”¨ 5 å±‚ `..`)
> - å­—å…¸/枚举注册位置
> - è·¯ç”±è§„则(动态路由,无需手动配置)
> - Select/SelectDialog ç»„件模式
> - å¼€å‘检查清单
FRONTEND_DEVELOPMENT.md
@@ -12,13 +12,13 @@
```typescript
// âœ… æ­£ç¡®ï¼šç»„件文件使用 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>
// âœ… æ­£ç¡®ï¼šç»Ÿä¸€ä½¿ç”¨ <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 @@
}
// âœ… æ­£ç¡®ï¼šä½¿ç”¨ 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>
mdm_frontend_change.md
@@ -1,5 +1,7 @@
# ç‰©æ–™ä¸»æ•°æ®æ¨¡å— (MDM) å‰ç«¯å˜æ›´æ–‡æ¡£
> **基准文档**:本文档为 MDM æ¨¡å—çš„**业务与 API å˜æ›´åŸºå‡†**。页面实现方式(组件、表格、表单、目录结构)须遵循 [`FRONTEND_DEVELOPMENT.md`](./FRONTEND_DEVELOPMENT.md),本文档中的 Element Plus ç¤ºä¾‹ä»…描述业务字段与交互,不可直接照搬 UI æ¡†æž¶ã€‚
## ä¸€ã€æ–°å¢žé¡µé¢
### 1.1 ç‰©æ–™ä¸»æ•°æ®ç®¡ç†
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 {
  /** æ•°å­—化设计归档台账 */
  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 });
}
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}`);
}
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}`);
}
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);
}
src/api/mes/pd/mock/enabled.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,4 @@
/** æ˜¯å¦å¯ç”¨äº§å“è®¾è®¡ï¼ˆPD)模块本地 Mock */
export function isPdMockEnabled(): boolean {
  return import.meta.env.VITE_PD_MOCK === 'true';
}
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: '散热优化改版,待归档',
    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: '网关外壳工程图',
    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: '散热优化 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: '网关外壳工程图',
    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: '散热优化 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: '优化散热布局',
    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 = [];
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] ?? []);
  },
};
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')}`;
}
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 },
  });
}
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 },
  });
}
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 },
  });
}
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 } },
  );
}
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}`,
  );
}
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}`,
  );
}
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,
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 - ä»“储管理模块 ========== */
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'),
      },
    ],
  },
];
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: '社会信用代码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入统一社会信用代码',
      },
    },
    {
      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、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      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'] {
src/views/mes/md/client/index.vue
ÎļþÒÑɾ³ý
src/views/mes/md/client/modules/form.vue
ÎļþÒÑɾ³ý
src/views/mes/md/client/modules/import-form.vue
ÎļþÒÑɾ³ý
src/views/mes/md/client/modules/product-sales-line-list.vue
ÎļþÒÑɾ³ý
src/views/mes/md/client/modules/product-sales-list.vue
ÎļþÒÑɾ³ý
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,
src/views/mes/md/unitmeasure/data.ts
ÎļþÒÑɾ³ý
src/views/mes/md/unitmeasure/modules/form.vue
ÎļþÒÑɾ³ý
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: '社会信用代码',
      component: 'Input',
      componentProps: {
        placeholder: '请输入统一社会信用代码',
      },
    },
    {
      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、xlsx æ ¼å¼æ–‡ä»¶',
    },
    {
      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'] {
src/views/mes/md/vendor/index.vue
ÎļþÒÑɾ³ý
src/views/mes/md/vendor/modules/form.vue
ÎļþÒÑɾ³ý
src/views/mes/md/vendor/modules/import-form.vue
ÎļþÒÑɾ³ý
src/views/mes/md/vendor/modules/item-receipt-detail.vue
ÎļþÒÑɾ³ý
src/views/mes/md/vendor/modules/item-receipt-line-list.vue
ÎļþÒÑɾ³ý
src/views/mes/md/vendor/modules/item-receipt-list.vue
ÎļþÒÑɾ³ý
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';
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: "请输入归档编号" },
    },
    {
      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",
    },
  ];
}
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>
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="`正在查看 ${materialVersion} ç‰ˆæœ¬èµ„料(${materialVersionHint})`"
            @close="clearMaterialVersionFilter"
          />
          <MaterialPanel
            form-type="detail"
            :project-id="detail.projectId"
            table-title="版本资料"
            :version="materialVersion"
          />
        </Tabs.TabPane>
      </Tabs>
    </template>
  </Modal>
</template>
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、V2.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:
      '审核通过后任务进入「已通过」状态,可进行归档操作。请确认设计资料符合要求。',
    remarkLabel: '审核意见',
    remarkTag: '【审核意见】',
    remarkPlaceholder: '可选,填写审核通过意见',
    successMessage: '资料审核通过',
    alertType: 'success',
  },
  archive: {
    title: '归档设计任务',
    alertMessage: '确认归档',
    alertDescription:
      '归档后将生成本版本归档台账记录,任务状态变为已归档,设计资料将锁定不可修改。',
    remarkLabel: '归档说明',
    remarkTag: '【归档说明】',
    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 };
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: '备注' },
  ];
}
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>
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>
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>
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>
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>
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>
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>
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 },
    },
  ];
}
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>
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>
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>
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>
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>
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>
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 },
    },
  ];
}
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>
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>
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 },
    },
  ];
}
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>
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>
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>
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>
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';
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';
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';
/** å¾…排产工单列表的搜索表单 */
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: '请输入接口地址(下拉选项时配置)',
      },
      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',
      },
    },
  ];
}
src/views/mes/process-design/parameter/index.vue
copy from src/views/mes/md/unitmeasure/index.vue copy to src/views/mes/process-design/parameter/index.vue
Îļþ´Ó src/views/mes/md/unitmeasure/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),
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>
src/views/mes/process-design/process/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
export { default as ProProcessSelect } from './select.vue';
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>
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' },
    },
  ];
}
src/views/mes/process-design/process/index.vue
ÎļþÃû´Ó src/views/mes/md/unitmeasure/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),
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>
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>
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>
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>
src/views/mes/process-design/route/components/index.ts
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
export { default as RouteColorPicker } from './color-picker.vue';
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,
      },
    },
  ];
}
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>
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>
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>
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 {
      // å·¥åºä¸‹æ‹‰ä¾èµ–远程数据,schema åœ¨å¼¹çª—打开时再生成
      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>
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>
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>
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>
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>
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',
      },
    },
  ];
}
src/views/mes/process-design/template/index.vue
copy from src/views/mes/md/unitmeasure/index.vue copy to src/views/mes/process-design/template/index.vue
Îļþ´Ó src/views/mes/md/unitmeasure/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),
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);
      // å›žæ˜¾å·²é€‰å‚数(从详情接口返回的参数列表中获取)
      if (formData.value.parameterIds) {
        // parameterIds å·²ç”±åŽç«¯è¿”回,selectedParameters åœ¨è¯¦æƒ…模式下展示
      }
    } 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>
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>
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,
          },
        },
      },
    },
  };
});
});