From f08a81f01bc4a4104dd9ef04e071bb0bd0eb8afe Mon Sep 17 00:00:00 2001
From: zhangwencui <1064582902@qq.com>
Date: 星期四, 23 七月 2026 10:50:19 +0800
Subject: [PATCH] Merge branch 'dev_pro2.0' of http://114.132.189.42:9002/r/mom-pro2-before into dev_pro2.0
---
CLAUDE.md | 6
src/api/mes/pro/task/index.ts | 42
src/views/wls/materialstock/data.ts | 11
src/views/mes/pro/workorder-center/data.ts | 182 +++
.claude/skills/vben-table-form.md | 421 +++++++
pnpm-lock.yaml | 64
src/api/mes/pro/workorder/process/index.ts | 69 +
src/views/mes/pro/feedback/index.vue | 40
src/views/mes/pro/task/modules/schedule-form.vue | 50
src/views/mes/pro/workorder-center/modules/quick-feedback.vue | 158 ++
src/views/wls/materialstock/components/index.ts | 1
src/api/mes/pro/feedback/index.ts | 55
src/views/mes/pro/workorder/modules/process-list.vue | 304 +++++
.claude/rules/typescript.md | 135 ++
.claude/skills/api-service-pattern.md | 213 +++
src/components/file-preview/file-preview-modal.vue | 4
src/views/mes/pro/workorder/modules/process-form.vue | 319 +++++
src/views/mes/pro/feedback/modules/batch-feedback.vue | 147 ++
.claude/settings.local.json | 13
src/views/mes/pro/workorder-center/modules/schedule-preview.vue | 86 +
src/views/wls/materialstock/index.vue | 33
src/views/mes/pro/workorder/data.ts | 103 +
src/views/mes/pro/workorder-center/index.vue | 313 +++++
src/api/mes/pro/workorder/index.ts | 47
src/views/mes/pro/task/index.vue | 44
src/views/mes/pro/workorder/modules/form.vue | 115 +
package.json | 5
src/api/mes/wm/materialstock/index.ts | 15
.claude/skills/vue-component-patterns.md | 170 ++
internal/node-utils/package.json | 3
.claude/rules/vue-component.md | 166 ++
src/views/wls/materialstock/components/move-modal.vue | 237 +++
32 files changed, 3,487 insertions(+), 84 deletions(-)
diff --git a/.claude/rules/typescript.md b/.claude/rules/typescript.md
new file mode 100644
index 0000000..e2f700b
--- /dev/null
+++ b/.claude/rules/typescript.md
@@ -0,0 +1,135 @@
+---
+paths:
+ - "**/*.ts"
+ - "**/*.vue"
+---
+
+# TypeScript 鍓嶇寮�鍙戣鍒�
+
+> 鍩轰簬 ECC TypeScript 瑙勫垯鎵╁睍锛岄�傞厤 Vue Vben Admin 椤圭洰銆�
+
+## 绫诲瀷瀹氫箟
+
+### 绂佹 any
+
+```typescript
+// 鉂� 閿欒锛氫娇鐢� any
+function handleData(data: any) {
+ return data.value;
+}
+
+// 鉁� 姝g‘锛氫娇鐢ㄥ叿浣撶被鍨�
+interface DataType {
+ value: string;
+}
+
+function handleData(data: DataType) {
+ return data.value;
+}
+
+// 鉁� 姝g‘锛氫娇鐢� unknown + 绫诲瀷瀹堝崼
+function getErrorMessage(error: unknown): string {
+ if (error instanceof Error) {
+ return error.message;
+ }
+ return '鏈煡閿欒';
+}
+```
+
+### import type
+
+```typescript
+// 鉁� 姝g‘锛氱被鍨嬪鍏ヤ娇鐢� import type
+import type { UserInfo } from '#/api/system/user';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+
+// 鉂� 閿欒锛氭櫘閫氬鍏ョ被鍨�
+import { UserInfo } from '#/api/system/user';
+```
+
+### 鎺ュ彛瀹氫箟
+
+```typescript
+// 鉁� 姝g‘锛氬疄浣撴帴鍙�
+export namespace UserApi {
+ export interface User {
+ id?: number;
+ username: string;
+ nickname?: string;
+ status: number;
+ }
+}
+
+// 鉁� 姝g‘锛歅rops 鎺ュ彛
+interface Props {
+ userId: number;
+ userName?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ userName: '',
+});
+```
+
+## 涓嶅彲鍙樻��
+
+```typescript
+// 鉂� 閿欒锛氱洿鎺ヤ慨鏀� props
+props.userName = 'new name';
+
+// 鉁� 姝g‘锛氫娇鐢� emit 鏇存柊
+emit('update:userName', 'new name');
+
+// 鉁� 姝g‘锛氫娇鐢ㄥ睍寮�杩愮畻绗�
+const newUser = { ...user, name: 'new name' };
+```
+
+## 閿欒澶勭悊
+
+```typescript
+// 鉁� 姝g‘锛歛sync/await + try-catch
+async function loadUser(id: number) {
+ try {
+ const user = await getUser(id);
+ return user;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ message.error(error.message);
+ }
+ throw error;
+ }
+}
+```
+
+## 璺緞鍒悕
+
+```typescript
+// 鉁� 姝g‘锛氫娇鐢� # 鍒悕
+import { getUser } from '#/api/system/user';
+import DictTag from '#/components/dict-tag/dict-tag.vue';
+
+// 鉂� 閿欒锛氫娇鐢ㄧ浉瀵硅矾寰�
+import { getUser } from '../../../api/system/user';
+```
+
+## 缁勪欢鍛藉悕
+
+```typescript
+// 鉁� 姝g‘锛氱粍浠舵枃浠跺悕 PascalCase
+UserList.vue
+DictTag.vue
+TableAction.vue
+
+// 鉁� 姝g‘锛氬畾涔夌粍浠跺悕
+defineOptions({ name: 'UserList' });
+```
+
+## 鎺у埗鍙拌緭鍑�
+
+```typescript
+// 鉂� 绂佹锛歝onsole.log 鍦ㄧ敓浜т唬鐮佷腑
+console.log('debug info');
+
+// 鉁� 姝g‘锛氫娇鐢ㄦ棩蹇楀簱鎴栫Щ闄�
+// 鐢熶骇鐜绉婚櫎鎵�鏈夎皟璇曡緭鍑�
+```
\ No newline at end of file
diff --git a/.claude/rules/vue-component.md b/.claude/rules/vue-component.md
new file mode 100644
index 0000000..922b860
--- /dev/null
+++ b/.claude/rules/vue-component.md
@@ -0,0 +1,166 @@
+---
+paths:
+ - "**/*.vue"
+---
+
+# Vue 缁勪欢寮�鍙戣鍒�
+
+> 鍩轰簬 Vue Vben Admin 椤圭洰瑙勮寖銆�
+
+## 缁勪欢缁撴瀯
+
+### script setup
+
+```vue
+<script lang="ts" setup>
+// 1. 缁勪欢鍛藉悕锛堝繀椤伙級
+defineOptions({ name: 'ComponentName' });
+
+// 2. 绫诲瀷瀵煎叆
+import type { PropType } from './types';
+
+// 3. Props 瀹氫箟
+interface Props {
+ id: number;
+ name?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ name: '',
+});
+
+// 4. Emits 瀹氫箟
+interface Emits {
+ (e: 'update', value: string): void;
+ (e: 'delete', id: number): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 5. 鍝嶅簲寮忕姸鎬�
+const loading = ref(false);
+
+// 6. 璁$畻灞炴��
+const displayName = computed(() => props.name || '鏈懡鍚�');
+
+// 7. 鏂规硶
+function handleSubmit() {
+ emit('update', displayName.value);
+}
+</script>
+
+<template>
+ <div class="component-name">
+ <!-- 妯℃澘鍐呭 -->
+ </div>
+</template>
+
+<style lang="scss" scoped>
+.component-name {
+ /* 鏍峰紡 */
+}
+</style>
+```
+
+## Props 瑙勮寖
+
+```vue
+<script lang="ts" setup>
+// 鉁� 姝g‘锛氫娇鐢� withDefaults + defineProps
+interface Props {
+ userId: number;
+ userName?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ userName: '',
+});
+
+// 鉂� 閿欒锛氫娇鐢ㄨ繍琛屾椂澹版槑
+const props = defineProps({
+ userId: { type: Number, required: true },
+ userName: { type: String, default: '' },
+});
+</script>
+```
+
+## Emits 瑙勮寖
+
+```vue
+<script lang="ts" setup>
+// 鉁� 姝g‘锛氱被鍨嬪寲 Emits
+interface Emits {
+ (e: 'update', value: string): void;
+ (e: 'delete', id: number): void;
+ (e: 'change', data: { id: number; name: string }): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 鉂� 閿欒锛氳繍琛屾椂澹版槑
+const emit = defineEmits(['update', 'delete']);
+</script>
+```
+
+## 鏍峰紡瑙勮寖
+
+```vue
+<template>
+ <!-- 鉁� 姝g‘锛氫紭鍏堜娇鐢� Tailwind CSS -->
+ <div class="flex items-center p-4 bg-white rounded-lg">
+ <span class="text-base font-medium text-gray-900">鏍囬</span>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+/* 鉁� 姝g‘锛氱粍浠舵牱寮忎娇鐢� scoped */
+.component-name {
+ /* 鉁� 姝g‘锛氶鑹蹭娇鐢� CSS 鍙橀噺 */
+ background-color: var(--component-background);
+
+ /* 鉁� 姝g‘锛氳鐩栫粍浠跺簱鏍峰紡浣跨敤 :deep */
+ :deep(.ant-table) {
+ border-radius: 8px;
+ }
+}
+
+/* 鉂� 閿欒锛氱‖缂栫爜棰滆壊 */
+.bad-style {
+ background-color: #ffffff;
+}
+</style>
+```
+
+## 缁勪欢鍛藉悕
+
+| 绫诲瀷 | 鍛藉悕瑙勫垯 | 绀轰緥 |
+|-----|---------|-----|
+| 椤甸潰缁勪欢 | 涓庣洰褰曞悕涓�鑷� | `UserList.vue` 鈫� `name: 'UserList'` |
+| 琛ㄥ崟寮圭獥 | 鍔熻兘 + Form | `UserForm.vue` 鈫� `name: 'UserForm'` |
+| 璇︽儏寮圭獥 | 鍔熻兘 + Detail | `UserDetail.vue` 鈫� `name: 'UserDetail'` |
+| 閫夋嫨鍣� | 鍔熻兘 + Select | `UserSelect.vue` 鈫� `name: 'UserSelect'` |
+| 鍒楄〃缁勪欢 | 鍔熻兘 + List | `UserBomList.vue` 鈫� `name: 'UserBomList'` |
+
+## 瀵煎叆璺緞
+
+```vue
+<script lang="ts" setup>
+// 鉁� 姝g‘锛氫娇鐢� # 鍒悕
+import { getUser } from '#/api/system/user';
+import DictTag from '#/components/dict-tag/dict-tag.vue';
+import { useVbenForm } from '#/adapter/form';
+
+// 鉁� 姝g‘锛氱浉瀵硅矾寰勫鍏ュ悓鐩綍
+import Form from './modules/form.vue';
+import { useGridColumns } from './data';
+</script>
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **蹇呴』浣跨敤 `defineOptions` 瀹氫箟缁勪欢鍚�**
+2. **缁熶竴浣跨敤 `<script setup lang="ts">` 璇硶**
+3. **Props 浣跨敤 `withDefaults` + `defineProps<T>()`**
+4. **Emits 浣跨敤 `defineEmits<T>()`**
+5. **鏍峰紡浼樺厛浣跨敤 Tailwind CSS**
+6. **缁勪欢鏍峰紡蹇呴』 `scoped`**
\ No newline at end of file
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 5d7a2ad..3d8e4b9 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -9,7 +9,18 @@
"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\\\\', ''\\) })"
+ "PowerShell(cd \"D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\"; Get-ChildItem -Path \"src/router\" -Recurse -File | Select-Object -ExpandProperty FullName | ForEach-Object { $_.Replace\\('D:\\\\宸ヤ綔\\\\鑺鍏徃\\\\椤圭洰\\\\杩涢攢瀛榎\\\mom-pro2-before\\\\', ''\\) })",
+ "Bash(pnpm approve-builds *)",
+ "Bash(pnpm list *)",
+ "Bash(pnpm search *)",
+ "Bash(pnpm add *)",
+ "Bash(pnpm exec *)",
+ "Bash(pnpm stub *)",
+ "Bash(pnpm dev *)",
+ "Bash(Get-Content \"C:\\\\Users\\\\浜慭\\\AppData\\\\Local\\\\Temp\\\\claude\\\\D-----------mom-pro2-after-mom-pro2-before\\\\3ceea64d-d347-46b2-88a2-57009ccd0961\\\\tasks\\\\bak5v4put.output\")",
+ "Bash(pnpm rebuild *)",
+ "Bash(node lib/script/postinstall.js)",
+ "Bash(mvn compile *)"
]
}
}
diff --git a/.claude/skills/api-service-pattern.md b/.claude/skills/api-service-pattern.md
new file mode 100644
index 0000000..ddb07ce
--- /dev/null
+++ b/.claude/skills/api-service-pattern.md
@@ -0,0 +1,213 @@
+---
+name: api-service-pattern
+description: API 鎺ュ彛瀹氫箟妯″紡锛屽寘鎷被鍨嬪畾涔夈�佹帴鍙e嚱鏁般�佸垎椤垫煡璇€�佸懡鍚嶇┖闂磋鑼冦��
+origin: ECC
+---
+
+# API 鎺ュ彛瀹氫箟妯″紡
+
+椤圭洰 API 鎺ュ彛寮�鍙戣鑼冿紝浣跨敤 namespace 缁勭粐绫诲瀷鍜屽嚱鏁般��
+
+## When to Use
+
+- 鍒涘缓鏂扮殑 API 鎺ュ彛鏂囦欢
+- 瀹氫箟鎺ュ彛璇锋眰/鍝嶅簲绫诲瀷
+- 缂栧啓 CRUD 鎺ュ彛鍑芥暟
+- 澶勭悊鍒嗛〉鏌ヨ
+
+## 鏂囦欢缁撴瀯
+
+```
+src/api/
+鈹斺攢鈹� mes/
+ 鈹斺攢鈹� pro/
+ 鈹斺攢鈹� workorder/
+ 鈹溾攢鈹� index.ts # 宸ュ崟鎺ュ彛
+ 鈹斺攢鈹� bom/
+ 鈹斺攢鈹� index.ts # 宸ュ崟 BOM 鎺ュ彛
+```
+
+## 鎺ュ彛瀹氫箟妯℃澘
+
+```typescript
+import type { PageParam, PageResult } from '@vben/request';
+
+import { requestClient } from '#/api/request';
+
+export namespace WorkOrderApi {
+ /** 宸ュ崟淇℃伅 */
+ export interface WorkOrder {
+ id?: number; // 缂栧彿
+ code?: string; // 缂栫爜
+ name?: string; // 鍚嶇О
+ status?: number; // 鐘舵��
+ createTime?: number; // 鍒涘缓鏃堕棿
+ }
+
+ /** 鍒嗛〉鏌ヨ鍙傛暟 */
+ export interface PageParams extends PageParam {
+ code?: string;
+ name?: string;
+ status?: number;
+ }
+}
+
+/** 鏌ヨ宸ュ崟鍒嗛〉 */
+export function getWorkOrderPage(params: WorkOrderApi.PageParams) {
+ return requestClient.get<PageResult<WorkOrderApi.WorkOrder>>(
+ '/mes/pro/work-order/page',
+ { params },
+ );
+}
+
+/** 鏌ヨ宸ュ崟璇︽儏 */
+export function getWorkOrder(id: number) {
+ return requestClient.get<WorkOrderApi.WorkOrder>(
+ `/mes/pro/work-order/get?id=${id}`,
+ );
+}
+
+/** 鏂板宸ュ崟 */
+export function createWorkOrder(data: WorkOrderApi.WorkOrder) {
+ return requestClient.post<number>('/mes/pro/work-order/create', data);
+}
+
+/** 淇敼宸ュ崟 */
+export function updateWorkOrder(data: WorkOrderApi.WorkOrder) {
+ return requestClient.put('/mes/pro/work-order/update', data);
+}
+
+/** 鍒犻櫎宸ュ崟 */
+export function deleteWorkOrder(id: number) {
+ return requestClient.delete(`/mes/pro/work-order/delete?id=${id}`);
+}
+
+/** 瀵煎嚭宸ュ崟 */
+export function exportWorkOrder(params: any) {
+ return requestClient.download('/mes/pro/work-order/export-excel', { params });
+}
+```
+
+## 绫诲瀷瀹氫箟瑙勮寖
+
+### 瀹炰綋鎺ュ彛
+
+```typescript
+export namespace ItemApi {
+ /** 鐗╂枡淇℃伅 */
+ export interface Item {
+ id?: number; // 缂栧彿锛堝彲閫夛紝鏂板鏃舵棤锛�
+ code: string; // 缂栫爜锛堝繀濉級
+ name: string; // 鍚嶇О锛堝繀濉級
+ specification?: string; // 瑙勬牸鍨嬪彿
+ unitMeasureId?: number; // 鍗曚綅缂栧彿
+ unitMeasureName?: string; // 鍗曚綅鍚嶇О锛堝啑浣欏瓧娈碉級
+ status?: number; // 鐘舵��
+ remark?: string; // 澶囨敞
+ createTime?: number; // 鍒涘缓鏃堕棿
+ }
+}
+```
+
+### 鍒嗛〉鍙傛暟
+
+```typescript
+export namespace ItemApi {
+ /** 鍒嗛〉鏌ヨ鍙傛暟 */
+ export interface PageParams extends PageParam {
+ code?: string; // 缂栫爜锛堟ā绯婏級
+ name?: string; // 鍚嶇О锛堟ā绯婏級
+ status?: number; // 鐘舵��
+ categoryId?: number; // 鍒嗙被缂栧彿
+ createTime?: number[]; // 鍒涘缓鏃堕棿鑼冨洿
+ }
+}
+```
+
+### 鍒楄〃鎺ュ彛
+
+```typescript
+export namespace ProcessApi {
+ /** 宸ュ簭绮剧畝淇℃伅锛堜笅鎷夐�夋嫨鐢級 */
+ export interface ProcessSimple {
+ id: number;
+ code: string;
+ name: string;
+ }
+}
+
+/** 鏌ヨ宸ュ簭鍒楄〃 */
+export function getProcessList(params?: ProcessApi.ListParams) {
+ return requestClient.get<ProcessApi.Process[]>(
+ '/mes/pro/process/list',
+ { params },
+ );
+}
+
+/** 鏌ヨ宸ュ簭绮剧畝鍒楄〃 */
+export function getProcessSimpleList() {
+ return requestClient.get<ProcessApi.ProcessSimple[]>(
+ '/mes/pro/process/simple-list',
+ );
+}
+```
+
+## 璇锋眰鏂规硶鏄犲皠
+
+| 鎿嶄綔 | 鏂规硶 | URL 鏍煎紡 | 杩斿洖绫诲瀷 |
+|-----|------|---------|---------|
+| 鍒嗛〉鏌ヨ | GET | `/module/page` | `PageResult<T>` |
+| 鍒楄〃鏌ヨ | GET | `/module/list` | `T[]` |
+| 绮剧畝鍒楄〃 | GET | `/module/simple-list` | `SimpleT[]` |
+| 璇︽儏鏌ヨ | GET | `/module/get?id=` | `T` |
+| 鏂板 | POST | `/module/create` | `number` |
+| 淇敼 | PUT | `/module/update` | `void` |
+| 鍒犻櫎 | DELETE | `/module/delete?id=` | `void` |
+| 瀵煎嚭 | GET | `/module/export-excel` | `Blob` |
+
+## 瀛愭ā鍧楁帴鍙�
+
+瀛愭ā鍧楁帴鍙f斁鍦ㄧ埗妯″潡鐩綍涓嬶細
+
+```
+src/api/mes/pro/workorder/
+鈹溾攢鈹� index.ts # 宸ュ崟鎺ュ彛
+鈹斺攢鈹� bom/
+ 鈹斺攢鈹� index.ts # 宸ュ崟 BOM 鎺ュ彛
+```
+
+```typescript
+// bom/index.ts
+export namespace WorkOrderBomApi {
+ export interface WorkOrderBom {
+ id?: number;
+ workOrderId?: number; // 鍏宠仈鐖惰〃
+ itemId?: number;
+ itemName?: string;
+ quantity?: number;
+ }
+
+ export interface PageParams extends PageParam {
+ workOrderId?: number; // 蹇呴』鏈夌埗琛� ID
+ }
+}
+```
+
+## 瀵煎叆浣跨敤
+
+```typescript
+// 瀵煎叆绫诲瀷
+import type { WorkOrderApi } from '#/api/mes/pro/workorder';
+import type { WorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
+
+// 瀵煎叆鍑芥暟
+import { getWorkOrderPage, createWorkOrder } from '#/api/mes/pro/workorder';
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **浣跨敤 namespace 缁勭粐绫诲瀷**锛氶伩鍏嶇被鍨嬪懡鍚嶅啿绐�
+2. **瀹炰綋瀛楁鍏ㄩ儴鍙��**锛氶�傚簲鏂板/缂栬緫/鏌ヨ鍦烘櫙
+3. **鍒嗛〉鍙傛暟缁ф壙 PageParam**锛氬寘鍚� `pageNo`銆乣pageSize`
+4. **杩斿洖绫诲瀷鏄庣‘鎸囧畾**锛歚requestClient.get<T>`
+5. **璺緞鍙傛暟浣跨敤妯℃澘瀛楃涓�**锛歚/module/get?id=${id}`
\ No newline at end of file
diff --git a/.claude/skills/vben-table-form.md b/.claude/skills/vben-table-form.md
new file mode 100644
index 0000000..81ab6c2
--- /dev/null
+++ b/.claude/skills/vben-table-form.md
@@ -0,0 +1,421 @@
+---
+name: vben-table-form
+description: Vue Vben Admin 琛ㄦ牸鍜岃〃鍗曞紑鍙戞ā寮忥紝鍖呮嫭 useVbenVxeGrid銆乽seVbenModal銆乽seVbenForm 鐨勪娇鐢ㄣ��
+origin: ECC
+---
+
+# Vue Vben Admin 琛ㄦ牸琛ㄥ崟妯″紡
+
+椤圭洰鏍稿績缁勪欢鐨勪娇鐢ㄦ寚鍗楋細琛ㄦ牸锛坲seVbenVxeGrid锛夈�佽〃鍗曞脊绐楋紙useVbenModal锛夈�佽〃鍗曪紙useVbenForm锛夈��
+
+## When to Use
+
+- 寮�鍙戝垪琛ㄩ〉闈�
+- 寮�鍙戣〃鍗曞脊绐�
+- 閰嶇疆琛ㄦ牸鍒楀拰鎼滅储鏉′欢
+- 澶勭悊澧炲垹鏀规煡鎿嶄綔
+
+## 琛ㄦ牸缁勪欢 (useVbenVxeGrid)
+
+### 鍩虹鐢ㄦ硶
+
+```vue
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { UserApi } from '#/api/system/user';
+
+import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
+import { getUserPage } from '#/api/system/user';
+import { getDictOptions } from '@vben/hooks';
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: [
+ {
+ fieldName: 'username',
+ label: '鐢ㄦ埛鍚�',
+ component: 'Input',
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions('common_status', 'number'),
+ },
+ },
+ ],
+ },
+ gridOptions: {
+ columns: [
+ { type: 'checkbox', width: 50 },
+ { field: 'username', title: '鐢ㄦ埛鍚�', width: 120 },
+ { field: 'nickname', title: '鏄电О', minWidth: 150 },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: 'common_status' },
+ },
+ },
+ { field: 'createTime', title: '鍒涘缓鏃堕棿', width: 180, formatter: 'formatDateTime' },
+ { title: '鎿嶄綔', width: 180, fixed: 'right', slots: { default: 'actions' } },
+ ],
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getUserPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<UserApi.User>,
+});
+
+// 鍒锋柊琛ㄦ牸
+function handleRefresh() {
+ gridApi.query();
+}
+</script>
+
+<template>
+ <Grid table-title="鐢ㄦ埛鍒楄〃">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鏂板',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '缂栬緫',
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ onClick: handleEdit.bind(null, row),
+ },
+ {
+ label: '鍒犻櫎',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ popConfirm: {
+ title: `纭鍒犻櫎銆�${row.username}銆戝悧锛焋,
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+</template>
+```
+
+### 琛ㄦ牸鍒楁覆鏌撳櫒
+
+| 娓叉煋鍣� | 鐢ㄩ�� | 绀轰緥 |
+|-------|------|------|
+| `CellDict` | 瀛楀吀鏍囩 | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` |
+| `CellImage` | 鍗曞浘 | `cellRender: { name: 'CellImage' }` |
+| `CellImages` | 澶氬浘 | `cellRender: { name: 'CellImages' }` |
+| `CellLink` | 閾炬帴 | `cellRender: { name: 'CellLink', props: { text: '鏌ョ湅' } }` |
+| `CellSwitch` | 寮�鍏� | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }` |
+
+### 琛ㄦ牸浜嬩欢
+
+```typescript
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridEvents: {
+ checkboxAll: ({ records }) => {
+ checkedIds.value = records.map(item => item.id!);
+ },
+ checkboxChange: ({ records }) => {
+ checkedIds.value = records.map(item => item.id!);
+ },
+ },
+});
+```
+
+## 琛ㄥ崟寮圭獥 (useVbenModal)
+
+### 鍒楄〃椤靛畾涔�
+
+```vue
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+import Form from './modules/form.vue';
+
+const [FormModal, formModalApi] = useVbenModal({
+ connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+// 鏂板
+function handleCreate() {
+ formModalApi.setData({ formType: 'create' }).open();
+}
+
+// 缂栬緫
+function handleEdit(row: UserApi.User) {
+ formModalApi.setData({ formType: 'update', id: row.id }).open();
+}
+</script>
+
+<template>
+ <FormModal @success="handleRefresh" />
+</template>
+```
+
+### 琛ㄥ崟缁勪欢
+
+```vue
+<script lang="ts" setup>
+import type { UserApi } from '#/api/system/user';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { useVbenForm } from '#/adapter/form';
+import { createUser, updateUser, getUser } from '#/api/system/user';
+import { $t } from '#/locales';
+
+const emit = defineEmits(['success']);
+const formData = ref<UserApi.User>();
+const formType = ref<'create' | 'update'>('create');
+
+const getTitle = computed(() => {
+ return formType.value === 'update'
+ ? $t('ui.actionTitle.edit', ['鐢ㄦ埛'])
+ : $t('ui.actionTitle.create', ['鐢ㄦ埛']);
+});
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: [
+ {
+ fieldName: 'username',
+ label: '鐢ㄦ埛鍚�',
+ component: 'Input',
+ rules: 'required',
+ },
+ {
+ fieldName: 'nickname',
+ label: '鏄电О',
+ component: 'Input',
+ rules: 'required',
+ },
+ {
+ fieldName: 'email',
+ label: '閭',
+ component: 'Input',
+ rules: 'email',
+ },
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions('common_status', 'number'),
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ },
+ ],
+ 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();
+
+ try {
+ if (formData.value?.id) {
+ await updateUser({ ...data, id: formData.value.id });
+ } else {
+ await createUser(data);
+ }
+ await modalApi.close();
+ emit('success');
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ return;
+ }
+
+ const data = modalApi.getData<{ formType: string; id?: number }>();
+ formType.value = data.formType;
+
+ if (data.id) {
+ modalApi.lock();
+ try {
+ formData.value = await getUser(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-2/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
+```
+
+## 琛ㄥ崟 Schema 閰嶇疆
+
+### 甯哥敤缁勪欢
+
+```typescript
+const schema: VbenFormSchema[] = [
+ // 杈撳叆妗�
+ {
+ fieldName: 'name',
+ label: '鍚嶇О',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ悕绉�',
+ },
+ rules: 'required',
+ },
+
+ // 鏁板瓧杈撳叆
+ {
+ fieldName: 'quantity',
+ label: '鏁伴噺',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ precision: 2,
+ },
+ },
+
+ // 涓嬫媺閫夋嫨
+ {
+ fieldName: 'status',
+ label: '鐘舵��',
+ component: 'Select',
+ componentProps: {
+ options: getDictOptions('common_status', 'number'),
+ },
+ },
+
+ // 鏃ユ湡閫夋嫨
+ {
+ fieldName: 'date',
+ label: '鏃ユ湡',
+ component: 'DatePicker',
+ componentProps: {
+ class: '!w-full',
+ format: 'YYYY-MM-DD',
+ valueFormat: 'x',
+ },
+ },
+
+ // 闅愯棌瀛楁
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: {
+ triggerFields: [''],
+ show: () => false,
+ },
+ },
+
+ // 鏉′欢鏄剧ず
+ {
+ fieldName: 'clientId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ dependencies: {
+ triggerFields: ['orderSourceType'],
+ show: (values) => values.orderSourceType === 'ORDER',
+ },
+ },
+];
+```
+
+## TableAction 鎿嶄綔鎸夐挳
+
+```vue
+<TableAction
+ :actions="[
+ {
+ label: '缂栬緫',
+ type: 'link',
+ icon: ACTION_ICON.EDIT,
+ auth: ['system:user:update'],
+ onClick: handleEdit,
+ },
+ {
+ label: '鍒犻櫎',
+ type: 'link',
+ danger: true,
+ icon: ACTION_ICON.DELETE,
+ auth: ['system:user:delete'],
+ popConfirm: {
+ title: '纭鍒犻櫎鍚楋紵',
+ confirm: handleDelete,
+ },
+ },
+ ]"
+ :drop-down-actions="[
+ {
+ label: '鍒嗛厤瑙掕壊',
+ auth: ['system:user:assign-role'],
+ onClick: handleAssignRole,
+ },
+ ]"
+/>
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **琛ㄦ牸蹇呴』鏈� `keyField`**锛歚rowConfig: { keyField: 'id' }`
+2. **寮圭獥蹇呴』 `destroyOnClose: true`**锛氶槻姝㈡暟鎹畫鐣�
+3. **琛ㄥ崟楠岃瘉浣跨敤 `rules` 灞炴��**锛氭敮鎸� `required`銆乣email`銆乣selectRequired` 绛�
+4. **鏉冮檺鎺у埗浣跨敤 `auth` 灞炴��**锛歚auth: ['system:user:update']`
\ No newline at end of file
diff --git a/.claude/skills/vue-component-patterns.md b/.claude/skills/vue-component-patterns.md
new file mode 100644
index 0000000..d110756
--- /dev/null
+++ b/.claude/skills/vue-component-patterns.md
@@ -0,0 +1,170 @@
+---
+name: vue-component-patterns
+description: Vue 3 缁勪欢寮�鍙戞ā寮忥紝鍖呮嫭 Composition API銆丳rops/Emits 瀹氫箟銆佺粍浠跺懡鍚嶈鑼冦�傞�傜敤浜庡紑鍙� Vue 缁勪欢鏃躲��
+origin: ECC
+---
+
+# Vue 3 缁勪欢寮�鍙戞ā寮�
+
+鍩轰簬 Vue Vben Admin 椤圭洰鐨� Vue 3 缁勪欢寮�鍙戣鑼冦��
+
+## When to Use
+
+- 寮�鍙戞柊鐨� Vue 缁勪欢
+- 瀹氫箟缁勪欢 Props 鍜� Emits
+- 浣跨敤 Composition API
+- 缁勪欢鐘舵�佺鐞嗗拰鐢熷懡鍛ㄦ湡
+
+## How It Works
+
+椤圭洰浣跨敤 Vue 3.5 + TypeScript + `<script setup>` 璇硶銆�
+
+## 缁勪欢瀹氫箟瑙勮寖
+
+### 鍩虹缁撴瀯
+
+```vue
+<script lang="ts" setup>
+import type { PropsType } from './types';
+
+import { ref, computed } from 'vue';
+
+// 1. 缁勪欢鍛藉悕锛堝繀椤伙級
+defineOptions({ name: 'ComponentName' });
+
+// 2. Props 瀹氫箟
+interface Props {
+ userId: number;
+ userName?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), {
+ userName: '',
+});
+
+// 3. Emits 瀹氫箟
+interface Emits {
+ (e: 'update', value: string): void;
+ (e: 'delete', id: number): void;
+}
+
+const emit = defineEmits<Emits>();
+
+// 4. 鍝嶅簲寮忕姸鎬�
+const loading = ref(false);
+
+// 5. 璁$畻灞炴��
+const displayName = computed(() => props.userName || '鏈懡鍚�');
+
+// 6. 鏂规硶
+function handleSubmit() {
+ emit('update', displayName.value);
+}
+</script>
+
+<template>
+ <div class="component-name">
+ <!-- 妯℃澘鍐呭 -->
+ </div>
+</template>
+
+<style lang="scss" scoped>
+.component-name {
+ /* 鏍峰紡 */
+}
+</style>
+```
+
+### TypeScript 瑙勮寖
+
+```typescript
+// 鉁� 姝g‘锛氫娇鐢� import type 瀵煎叆绫诲瀷
+import type { UserInfo } from '#/api/system/user';
+
+// 鉁� 姝g‘锛氬畾涔夊叿浣撴帴鍙�
+interface Props {
+ userId: number;
+ userName?: string;
+}
+
+// 鉂� 閿欒锛氫娇鐢� any
+const data: any = {};
+
+// 鉁� 姝g‘锛氫娇鐢� unknown + 绫诲瀷瀹堝崼
+function getErrorMessage(error: unknown): string {
+ if (error instanceof Error) {
+ return error.message;
+ }
+ return '鏈煡閿欒';
+}
+```
+
+## 椤甸潰鏂囦欢缁撴瀯
+
+鏍囧噯 CRUD 椤甸潰鐨勬枃浠剁粨鏋勶細
+
+```
+views/
+鈹斺攢鈹� system/
+ 鈹斺攢鈹� user/
+ 鈹溾攢鈹� index.vue # 鍒楄〃椤�
+ 鈹溾攢鈹� data.ts # 鏁版嵁瀹氫箟锛堣〃鏍煎垪銆佽〃鍗� Schema锛�
+ 鈹斺攢鈹� modules/
+ 鈹溾攢鈹� form.vue # 琛ㄥ崟寮圭獥
+ 鈹斺攢鈹� detail.vue # 璇︽儏寮圭獥锛堝彲閫夛級
+```
+
+### 瀵煎叆璺緞灞傜骇
+
+| 鏂囦欢浣嶇疆 | 瀵煎叆灞傜骇 |
+|---------|---------|
+| `index.vue` | 4 灞� `../..` |
+| `modules/form.vue` | 5 灞� `../../..` |
+
+```typescript
+// index.vue 涓殑瀵煎叆
+import { useFormSchema } from '../data';
+
+// modules/form.vue 涓殑瀵煎叆
+import { useFormSchema } from '../../data';
+```
+
+## 缁勪欢鍛藉悕瑙勮寖
+
+| 绫诲瀷 | 鍛藉悕瑙勫垯 | 绀轰緥 |
+|-----|---------|-----|
+| 椤甸潰缁勪欢 | 涓庣洰褰曞悕涓�鑷� | `UserList.vue` |
+| 涓氬姟缁勪欢 | 鍔熻兘鍚� + 绫诲瀷 | `UserSelect.vue` |
+| 鍩虹缁勪欢 | 鍔熻兘鍚� | `Button.vue` |
+| 寮圭獥缁勪欢 | 鍔熻兘 + Form/Detail | `UserForm.vue` |
+
+## 鏍峰紡瑙勮寖
+
+```vue
+<template>
+ <!-- 浼樺厛浣跨敤 Tailwind CSS -->
+ <div class="flex items-center p-4 bg-white rounded-lg">
+ <span class="text-base font-medium text-gray-900">鏍囬</span>
+ </div>
+</template>
+
+<style lang="scss" scoped>
+/* 缁勪欢鏍峰紡浣跨敤 scoped */
+.component-name {
+ /* 棰滆壊浣跨敤 CSS 鍙橀噺 */
+ background-color: var(--component-background);
+
+ /* 瑕嗙洊缁勪欢搴撴牱寮忎娇鐢� :deep */
+ :deep(.ant-table) {
+ border-radius: 8px;
+ }
+}
+</style>
+```
+
+## 娉ㄦ剰浜嬮」
+
+1. **蹇呴』浣跨敤 `defineOptions` 瀹氫箟缁勪欢鍚�**
+2. **绂佹浣跨敤 `any` 绫诲瀷**
+3. **Props 浣跨敤 `withDefaults` + `defineProps<T>()`**
+4. **璺緞鍒悕 `#/*` 鎸囧悜 `./src/*`**
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
index 4954570..9995cba 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -82,9 +82,9 @@
## 鍙敤鎶�鑳�
-- `vue3-development` - Vue 3 寮�鍙戣鑼�
-- `antd-vue-components` - Ant Design Vue 缁勪欢妯″紡
-- `frontend-code-review` - 鍓嶇浠g爜瀹℃煡鎸囧崡
+- `vue-component-patterns` - Vue 3 缁勪欢寮�鍙戞ā寮忥紙Composition API銆丳rops/Emits锛�
+- `vben-table-form` - 琛ㄦ牸琛ㄥ崟缁勪欢浣跨敤锛坲seVbenVxeGrid銆乽seVbenModal銆乽seVbenForm锛�
+- `api-service-pattern` - API 鎺ュ彛瀹氫箟妯″紡锛坣amespace銆佺被鍨嬪畾涔夈�丆RUD 鍑芥暟锛�
## 娉ㄦ剰浜嬮」
diff --git a/internal/node-utils/package.json b/internal/node-utils/package.json
index 35892a1..618fa4a 100644
--- a/internal/node-utils/package.json
+++ b/internal/node-utils/package.json
@@ -38,5 +38,8 @@
"ora": "catalog:",
"pkg-types": "catalog:",
"rimraf": "catalog:"
+ },
+ "devDependencies": {
+ "unrun": "^0.3.1"
}
}
diff --git a/package.json b/package.json
index 109e0d2..4dbcf6f 100644
--- a/package.json
+++ b/package.json
@@ -84,5 +84,6 @@
"tsx": "^4.22.4",
"typescript": "catalog:",
"vite": "catalog:"
- }
-}
\ No newline at end of file
+ },
+ "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38"
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bc9f97f..e317c5f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -695,7 +695,7 @@
version: 2.7.0
tsdown:
specifier: ^0.22.3
- version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)
+ version: 0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13))
tsx:
specifier: ^4.22.4
version: 4.22.4
@@ -871,6 +871,10 @@
rimraf:
specifier: 'catalog:'
version: 6.1.3
+ devDependencies:
+ unrun:
+ specifier: ^0.3.1
+ version: 0.3.1(synckit@0.11.13)
internal/tailwind-config:
dependencies:
@@ -7315,9 +7319,6 @@
lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
- lodash.sortby@4.7.0:
- resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
-
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
@@ -8635,10 +8636,9 @@
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
- source-map@0.8.0-beta.0:
- resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==}
- engines: {node: '>= 8'}
- deprecated: The work that was done in this beta branch won't be included in future versions
+ source-map@0.8.0:
+ resolution: {integrity: sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==}
+ engines: {node: '>= 12'}
spawndamnit@3.0.1:
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
@@ -9026,9 +9026,6 @@
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
- tr46@1.0.1:
- resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==}
-
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -9304,6 +9301,16 @@
unplugin@3.0.0:
resolution: {integrity: sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==}
engines: {node: ^20.19.0 || >=22.12.0}
+
+ unrun@0.3.1:
+ resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==}
+ engines: {node: ^22.13.0 || >=24.0.0}
+ hasBin: true
+ peerDependencies:
+ synckit: ^0.11.11
+ peerDependenciesMeta:
+ synckit:
+ optional: true
unstorage@1.17.5:
resolution: {integrity: sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==}
@@ -9633,9 +9640,6 @@
webidl-conversions@3.0.0:
resolution: {integrity: sha512-GOK494qXZl0XBbr/YPRW2BL6oLfe+d0KDSWndKH7NdxJwJ0fCdhQG46Q2D7BmIS6cN/FJgtju8srRt/jlG9abA==}
- webidl-conversions@4.0.2:
- resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==}
-
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
@@ -9654,9 +9658,6 @@
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
-
- whatwg-url@7.0.0:
- resolution: {integrity: sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==}
which-boxed-primitive@1.1.1:
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
@@ -15596,8 +15597,6 @@
lodash.snakecase@4.1.1: {}
- lodash.sortby@4.7.0: {}
-
lodash.startcase@4.4.0: {}
lodash.truncate@4.4.2: {}
@@ -17085,9 +17084,7 @@
source-map@0.7.6: {}
- source-map@0.8.0-beta.0:
- dependencies:
- whatwg-url: 7.0.0
+ source-map@0.8.0: {}
spawndamnit@3.0.1:
dependencies:
@@ -17551,10 +17548,6 @@
tr46@0.0.3: {}
- tr46@1.0.1:
- dependencies:
- punycode: 2.3.1
-
tree-kill@1.2.2: {}
trim-newlines@5.0.0: {}
@@ -17576,7 +17569,7 @@
minimist: 1.2.8
strip-bom: 3.0.0
- tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3):
+ tsdown@0.22.3(tsx@4.22.4)(typescript@6.0.3)(unrun@0.3.1(synckit@0.11.13)):
dependencies:
ansis: 4.3.1
cac: 7.0.0
@@ -17596,6 +17589,7 @@
optionalDependencies:
tsx: 4.22.4
typescript: 6.0.3
+ unrun: 0.3.1(synckit@0.11.13)
transitivePeerDependencies:
- '@ts-macro/tsc'
- '@typescript/native-preview'
@@ -17829,6 +17823,12 @@
'@jridgewell/remapping': 2.3.5
picomatch: 4.0.4
webpack-virtual-modules: 0.6.2
+
+ unrun@0.3.1(synckit@0.11.13):
+ dependencies:
+ rolldown: 1.1.2
+ optionalDependencies:
+ synckit: 0.11.13
unstorage@1.17.5(db0@0.3.4)(ioredis@5.11.1):
dependencies:
@@ -18148,8 +18148,6 @@
webidl-conversions@3.0.0: {}
- webidl-conversions@4.0.2: {}
-
webpack-virtual-modules@0.6.2: {}
webrtc-adapter@9.0.5:
@@ -18166,12 +18164,6 @@
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.0
-
- whatwg-url@7.0.0:
- dependencies:
- lodash.sortby: 4.7.0
- tr46: 1.0.1
- webidl-conversions: 4.0.2
which-boxed-primitive@1.1.1:
dependencies:
@@ -18254,7 +18246,7 @@
glob: 11.0.1
pretty-bytes: 5.6.0
rollup: 4.62.2
- source-map: 0.8.0-beta.0
+ source-map: 0.8.0
stringify-object: 3.3.0
strip-comments: 2.0.1
tempy: 0.6.0
diff --git a/src/api/mes/pro/feedback/index.ts b/src/api/mes/pro/feedback/index.ts
index b098949..78eecc3 100644
--- a/src/api/mes/pro/feedback/index.ts
+++ b/src/api/mes/pro/feedback/index.ts
@@ -114,6 +114,61 @@
return requestClient.put<boolean>(`/mes/pro/feedback/approve?id=${id}`);
}
+/** 蹇�熸姤宸ヨ姹� */
+export interface QuickCreateReq {
+ taskId: number;
+ feedbackQuantity: number;
+ qualifiedQuantity?: number;
+ unqualifiedQuantity?: number;
+ remark?: string;
+}
+
+/** 蹇�熸姤宸ュ搷搴� */
+export interface QuickCreateResp {
+ feedbackId?: number;
+ taskStatus?: number;
+ workOrderStatus?: number;
+}
+
+/** 鎵归噺鎶ュ伐璇锋眰 */
+export interface BatchCreateReq {
+ taskIds: number[];
+ feedbackQuantity: number;
+ qualifiedQuantity?: number;
+ unqualifiedQuantity?: number;
+ remark?: string;
+}
+
+/** 鎵归噺鎶ュ伐鍝嶅簲 */
+export interface BatchCreateResp {
+ successCount?: number;
+ failCount?: number;
+ results?: BatchResult[];
+}
+
+export interface BatchResult {
+ taskId?: number;
+ success?: boolean;
+ feedbackId?: number;
+ errorMsg?: string;
+}
+
+/** 蹇�熸姤宸ワ紙鍒涘缓骞舵彁浜わ級 */
+export function quickCreateFeedback(data: QuickCreateReq) {
+ return requestClient.post<QuickCreateResp>(
+ '/mes/pro/feedback/quick-create',
+ data,
+ );
+}
+
+/** 鎵归噺鎶ュ伐 */
+export function batchCreateFeedback(data: BatchCreateReq) {
+ return requestClient.post<BatchCreateResp>(
+ '/mes/pro/feedback/batch-create',
+ data,
+ );
+}
+
/** 鎸変换鍔℃煡璇㈡姤宸ュ巻鍙� */
export function getFeedbackListByTask(taskId: number) {
return requestClient.get<MesProFeedbackApi.Feedback[]>(
diff --git a/src/api/mes/pro/task/index.ts b/src/api/mes/pro/task/index.ts
index 2dba85e..f710da5 100644
--- a/src/api/mes/pro/task/index.ts
+++ b/src/api/mes/pro/task/index.ts
@@ -114,6 +114,48 @@
return requestClient.get<any[]>('/mes/pro/task/gantt-list', { params });
}
+/** 涓�閿帓浜ч瑙堣姹� */
+export interface AutoGenerateReq {
+ workOrderId: number;
+ routeId?: number;
+ autoAssignWorkstation?: boolean;
+ autoCalculateTime?: boolean;
+}
+
+/** 涓�閿帓浜ч瑙堝搷搴� */
+export interface AutoGeneratePreview {
+ workOrderId?: number;
+ routeId?: number;
+ routeName?: string;
+ taskList?: TaskPreview[];
+}
+
+export interface TaskPreview {
+ processId?: number;
+ processName?: string;
+ sort?: number;
+ suggestWorkstationId?: number;
+ suggestWorkstationName?: string;
+ quantity?: number;
+ keyFlag?: boolean;
+}
+
+/** 涓�閿帓浜ч瑙� */
+export function previewAutoGenerate(data: AutoGenerateReq) {
+ return requestClient.post<AutoGeneratePreview>(
+ '/mes/pro/task/auto-generate/preview',
+ data,
+ );
+}
+
+/** 涓�閿帓浜ф墽琛� */
+export function autoGenerateTasks(data: AutoGenerateReq) {
+ return requestClient.post<{ taskIds: number[] }>(
+ '/mes/pro/task/auto-generate',
+ data,
+ );
+}
+
/** 鏌ヨ寰呮姤宸ュ彴璐︼紙鍒嗛〉锛� */
export function getPendingFeedbackPage(
params: MesProTaskApi.PendingFeedbackPageParams,
diff --git a/src/api/mes/pro/workorder/index.ts b/src/api/mes/pro/workorder/index.ts
index 97c689e..15a7635 100644
--- a/src/api/mes/pro/workorder/index.ts
+++ b/src/api/mes/pro/workorder/index.ts
@@ -99,3 +99,50 @@
export function confirmWorkOrder(id: number) {
return requestClient.put(`/mes/pro/work-order/confirm?id=${id}`);
}
+
+/** 宸ュ崟杩涘害璇︽儏 */
+export interface WorkOrderProgress {
+ workOrderId?: number;
+ workOrderCode?: string;
+ quantity?: number;
+ quantityScheduled?: number;
+ quantityProduced?: number;
+ progressPercent?: number;
+ processList?: ProcessProgress[];
+}
+
+export interface ProcessProgress {
+ processId?: number;
+ processCode?: string;
+ processName?: string;
+ keyFlag?: boolean;
+ plannedQuantity?: number;
+ producedQuantity?: number;
+ progressPercent?: number;
+ status?: number;
+ taskList?: TaskInfo[];
+}
+
+export interface TaskInfo {
+ taskId?: number;
+ taskCode?: string;
+ workstationName?: string;
+ quantity?: number;
+ producedQuantity?: number;
+ progressPercent?: number;
+ status?: number;
+}
+
+/** 鑾峰彇宸ュ崟杩涘害璇︽儏 */
+export function getWorkOrderProgress(id: number) {
+ return requestClient.get<WorkOrderProgress>(
+ `/mes/pro/work-order/progress/${id}`,
+ );
+}
+
+/** 妫�鏌ュ苟鑷姩瀹屾垚宸ュ崟 */
+export function checkAndAutoFinishWorkOrder(id: number) {
+ return requestClient.post<boolean>(
+ `/mes/pro/work-order/check-auto-finish/${id}`,
+ );
+}
diff --git a/src/api/mes/pro/workorder/process/index.ts b/src/api/mes/pro/workorder/process/index.ts
new file mode 100644
index 0000000..6833d3f
--- /dev/null
+++ b/src/api/mes/pro/workorder/process/index.ts
@@ -0,0 +1,69 @@
+import type { MesProWorkOrderProcessApi } from './index';
+
+import { requestClient } from '#/api/request';
+
+export namespace MesProWorkOrderProcessApi {
+ /** MES 鐢熶骇宸ュ崟宸ュ簭 */
+ export interface WorkOrderProcess {
+ id?: number; // 缂栧彿
+ workOrderId?: number; // 鐢熶骇宸ュ崟缂栧彿
+ processId?: number; // 宸ュ簭缂栧彿
+ processCode?: string; // 宸ュ簭缂栫爜
+ processName?: string; // 宸ュ簭鍚嶇О
+ sort?: number; // 搴忓彿
+ nextProcessId?: number; // 涓嬩竴閬撳伐搴忕紪鍙�
+ nextProcessName?: string; // 涓嬩竴閬撳伐搴忓悕绉�
+ linkType?: number; // 涓庝笅涓�閬撳伐搴忓叧绯�
+ prepareTime?: number; // 鍑嗗鏃堕棿锛堝垎閽燂級
+ waitTime?: number; // 绛夊緟鏃堕棿锛堝垎閽燂級
+ colorCode?: string; // 鐢樼壒鍥炬樉绀洪鑹�
+ keyFlag?: boolean; // 鏄惁鍏抽敭宸ュ簭
+ checkFlag?: boolean; // 鏄惁璐ㄦ宸ュ簭
+ backflushFlag?: boolean; // 鏄惁鍊掑啿
+ outputItemId?: number; // 浜у嚭浜у搧鐗╂枡缂栧彿
+ outputItemCode?: string; // 浜у嚭浜у搧鐗╂枡缂栫爜
+ outputItemName?: string; // 浜у嚭浜у搧鐗╂枡鍚嶇О
+ remark?: string; // 澶囨敞
+ }
+}
+
+/** 鏌ヨ宸ュ崟宸ュ簭鍒楄〃 */
+export function getWorkOrderProcessListByWorkOrderId(workOrderId: number) {
+ return requestClient.get<MesProWorkOrderProcessApi.WorkOrderProcess[]>(
+ `/mes/pro/work-order-process/list-by-work-order-id?workOrderId=${workOrderId}`,
+ );
+}
+
+/** 鏍规嵁浜у搧鑾峰彇宸ヨ壓璺嚎宸ュ簭鍒楄〃锛堢敤浜庡伐鍗曢濉級 */
+export function getProcessListByProductId(productId: number) {
+ return requestClient.get<MesProWorkOrderProcessApi.WorkOrderProcess[]>(
+ `/mes/pro/work-order-process/list-by-product-id?productId=${productId}`,
+ );
+}
+
+/** 鎵归噺淇濆瓨宸ュ崟宸ュ簭锛堟柊澧�+鏇存柊+鍒犻櫎锛� */
+export function batchSaveWorkOrderProcess(data: MesProWorkOrderProcessApi.WorkOrderProcess[]) {
+ return requestClient.post('/mes/pro/work-order-process/batch-save', data);
+}
+
+/** 鏌ヨ宸ュ崟宸ュ簭璇︽儏 */
+export function getWorkOrderProcess(id: number) {
+ return requestClient.get<MesProWorkOrderProcessApi.WorkOrderProcess>(
+ `/mes/pro/work-order-process/get?id=${id}`,
+ );
+}
+
+/** 鏂板宸ュ崟宸ュ簭 */
+export function createWorkOrderProcess(data: MesProWorkOrderProcessApi.WorkOrderProcess) {
+ return requestClient.post<number>('/mes/pro/work-order-process/create', data);
+}
+
+/** 淇敼宸ュ崟宸ュ簭 */
+export function updateWorkOrderProcess(data: MesProWorkOrderProcessApi.WorkOrderProcess) {
+ return requestClient.put('/mes/pro/work-order-process/update', data);
+}
+
+/** 鍒犻櫎宸ュ崟宸ュ簭 */
+export function deleteWorkOrderProcess(id: number) {
+ return requestClient.delete(`/mes/pro/work-order-process/delete?id=${id}`);
+}
\ No newline at end of file
diff --git a/src/api/mes/wm/materialstock/index.ts b/src/api/mes/wm/materialstock/index.ts
index 97f95b1..edbd6fe 100644
--- a/src/api/mes/wm/materialstock/index.ts
+++ b/src/api/mes/wm/materialstock/index.ts
@@ -106,6 +106,21 @@
return requestClient.post<number>('/mes/wm/material-stock/adjust-out', data);
}
+/** 搴撳瓨绉诲簱 */
+export interface MaterialStockMoveParams {
+ materialStockId: number; // 搴撳瓨璁板綍ID
+ toWarehouseId: number; // 鐩爣浠撳簱ID
+ toLocationId: number; // 鐩爣搴撳尯ID
+ toAreaId: number; // 鐩爣搴撲綅ID
+ quantity: number; // 绉诲簱鏁伴噺
+ remark?: string; // 澶囨敞
+}
+
+/** 搴撳瓨绉诲簱 */
+export function moveMaterialStock(data: MaterialStockMoveParams) {
+ return requestClient.post<boolean>('/mes/wm/material-stock/move', data);
+}
+
/** 搴撳瓨璋冩暣锛堟鏁�=鍏ュ簱锛岃礋鏁�=鍑哄簱锛�- 鏃ф帴鍙o紝淇濈暀鍏煎 */
export interface MaterialStockAdjustParams {
itemId: number; // 鐗╂枡ID
diff --git a/src/components/file-preview/file-preview-modal.vue b/src/components/file-preview/file-preview-modal.vue
index e6732e0..118d4d9 100644
--- a/src/components/file-preview/file-preview-modal.vue
+++ b/src/components/file-preview/file-preview-modal.vue
@@ -15,9 +15,7 @@
const VueOfficePptx = defineAsyncComponent(() => import('@vue-office/pptx'));
// @vue-office/excel 闇�瑕侀澶栧鍏� CSS
-// @vue-office/excel 闇�瑕侀澶栧鍏� CSS
-// @vue-office/excel 闇�瑕侀澶栧鍏� CSS
-import '@vue-office/excel-css';
+import '@vue-office/excel/lib/v3/style.css';
const props = withDefaults(
defineProps<{
diff --git a/src/views/mes/pro/feedback/index.vue b/src/views/mes/pro/feedback/index.vue
index b4457b7..239da4a 100644
--- a/src/views/mes/pro/feedback/index.vue
+++ b/src/views/mes/pro/feedback/index.vue
@@ -19,12 +19,24 @@
import { useGridColumns, useGridFormSchema } from './data';
import Form from './modules/form.vue';
+import QuickFeedbackForm from '../workorder-center/modules/quick-feedback.vue';
+import BatchFeedbackForm from './modules/batch-feedback.vue';
const userStore = useUserStore();
const currentUserId = userStore.userInfo?.id; // 褰撳墠鐧诲綍鐢ㄦ埛 ID锛岀敤浜庡鎵逛汉鏉冮檺鍒ゆ柇
const [FormModal, formModalApi] = useVbenModal({
connectedComponent: Form,
+ destroyOnClose: true,
+});
+
+const [QuickFeedbackModal, quickFeedbackModalApi] = useVbenModal({
+ connectedComponent: QuickFeedbackForm,
+ destroyOnClose: true,
+});
+
+const [BatchFeedbackModal, batchFeedbackModalApi] = useVbenModal({
+ connectedComponent: BatchFeedbackForm,
destroyOnClose: true,
});
@@ -79,6 +91,16 @@
downloadFileFromBlobPart({ fileName: '鐢熶骇鎶ュ伐.xls', source: data });
}
+/** 蹇�熸姤宸� */
+function handleQuickCreate() {
+ quickFeedbackModalApi.setData({}).open();
+}
+
+/** 鎵归噺鎶ュ伐 */
+function handleBatchCreate() {
+ batchFeedbackModalApi.setData({}).open();
+}
+
const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { schema: useGridFormSchema() },
gridOptions: {
@@ -103,7 +125,10 @@
</script>
<template>
- <Page auto-content-height><FormModal @success="handleRefresh" />
+ <Page auto-content-height>
+ <FormModal @success="handleRefresh" />
+ <QuickFeedbackModal @success="handleRefresh" />
+ <BatchFeedbackModal @success="handleRefresh" />
<Grid table-title="鐢熶骇鎶ュ伐鍒楄〃">
<template #toolbar-tools>
<TableAction
@@ -116,6 +141,19 @@
onClick: handleCreate,
},
{
+ label: '蹇�熸姤宸�',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-feedback:create'],
+ onClick: handleQuickCreate,
+ },
+ {
+ label: '鎵归噺鎶ュ伐',
+ type: 'primary',
+ auth: ['mes:pro-feedback:create'],
+ onClick: handleBatchCreate,
+ },
+ {
label: $t('ui.actionTitle.export'),
type: 'primary',
icon: ACTION_ICON.DOWNLOAD,
diff --git a/src/views/mes/pro/feedback/modules/batch-feedback.vue b/src/views/mes/pro/feedback/modules/batch-feedback.vue
new file mode 100644
index 0000000..e8e08b2
--- /dev/null
+++ b/src/views/mes/pro/feedback/modules/batch-feedback.vue
@@ -0,0 +1,147 @@
+<script lang="ts" setup>
+import type { MesProTaskApi } from '#/api/mes/pro/task';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { Button, message, Tag } from 'ant-design-vue';
+
+import { batchCreateFeedback } from '#/api/mes/pro/feedback';
+import ProTaskSelectDialog from '#/views/mes/pro/task/components/select-dialog.vue';
+
+defineOptions({ name: 'BatchFeedbackForm' });
+
+interface Emits {
+ (e: 'success'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const [Modal, modalApi] = useVbenModal({
+ onOpenChange: (open) => {
+ if (open) {
+ selectedTasks.value = [];
+ formData.value = {
+ feedbackQuantity: undefined,
+ qualifiedQuantity: undefined,
+ unqualifiedQuantity: undefined,
+ remark: '',
+ };
+ }
+ },
+});
+
+const taskDialogRef = ref<InstanceType<typeof ProTaskSelectDialog>>();
+const selectedTasks = ref<MesProTaskApi.Task[]>([]);
+const formData = ref({
+ feedbackQuantity: undefined as number | undefined,
+ qualifiedQuantity: undefined as number | undefined,
+ unqualifiedQuantity: undefined as number | undefined,
+ remark: '',
+});
+const submitting = ref(false);
+
+function handleOpenTaskDialog() {
+ taskDialogRef.value?.open([], { multiple: true });
+}
+
+function handleTaskSelected(rows: MesProTaskApi.Task[]) {
+ selectedTasks.value = rows;
+}
+
+function handleRemoveTask(taskId: number) {
+ selectedTasks.value = selectedTasks.value.filter((t) => t.id !== taskId);
+}
+
+async function handleSubmit() {
+ if (!selectedTasks.value.length) {
+ message.warning('璇烽�夋嫨浠诲姟');
+ return;
+ }
+ if (!formData.value.feedbackQuantity) {
+ message.warning('璇疯緭鍏ユ姤宸ユ暟閲�');
+ return;
+ }
+ submitting.value = true;
+ try {
+ const result = await batchCreateFeedback({
+ taskIds: selectedTasks.value.map((t) => t.id!),
+ feedbackQuantity: formData.value.feedbackQuantity,
+ qualifiedQuantity: formData.value.qualifiedQuantity,
+ unqualifiedQuantity: formData.value.unqualifiedQuantity,
+ remark: formData.value.remark || undefined,
+ });
+ if (result.failCount) {
+ message.warning(
+ `鎵归噺鎶ュ伐瀹屾垚锛氭垚鍔� ${result.successCount} 涓紝澶辫触 ${result.failCount} 涓猔,
+ );
+ } else {
+ message.success(`鎵归噺鎶ュ伐鎴愬姛锛屽叡 ${result.successCount} 涓猔);
+ }
+ modalApi.close();
+ emit('success');
+ } finally {
+ submitting.value = false;
+ }
+}
+</script>
+
+<template>
+ <Modal title="鎵归噺鎶ュ伐" @ok="handleSubmit">
+ <a-form layout="vertical">
+ <a-form-item label="閫夋嫨浠诲姟" required>
+ <div class="mb-2">
+ <Button type="dashed" @click="handleOpenTaskDialog">
+ {{ selectedTasks.length ? `宸查�� ${selectedTasks.length} 涓换鍔 : '鐐瑰嚮閫夋嫨浠诲姟' }}
+ </Button>
+ </div>
+ <div v-if="selectedTasks.length" class="flex flex-wrap gap-1">
+ <Tag
+ v-for="task in selectedTasks"
+ :key="task.id"
+ closable
+ @close="handleRemoveTask(task.id!)"
+ >
+ {{ task.code }}
+ </Tag>
+ </div>
+ </a-form-item>
+ <a-form-item label="鎶ュ伐鏁伴噺" required>
+ <a-input-number
+ v-model:value="formData.feedbackQuantity"
+ :min="0.01"
+ :precision="2"
+ placeholder="璇疯緭鍏ユ姤宸ユ暟閲忥紙搴旂敤鍒版墍鏈夐�変腑浠诲姟锛�"
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="鍚堟牸鍝佹暟閲�">
+ <a-input-number
+ v-model:value="formData.qualifiedQuantity"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ悎鏍煎搧鏁伴噺"
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="涓嶈壇鍝佹暟閲�">
+ <a-input-number
+ v-model:value="formData.unqualifiedQuantity"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ笉鑹搧鏁伴噺"
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="澶囨敞">
+ <a-textarea
+ v-model:value="formData.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ :rows="2"
+ />
+ </a-form-item>
+ </a-form>
+ <ProTaskSelectDialog ref="taskDialogRef" @selected="handleTaskSelected" />
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/task/index.vue b/src/views/mes/pro/task/index.vue
index ed4237c..cb0bb3a 100644
--- a/src/views/mes/pro/task/index.vue
+++ b/src/views/mes/pro/task/index.vue
@@ -11,21 +11,27 @@
MesProWorkOrderTypeEnum,
} from '@vben/constants';
-import { Button, Card } from 'ant-design-vue';
+import { Button, Card, message } from 'ant-design-vue';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getGanttTaskList } from '#/api/mes/pro/task';
+import { getGanttTaskList, previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
import { getWorkOrderPage } from '#/api/mes/pro/workorder';
import { GanttChart } from './components';
import { useGridColumns, useGridFormSchema } from './data';
import ScheduleForm from './modules/schedule-form.vue';
+import SchedulePreview from '../workorder-center/modules/schedule-preview.vue';
const router = useRouter();
const ganttTasks = ref<any[]>([]); // 鐢樼壒鍥鹃瑙堟暟鎹�
const [ScheduleModal, scheduleModalApi] = useVbenModal({
connectedComponent: ScheduleForm,
+ destroyOnClose: true,
+});
+
+const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({
+ connectedComponent: SchedulePreview,
destroyOnClose: true,
});
@@ -51,6 +57,29 @@
/** 鎺掍骇 */
function handleSchedule(row: MesProWorkOrderApi.WorkOrder) {
scheduleModalApi.setData({ formType: 'schedule', id: row.id }).open();
+}
+
+/** 涓�閿帓浜ч瑙� */
+async function handleAutoSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
+ try {
+ const preview = await previewAutoGenerate({ workOrderId: row.id! });
+ schedulePreviewModalApi
+ .setData({ workOrder: row, preview })
+ .open();
+ } catch {
+ message.error('鑾峰彇鎺掍骇棰勮澶辫触');
+ }
+}
+
+/** 涓�閿帓浜ф墽琛� */
+async function handleAutoSchedule(row: MesProWorkOrderApi.WorkOrder) {
+ try {
+ const result = await autoGenerateTasks({ workOrderId: row.id! });
+ message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
+ handleRefresh();
+ } catch {
+ message.error('鎺掍骇澶辫触');
+ }
}
/** 鎵撳紑鐢樼壒鍥剧紪杈戦〉闈� */
@@ -100,7 +129,9 @@
</script>
<template>
- <Page auto-content-height><ScheduleModal @success="handleRefresh" />
+ <Page auto-content-height>
+ <ScheduleModal @success="handleRefresh" />
+ <SchedulePreviewModal @success="handleRefresh" />
<!-- 鎺掍骇鐢樼壒鍥鹃瑙� -->
<Card title="鎺掍骇鐢樼壒鍥�" class="mb-4">
@@ -135,6 +166,13 @@
ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
onClick: handleSchedule.bind(null, row),
},
+ {
+ label: '涓�閿帓浜�',
+ type: 'link',
+ auth: ['mes:pro-task:create'],
+ ifShow: row.status === MesProWorkOrderStatusEnum.CONFIRMED,
+ onClick: handleAutoSchedulePreview.bind(null, row),
+ },
]"
/>
</template>
diff --git a/src/views/mes/pro/task/modules/schedule-form.vue b/src/views/mes/pro/task/modules/schedule-form.vue
index 592d470..e51d7b7 100644
--- a/src/views/mes/pro/task/modules/schedule-form.vue
+++ b/src/views/mes/pro/task/modules/schedule-form.vue
@@ -1,6 +1,6 @@
锘�<script lang="ts" setup>
-import type { MesProRouteProcessApi } from '#/api/mes/pro/route/process';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
import { computed, ref } from 'vue';
@@ -11,6 +11,7 @@
import { useVbenForm } from '#/adapter/form';
import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
import { finishWorkOrder, getWorkOrder } from '#/api/mes/pro/workorder';
+import { getWorkOrderProcessListByWorkOrderId } from '#/api/mes/pro/workorder/process';
import { useScheduleFormSchema } from '../data';
import TaskList from './task-list.vue';
@@ -18,7 +19,8 @@
const emit = defineEmits(['success']);
const formType = ref<'detail' | 'schedule'>('schedule');
const workOrder = ref<MesProWorkOrderApi.WorkOrder>(); // 褰撳墠璁㈠崟璇︽儏
-const routeProcessList = ref<MesProRouteProcessApi.RouteProcess[]>([]); // 宸ヨ壓璺嚎宸ュ簭鍒楄〃
+/** 鎺掍骇宸ュ簭鍒楄〃 */
+const routeProcessList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]);
const activeProcessStep = ref(0); // 褰撳墠宸ュ簭姝ラ绱㈠紩
const currentRouteId = ref(0); // 褰撳墠宸ヨ壓璺嚎缂栧彿
@@ -41,17 +43,23 @@
wrapperClass: 'grid-cols-3',
});
-/** 鍔犺浇宸ヨ壓璺嚎宸ュ簭鍒楄〃 */
-async function loadRouteProcesses(productId: number) {
- const processes = await getRouteProcessListByProduct(productId);
- if (!processes || processes.length === 0) {
- message.warning('褰撳墠浜у搧鏈厤缃伐鑹鸿矾绾匡紝璇峰厛鍦ㄥ伐鑹鸿矾绾夸腑缁存姢');
+/** 鍔犺浇宸ュ崟宸ュ簭鍒楄〃锛堜粎浣跨敤宸ュ崟宸ュ簭锛屼笉鍥為��鍒板伐鑹鸿矾绾挎ā鏉匡級 */
+async function loadRouteProcesses(workOrderId: number, productId: number) {
+ const woProcesses = await getWorkOrderProcessListByWorkOrderId(workOrderId);
+ if (!woProcesses || woProcesses.length === 0) {
+ message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅');
return;
}
- currentRouteId.value = processes[0]!.routeId ?? 0;
- routeProcessList.value = [...processes].toSorted(
+ routeProcessList.value = [...woProcesses].toSorted(
(a, b) => (a.sort ?? 0) - (b.sort ?? 0),
);
+ // 纭畾宸ヨ壓璺嚎ID锛氬伐鍗曡矾绾� 鈫� 浜у搧璺嚎
+ if (workOrder.value?.routeId) {
+ currentRouteId.value = workOrder.value.routeId;
+ } else {
+ const processes = await getRouteProcessListByProduct(productId);
+ currentRouteId.value = processes?.[0]?.routeId ?? 0;
+ }
}
/** 瀹屾垚璁㈠崟 */
@@ -94,7 +102,10 @@
// 璁剧疆鍒� values
await formApi.setValues(workOrder.value);
if (workOrder.value.productId) {
- await loadRouteProcesses(workOrder.value.productId);
+ await loadRouteProcesses(
+ workOrder.value.id!,
+ workOrder.value.productId,
+ );
}
} finally {
modalApi.unlock();
@@ -138,13 +149,18 @@
</template>
<template #prepend-footer>
<div class="flex flex-auto items-center gap-2">
- <Popconfirm
- v-if="formType === 'schedule'"
- title="纭瑕佸畬鎴愯璁㈠崟鍚楋紵瀹屾垚鍚庤鍗曚笅鎵�鏈変换鍔″皢鏍囪涓哄凡瀹屾垚銆�"
- @confirm="handleFinish"
- >
- <Button type="primary">瀹屾垚</Button>
- </Popconfirm>
+ <template v-if="formType === 'schedule'">
+ <Popconfirm
+ v-if="routeProcessList.length > 0"
+ title="纭瑕佸畬鎴愯璁㈠崟鍚楋紵瀹屾垚鍚庤鍗曚笅鎵�鏈変换鍔″皢鏍囪涓哄凡瀹屾垚銆�"
+ @confirm="handleFinish"
+ >
+ <Button type="primary">瀹屾垚</Button>
+ </Popconfirm>
+ <span v-else class="text-gray-400 text-sm">
+ 褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅鍚庡啀瀹屾垚
+ </span>
+ </template>
</div>
</template>
</Modal>
diff --git a/src/views/mes/pro/workorder-center/data.ts b/src/views/mes/pro/workorder-center/data.ts
new file mode 100644
index 0000000..7fc7214
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/data.ts
@@ -0,0 +1,182 @@
+import type { VbenFormSchema } from '#/adapter/form';
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+
+import { markRaw } from 'vue';
+
+import {
+ DICT_TYPE,
+ MesProWorkOrderStatusEnum,
+ MesProWorkOrderTypeEnum,
+} from '@vben/constants';
+import { getDictOptions } from '@vben/hooks';
+
+import CrmCustomerSelect from '#/components/crm-customer-select.vue';
+import { getRangePickerDefaultProps } from '#/utils';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+/** 宸ュ崟涓績鎼滅储琛ㄥ崟 */
+export function useGridFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'code',
+ label: '璁㈠崟缂栫爜',
+ component: 'Input',
+ componentProps: { allowClear: true, placeholder: '璇疯緭鍏ヨ鍗曠紪鐮�' },
+ },
+ {
+ fieldName: 'name',
+ label: '璁㈠崟鍚嶇О',
+ component: 'Input',
+ componentProps: { allowClear: true, placeholder: '璇疯緭鍏ヨ鍗曞悕绉�' },
+ },
+ {
+ fieldName: 'productId',
+ label: '浜у搧',
+ component: markRaw(MdItemSelect),
+ componentProps: { placeholder: '璇烽�夋嫨浜у搧' },
+ },
+ {
+ fieldName: 'clientId',
+ label: '瀹㈡埛',
+ component: markRaw(CrmCustomerSelect),
+ componentProps: { placeholder: '璇烽�夋嫨瀹㈡埛' },
+ },
+ {
+ fieldName: 'requestDate',
+ label: '闇�姹傛棩鏈�',
+ component: 'RangePicker',
+ componentProps: {
+ ...getRangePickerDefaultProps(),
+ allowClear: true,
+ },
+ },
+ ];
+}
+
+/** 宸ュ崟涓績鍒楄〃瀛楁 */
+export function useGridColumns(): VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>['columns'] {
+ return [
+ {
+ field: 'code',
+ title: '璁㈠崟缂栫爜',
+ fixed: 'left',
+ width: 200,
+ treeNode: true,
+ slots: { default: 'code' },
+ },
+ {
+ field: 'name',
+ title: '璁㈠崟鍚嶇О',
+ minWidth: 150,
+ },
+ {
+ field: 'productCode',
+ title: '浜у搧缂栫爜',
+ width: 120,
+ },
+ {
+ field: 'productName',
+ title: '浜у搧鍚嶇О',
+ minWidth: 120,
+ },
+ {
+ field: 'productSpecification',
+ title: '瑙勬牸鍨嬪彿',
+ width: 120,
+ },
+ {
+ field: 'unitMeasureName',
+ title: '鍗曚綅',
+ width: 80,
+ },
+ {
+ field: 'quantity',
+ title: '鐢熶骇鏁伴噺',
+ width: 100,
+ },
+ {
+ field: 'quantityScheduled',
+ title: '宸叉帓浜�',
+ width: 100,
+ },
+ {
+ field: 'quantityProduced',
+ title: '宸茬敓浜�',
+ width: 100,
+ },
+ {
+ field: 'clientName',
+ title: '瀹㈡埛鍚嶇О',
+ width: 120,
+ },
+ {
+ field: 'requestDate',
+ title: '闇�姹傛棩鏈�',
+ width: 120,
+ formatter: 'formatDate',
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_WORK_ORDER_STATUS },
+ },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 200,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
+
+/** 杩涘害涓殑浠诲姟鍒楄〃瀛楁 */
+export function useTaskGridColumns(): VxeTableGridOptions<any>['columns'] {
+ return [
+ {
+ field: 'taskCode',
+ title: '浠诲姟缂栫爜',
+ width: 180,
+ },
+ {
+ field: 'workstationName',
+ title: '宸ヤ綔绔�',
+ width: 140,
+ },
+ {
+ field: 'quantity',
+ title: '鎺掍骇鏁伴噺',
+ width: 100,
+ },
+ {
+ field: 'producedQuantity',
+ title: '宸茬敓浜�',
+ width: 100,
+ },
+ {
+ field: 'progressPercent',
+ title: '杩涘害',
+ width: 120,
+ slots: { default: 'taskProgress' },
+ },
+ {
+ field: 'status',
+ title: '鐘舵��',
+ width: 100,
+ cellRender: {
+ name: 'CellDict',
+ props: { type: DICT_TYPE.MES_PRO_TASK_STATUS },
+ },
+ },
+ {
+ title: '鎿嶄綔',
+ width: 100,
+ fixed: 'right',
+ slots: { default: 'taskActions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/workorder-center/index.vue b/src/views/mes/pro/workorder-center/index.vue
new file mode 100644
index 0000000..b90ebb8
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/index.vue
@@ -0,0 +1,313 @@
+<script lang="ts" setup>
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi, TaskInfo } from '#/api/mes/pro/workorder';
+
+import { ref } from 'vue';
+import { useRouter } from 'vue-router';
+
+import { Page, useVbenModal } from '@vben/common-ui';
+import { MesProWorkOrderStatusEnum, MesProWorkOrderTypeEnum } from '@vben/constants';
+
+import { Button, Card, Collapse, Progress, Tag, message } from 'ant-design-vue';
+
+import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ getWorkOrderPage,
+ getWorkOrderProgress,
+ checkAndAutoFinishWorkOrder,
+} from '#/api/mes/pro/workorder';
+import { previewAutoGenerate, autoGenerateTasks } from '#/api/mes/pro/task';
+
+import { useGridColumns, useGridFormSchema, useTaskGridColumns } from './data';
+import QuickFeedbackForm from './modules/quick-feedback.vue';
+import SchedulePreviewModal from './modules/schedule-preview.vue';
+
+defineOptions({ name: 'WorkorderCenter' });
+
+const router = useRouter();
+
+const progressData = ref<MesProWorkOrderApi.WorkOrderProgress | null>(null);
+const activeProcessKeys = ref<string[]>([]);
+const expandedRowId = ref<number | null>(null);
+
+const [QuickFeedbackModal, quickFeedbackModalApi] = useVbenModal({
+ connectedComponent: QuickFeedbackForm,
+ destroyOnClose: true,
+});
+
+const [SchedulePreviewModal, schedulePreviewModalApi] = useVbenModal({
+ connectedComponent: SchedulePreviewModal,
+ destroyOnClose: true,
+});
+
+function handleRefresh() {
+ gridApi.query();
+ // 濡傛灉杩涘害璇︽儏灞曞紑涓紝鍒锋柊杩涘害鏁版嵁
+ if (expandedRowId.value) {
+ refreshProgress();
+ }
+}
+
+async function refreshProgress() {
+ if (!expandedRowId.value) return;
+ progressData.value = await getWorkOrderProgress(expandedRowId.value);
+}
+
+/** 蹇�熸姤宸ユ垚鍔熷悗鍒锋柊杩涘害骞舵鏌ヨ嚜鍔ㄥ畬鎴� */
+async function handleFeedbackSuccess() {
+ handleRefresh();
+ if (expandedRowId.value) {
+ try {
+ await checkAndAutoFinishWorkOrder(expandedRowId.value);
+ } catch {
+ // 鑷姩瀹屾垚妫�鏌ュけ璐ヤ笉褰卞搷涓绘祦绋�
+ }
+ }
+}
+
+/** 鏌ョ湅宸ュ崟杩涘害锛堝睍寮�琛岋級 */
+async function handleToggleProgress(row: MesProWorkOrderApi.WorkOrder) {
+ if (expandedRowId.value === row.id) {
+ expandedRowId.value = null;
+ progressData.value = null;
+ return;
+ }
+ expandedRowId.value = row.id!;
+ progressData.value = await getWorkOrderProgress(row.id!);
+ // 榛樿灞曞紑绗竴涓伐搴�
+ if (progressData.value?.processList?.length) {
+ activeProcessKeys.value = [String(progressData.value.processList[0].processId)];
+ }
+}
+
+/** 宸ュ簭杩涘害鏉¢鑹� */
+function getProcessStatusColor(status: number): string {
+ switch (status) {
+ case 2: return '#52c41a';
+ case 1: return '#1677ff';
+ default: return '#d9d9d9';
+ }
+}
+
+/** 宸ュ簭鐘舵�佹枃瀛� */
+function getProcessStatusText(status: number): string {
+ switch (status) {
+ case 2: return '宸插畬鎴�';
+ case 1: return '杩涜涓�';
+ default: return '鏈紑濮�';
+ }
+}
+
+/** 蹇�熸姤宸� */
+function handleQuickFeedback(task: TaskInfo) {
+ quickFeedbackModalApi
+ .setData({ taskId: task.taskId, taskCode: task.taskCode, taskQuantity: task.quantity, taskProducedQuantity: task.producedQuantity })
+ .open();
+}
+
+/** 涓�閿帓浜ч瑙� */
+async function handleSchedulePreview(row: MesProWorkOrderApi.WorkOrder) {
+ try {
+ const preview = await previewAutoGenerate({ workOrderId: row.id! });
+ schedulePreviewModalApi
+ .setData({ workOrder: row, preview })
+ .open();
+ } catch {
+ message.error('鑾峰彇鎺掍骇棰勮澶辫触');
+ }
+}
+
+/** 鎵ц涓�閿帓浜� */
+async function handleScheduleExecute(row: MesProWorkOrderApi.WorkOrder) {
+ try {
+ const result = await autoGenerateTasks({ workOrderId: row.id! });
+ message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
+ handleRefresh();
+ } catch {
+ message.error('鎺掍骇澶辫触');
+ }
+}
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ formOptions: {
+ schema: useGridFormSchema(),
+ },
+ gridOptions: {
+ columns: useGridColumns(),
+ height: 'auto',
+ keepSource: true,
+ proxyConfig: {
+ ajax: {
+ query: async ({ page }, formValues) => {
+ return await getWorkOrderPage({
+ pageNo: page.currentPage,
+ pageSize: page.pageSize,
+ status: MesProWorkOrderStatusEnum.CONFIRMED,
+ type: MesProWorkOrderTypeEnum.SELF,
+ ...formValues,
+ });
+ },
+ },
+ },
+ rowConfig: {
+ keyField: 'id',
+ isHover: true,
+ },
+ toolbarConfig: {
+ refresh: true,
+ search: true,
+ },
+ } as VxeTableGridOptions<MesProWorkOrderApi.WorkOrder>,
+});
+</script>
+
+<template>
+ <Page auto-content-height>
+ <QuickFeedbackModal @success="handleFeedbackSuccess" />
+ <SchedulePreviewModal @success="handleRefresh" />
+
+ <Grid table-title="宸ュ崟涓績">
+ <template #toolbar-tools>
+ <TableAction
+ :actions="[
+ {
+ label: '鍒涘缓宸ュ崟',
+ type: 'primary',
+ icon: ACTION_ICON.ADD,
+ auth: ['mes:pro-work-order:create'],
+ onClick: () => router.push({ name: 'MesProWorkorder' }),
+ },
+ ]"
+ />
+ </template>
+
+ <template #code="{ row }">
+ <Button type="link" @click="handleToggleProgress(row)">
+ {{ row.code }}
+ </Button>
+ </template>
+
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '鎺掍骇棰勮',
+ type: 'link',
+ auth: ['mes:pro-task:create'],
+ ifShow: row.quantityScheduled < row.quantity,
+ onClick: handleSchedulePreview.bind(null, row),
+ },
+ {
+ label: '涓�閿帓浜�',
+ type: 'link',
+ auth: ['mes:pro-task:create'],
+ ifShow: row.quantityScheduled < row.quantity,
+ popConfirm: {
+ title: '纭涓�閿帓浜э紵灏嗘寜鐓у伐鑹鸿矾绾胯嚜鍔ㄧ敓鎴愪换鍔°��',
+ confirm: handleScheduleExecute.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+
+ <!-- 灞曞紑鐨勫伐鍗曡繘搴﹁鎯� -->
+ <Card
+ v-if="progressData"
+ title="宸ュ崟杩涘害璇︽儏"
+ class="mt-4"
+ size="small"
+ >
+ <template #extra>
+ <Button type="link" @click="expandedRowId = null; progressData = null">鏀惰捣</Button>
+ </template>
+
+ <!-- 鎬讳綋杩涘害 -->
+ <div class="mb-4 flex items-center gap-4">
+ <span class="text-gray-500">鎬昏繘搴︼細</span>
+ <Progress
+ :percent="Math.round(progressData.progressPercent || 0)"
+ :stroke-color="{
+ from: '#1677ff',
+ to: '#52c41a',
+ }"
+ class="flex-1"
+ />
+ <span class="text-sm text-gray-400">
+ {{ progressData.quantityProduced }} / {{ progressData.quantity }}
+ </span>
+ </div>
+
+ <!-- 宸ュ簭杩涘害姝ラ鏉� -->
+ <div class="mb-4 flex items-center gap-0">
+ <template v-for="(proc, idx) in progressData.processList" :key="proc.processId!">
+ <!-- 鑺傜偣 -->
+ <div class="flex flex-col items-center" style="flex: 1">
+ <Tag :color="getProcessStatusColor(proc.status!)">
+ {{ proc.processName }}
+ </Tag>
+ <span class="mt-1 text-xs text-gray-400">
+ {{ getProcessStatusText(proc.status!) }}
+ </span>
+ <Progress
+ v-if="proc.keyFlag"
+ :percent="Math.round(proc.progressPercent || 0)"
+ :size="80"
+ type="circle"
+ :stroke-color="getProcessStatusColor(proc.status!)"
+ class="mt-1"
+ />
+ </div>
+ <!-- 杩炵嚎 -->
+ <div
+ v-if="idx < progressData.processList!.length - 1"
+ class="h-0.5 flex-1"
+ :style="{
+ background:
+ proc.status === 2
+ ? '#52c41a'
+ : proc.status === 1
+ ? '#1677ff'
+ : '#d9d9d9',
+ }"
+ />
+ </template>
+ </div>
+
+ <!-- 浠诲姟鏄庣粏 -->
+ <Collapse v-model:activeKey="activeProcessKeys">
+ <Collapse.Panel
+ v-for="proc in progressData.processList"
+ :key="String(proc.processId)"
+ :header="`${proc.processName}锛�${proc.producedQuantity || 0} / ${proc.plannedQuantity || proc.producedQuantity || 0}锛塦"
+ >
+ <vxe-grid
+ :key="proc.processId"
+ :columns="useTaskGridColumns()"
+ :data="proc.taskList || []"
+ height="auto"
+ size="small"
+ >
+ <template #taskProgress="{ row: taskRow }">
+ <Progress
+ :percent="Math.round(taskRow.progressPercent || 0)"
+ size="small"
+ />
+ </template>
+ <template #taskActions="{ row: taskRow }">
+ <Button
+ type="link"
+ size="small"
+ :disabled="taskRow.progressPercent >= 100"
+ @click="handleQuickFeedback(taskRow)"
+ >
+ 鎶ュ伐
+ </Button>
+ </template>
+ </vxe-grid>
+ </Collapse.Panel>
+ </Collapse>
+ </Card>
+ </Page>
+</template>
diff --git a/src/views/mes/pro/workorder-center/modules/quick-feedback.vue b/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
new file mode 100644
index 0000000..b249fdf
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/modules/quick-feedback.vue
@@ -0,0 +1,158 @@
+<script lang="ts" setup>
+import type { FormInstance } from 'ant-design-vue';
+import type { MesProTaskApi } from '#/api/mes/pro/task';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { quickCreateFeedback } from '#/api/mes/pro/feedback';
+import ProTaskSelect from '#/views/mes/pro/task/components/select.vue';
+
+defineOptions({ name: 'QuickFeedbackForm' });
+
+interface Props {
+ taskId?: number;
+ taskCode?: string;
+ taskQuantity?: number;
+ taskProducedQuantity?: number;
+}
+
+const props = withDefaults(defineProps<Props>(), {});
+
+interface Emits {
+ (e: 'success'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const [Modal, modalApi] = useVbenModal({
+ onOpenChange: (open) => {
+ if (open) {
+ selectedTaskId.value = props.taskId;
+ selectedTask.value = undefined;
+ formData.value = {
+ feedbackQuantity: undefined,
+ qualifiedQuantity: undefined,
+ unqualifiedQuantity: undefined,
+ remark: '',
+ };
+ }
+ },
+});
+
+const formRef = ref<FormInstance>();
+const selectedTaskId = ref<number | undefined>(props.taskId);
+const selectedTask = ref<MesProTaskApi.Task>();
+const formData = ref({
+ feedbackQuantity: undefined as number | undefined,
+ qualifiedQuantity: undefined as number | undefined,
+ unqualifiedQuantity: undefined as number | undefined,
+ remark: '',
+});
+const submitting = ref(false);
+
+const hasPredefinedTask = computed(() => !!props.taskId);
+
+const pendingQuantity = computed(() => {
+ if (hasPredefinedTask.value) {
+ if (props.taskQuantity == null || props.taskProducedQuantity == null) return 0;
+ return Math.max(0, Number(props.taskQuantity) - Number(props.taskProducedQuantity));
+ }
+ if (!selectedTask.value) return 0;
+ const qty = Number(selectedTask.value.quantity || 0);
+ const produced = Number(selectedTask.value.producedQuantity || 0);
+ return Math.max(0, qty - produced);
+});
+
+function handleTaskChange(task: MesProTaskApi.Task | undefined) {
+ selectedTask.value = task;
+ selectedTaskId.value = task?.id;
+}
+
+async function handleSubmit() {
+ const taskId = selectedTaskId.value;
+ if (!taskId) {
+ message.warning('璇烽�夋嫨浠诲姟');
+ return;
+ }
+ if (!formData.value.feedbackQuantity) {
+ message.warning('璇疯緭鍏ユ姤宸ユ暟閲�');
+ return;
+ }
+ submitting.value = true;
+ try {
+ await quickCreateFeedback({
+ taskId,
+ feedbackQuantity: formData.value.feedbackQuantity,
+ qualifiedQuantity: formData.value.qualifiedQuantity,
+ unqualifiedQuantity: formData.value.unqualifiedQuantity,
+ remark: formData.value.remark || undefined,
+ });
+ message.success('鎶ュ伐鎴愬姛');
+ modalApi.close();
+ emit('success');
+ } finally {
+ submitting.value = false;
+ }
+}
+</script>
+
+<template>
+ <Modal title="蹇�熸姤宸�" @ok="handleSubmit">
+ <a-form ref="formRef" :model="formData" layout="vertical">
+ <a-form-item label="浠诲姟" required>
+ <ProTaskSelect
+ v-if="!hasPredefinedTask"
+ v-model="selectedTaskId"
+ @change="handleTaskChange"
+ />
+ <a-input v-else :value="props.taskCode" disabled />
+ </a-form-item>
+ <a-form-item label="寰呮姤宸ユ暟閲�">
+ <a-input-number
+ :value="pendingQuantity"
+ disabled
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="鏈鎶ュ伐鏁伴噺" required>
+ <a-input-number
+ v-model:value="formData.feedbackQuantity"
+ :min="0.01"
+ :max="pendingQuantity || undefined"
+ :precision="2"
+ placeholder="璇疯緭鍏ユ姤宸ユ暟閲�"
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="鍚堟牸鍝佹暟閲�">
+ <a-input-number
+ v-model:value="formData.qualifiedQuantity"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ュ悎鏍煎搧鏁伴噺"
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="涓嶈壇鍝佹暟閲�">
+ <a-input-number
+ v-model:value="formData.unqualifiedQuantity"
+ :min="0"
+ :precision="2"
+ placeholder="璇疯緭鍏ヤ笉鑹搧鏁伴噺"
+ class="!w-full"
+ />
+ </a-form-item>
+ <a-form-item label="澶囨敞">
+ <a-textarea
+ v-model:value="formData.remark"
+ placeholder="璇疯緭鍏ュ娉�"
+ :rows="2"
+ />
+ </a-form-item>
+ </a-form>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/workorder-center/modules/schedule-preview.vue b/src/views/mes/pro/workorder-center/modules/schedule-preview.vue
new file mode 100644
index 0000000..9607a1b
--- /dev/null
+++ b/src/views/mes/pro/workorder-center/modules/schedule-preview.vue
@@ -0,0 +1,86 @@
+<script lang="ts" setup>
+import type { AutoGeneratePreview } from '#/api/mes/pro/task';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+
+import { ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message, Tag } from 'ant-design-vue';
+
+import { autoGenerateTasks } from '#/api/mes/pro/task';
+
+defineOptions({ name: 'SchedulePreviewModal' });
+
+interface Props {
+ workOrder?: MesProWorkOrderApi.WorkOrder;
+ preview?: AutoGeneratePreview;
+}
+
+const props = withDefaults(defineProps<Props>(), {});
+
+interface Emits {
+ (e: 'success'): void;
+}
+
+const emit = defineEmits<Emits>();
+
+const [Modal, modalApi] = useVbenModal({
+ onOpenChange: (open) => {
+ if (!open) return;
+ },
+});
+
+const executing = ref(false);
+
+async function handleExecute() {
+ if (!props.workOrder?.id) return;
+ executing.value = true;
+ try {
+ const result = await autoGenerateTasks({
+ workOrderId: props.workOrder.id,
+ routeId: props.preview?.routeId,
+ });
+ message.success(`鎺掍骇鎴愬姛锛岀敓鎴� ${result.taskIds?.length || 0} 涓换鍔);
+ modalApi.close();
+ emit('success');
+ } finally {
+ executing.value = false;
+ }
+}
+</script>
+
+<template>
+ <Modal title="涓�閿帓浜ч瑙�" @ok="handleExecute">
+ <a-descriptions :column="2" bordered size="small" class="mb-4">
+ <a-descriptions-item label="宸ュ崟缂栫爜">{{ props.workOrder?.code }}</a-descriptions-item>
+ <a-descriptions-item label="鐢熶骇鏁伴噺">{{ props.workOrder?.quantity }}</a-descriptions-item>
+ <a-descriptions-item label="宸ヨ壓璺嚎">{{ props.preview?.routeName }}</a-descriptions-item>
+ <a-descriptions-item label="棰勭敓鎴愪换鍔℃暟">{{ props.preview?.taskList?.length || 0 }}</a-descriptions-item>
+ </a-descriptions>
+
+ <vxe-grid
+ :columns="[
+ { field: 'sort', title: '搴忓彿', width: 60 },
+ { field: 'processName', title: '宸ュ簭鍚嶇О', minWidth: 120 },
+ {
+ field: 'keyFlag',
+ title: '鍏抽敭宸ュ簭',
+ width: 80,
+ slots: { default: 'keyFlag' },
+ },
+ { field: 'suggestWorkstationName', title: '寤鸿宸ヤ綔绔�', width: 140 },
+ { field: 'quantity', title: '鎺掍骇鏁伴噺', width: 100 },
+ ]"
+ :data="props.preview?.taskList || []"
+ height="auto"
+ size="small"
+ >
+ <template #keyFlag="{ row: taskRow }">
+ <Tag :color="taskRow.keyFlag ? 'blue' : 'default'">
+ {{ taskRow.keyFlag ? '鏄�' : '鍚�' }}
+ </Tag>
+ </template>
+ </vxe-grid>
+ </Modal>
+</template>
diff --git a/src/views/mes/pro/workorder/data.ts b/src/views/mes/pro/workorder/data.ts
index 265c93a..9a4bf1b 100644
--- a/src/views/mes/pro/workorder/data.ts
+++ b/src/views/mes/pro/workorder/data.ts
@@ -3,6 +3,7 @@
import type { MesMdProductBomApi } from '#/api/mes/md/item/productBom';
import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
import type { MesProWorkOrderBomApi } from '#/api/mes/pro/workorder/bom';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
import { h, markRaw } from 'vue';
@@ -734,3 +735,105 @@
},
];
}
+
+/** 璁㈠崟宸ュ簭鍒楄〃鐨勫瓧娈� */
+export function useProcessGridColumns(
+ draggable = false,
+): VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>['columns'] {
+ return [
+ ...(draggable
+ ? [
+ {
+ field: 'drag',
+ title: '',
+ width: 40,
+ fixed: 'left',
+ slots: { default: 'dragHandle' },
+ } as const,
+ ]
+ : []),
+ {
+ field: 'sort',
+ title: '搴忓彿',
+ width: 80,
+ },
+ {
+ field: 'processCode',
+ title: '宸ュ簭缂栫爜',
+ width: 120,
+ },
+ {
+ field: 'processName',
+ title: '宸ュ簭鍚嶇О',
+ minWidth: 150,
+ },
+ {
+ field: 'nextProcessName',
+ title: '涓嬩竴閬撳伐搴�',
+ width: 120,
+ },
+ {
+ field: 'linkType',
+ title: '宸ュ簭鍏崇郴',
+ width: 100,
+ formatter: ({ cellValue }) => {
+ const map: Record<number, string> = {
+ 0: '寮�濮�-寮�濮�',
+ 1: '缁撴潫-缁撴潫',
+ 2: '寮�濮�-缁撴潫',
+ 3: '缁撴潫-寮�濮�',
+ };
+ return map[cellValue] ?? '-';
+ },
+ },
+ {
+ field: 'prepareTime',
+ title: '鍑嗗鏃堕棿(鍒�)',
+ width: 100,
+ },
+ {
+ field: 'waitTime',
+ title: '绛夊緟鏃堕棿(鍒�)',
+ width: 100,
+ },
+ {
+ field: 'keyFlag',
+ title: '鍏抽敭宸ュ簭',
+ width: 80,
+ slots: { default: 'keyFlag' },
+ },
+ {
+ field: 'checkFlag',
+ title: '璐ㄦ宸ュ簭',
+ width: 80,
+ slots: { default: 'checkFlag' },
+ },
+ {
+ field: 'backflushFlag',
+ title: '鍊掑啿',
+ width: 80,
+ slots: { default: 'backflushFlag' },
+ },
+ {
+ field: 'outputItemCode',
+ title: '浜у嚭鐗╂枡缂栫爜',
+ width: 120,
+ },
+ {
+ field: 'outputItemName',
+ title: '浜у嚭鐗╂枡鍚嶇О',
+ width: 120,
+ },
+ {
+ field: 'remark',
+ title: '澶囨敞',
+ minWidth: 120,
+ },
+ {
+ title: '鎿嶄綔',
+ width: 120,
+ fixed: 'right',
+ slots: { default: 'actions' },
+ },
+ ];
+}
diff --git a/src/views/mes/pro/workorder/modules/form.vue b/src/views/mes/pro/workorder/modules/form.vue
index 676159e..dced20c 100644
--- a/src/views/mes/pro/workorder/modules/form.vue
+++ b/src/views/mes/pro/workorder/modules/form.vue
@@ -3,6 +3,7 @@
import type { MesProWorkOrderApi } from "#/api/mes/pro/workorder";
import type { MesProWorkOrderBomApi } from "#/api/mes/pro/workorder/bom";
+ import type { MesProWorkOrderProcessApi } from "#/api/mes/pro/workorder/process";
import { computed, ref } from "vue";
@@ -22,29 +23,33 @@
getWorkOrder,
updateWorkOrder,
} from "#/api/mes/pro/workorder";
+ import { batchSaveWorkOrderProcess, getWorkOrderProcessListByWorkOrderId } from "#/api/mes/pro/workorder/process";
import { $t } from "#/locales";
import { BarcodeDetail } from "#/views/wls/barcode/components";
import { useFormSchema } from "../data";
import BomList from "./bom-list.vue";
import ItemList from "./item-list.vue";
+ import ProcessList from "./process-list.vue";
const emit = defineEmits(["success"]);
const formType = ref<FormType>("create");
const formData = ref<MesProWorkOrderApi.WorkOrder>();
- const originalSnapshot = ref(""); // 琛ㄥ崟鍘熷鏁版嵁蹇収锛岀敤浜庣‘璁ゆ椂璺宠繃鏈彉鏇寸殑淇濆瓨璇锋眰
- const subTabsName = ref("bom"); // 褰撳墠閫変腑鐨勫瓙琛� Tab
- const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>(); // 鏉$爜璇︽儏寮圭獥
+ const originalSnapshot = ref("");
+ const subTabsName = ref("bom");
+ const barcodeDetailRef = ref<InstanceType<typeof BarcodeDetail>>();
+ const processListRef = ref<InstanceType<typeof ProcessList>>();
+ const currentProductId = ref<number>(); // 褰撳墠閫変腑鐨勪骇鍝� ID锛堢敤浜庡伐搴忓姞杞斤級
+ const tempProcessList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]); // 涓存椂宸ュ簭鍒楄〃
const isEditable = computed(() =>
- // 鏄惁涓虹紪杈戞ā寮忥紙鍙繚瀛橈級
["create", "update"].includes(formType.value)
);
- const isConfirm = computed(() => formType.value === "confirm"); // 鏄惁涓虹‘璁ゆā寮�
- const isFinish = computed(() => formType.value === "finish"); // 鏄惁涓哄畬鎴愭ā寮�
+ const isConfirm = computed(() => formType.value === "confirm");
+ const isFinish = computed(() => formType.value === "finish");
+ const isCreateMode = computed(() => formType.value === "create");
const canConfirm = computed(
() =>
- // 缂栬緫鎬佽崏绋垮彲纭
formType.value === "update" &&
formData.value?.status === MesProWorkOrderStatusEnum.PREPARE
);
@@ -69,6 +74,9 @@
}
});
+ // 鍒ゆ柇鏄惁鏄剧ず Tab锛堟柊澧炴ā寮忔垨宸叉湁 ID锛�
+ const showTabs = computed(() => isCreateMode.value || formData.value?.id);
+
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
@@ -81,6 +89,15 @@
schema: [],
showDefaultActions: false,
wrapperClass: "grid-cols-3",
+ handleValuesChange: (values, fieldsChanged) => {
+ // 浜у搧鍙樻洿鏃讹紝瑙﹀彂宸ュ簭鍔犺浇
+ if (isCreateMode.value && fieldsChanged.includes('productId')) {
+ const newProductId = values.productId;
+ if (newProductId && newProductId !== currentProductId.value) {
+ currentProductId.value = newProductId;
+ }
+ }
+ },
});
/** 閲嶆柊鎸傝浇 schema 骞舵寜琛ㄥ崟鎬佹帶鍒跺簳閮ㄦ寜閽� */
@@ -110,7 +127,7 @@
);
}
- /** 浠� BOM 鐗╂枡琛岄濉瓙璁㈠崟锛氬鐢ㄥ綋鍓嶈鍗曚笂涓嬫枃锛岃繘鍏ユ柊澧炴�� */
+ /** 浠� BOM 鐗╂枡琛岄濉瓙璁㈠崟 */
async function handleGenerateWorkOrder(
bomRow: MesProWorkOrderBomApi.WorkOrderBom
) {
@@ -137,8 +154,15 @@
type: current.type,
vendorId: current.vendorId,
});
+ // 璁剧疆褰撳墠浜у搧 ID锛岃Е鍙戝伐搴忓姞杞�
+ currentProductId.value = bomRow.itemId;
originalSnapshot.value = JSON.stringify(await formApi.getValues());
message.info("宸蹭粠 BOM 鐗╂枡棰勫~瀛愯鍗曪紝璇疯ˉ鍏呰鍗曠紪鐮佺瓑淇℃伅鍚庝繚瀛�");
+ }
+
+ /** 宸ュ簭鍒楄〃鍙樺寲鍥炶皟 */
+ function handleProcessListChange(list: MesProWorkOrderProcessApi.WorkOrderProcess[]) {
+ tempProcessList.value = list;
}
const [Modal, modalApi] = useVbenModal({
@@ -154,7 +178,7 @@
}
modalApi.lock();
try {
- // 缂栬緫鎬佹湁淇敼鏃跺厛淇濆瓨
+ // 1. 淇濆瓨宸ュ崟淇℃伅锛堝鏋滄湁鍙樻洿锛�
if (isEditable.value) {
const current = JSON.stringify(await formApi.getValues());
if (current !== originalSnapshot.value) {
@@ -163,6 +187,21 @@
originalSnapshot.value = current;
}
}
+ // 2. 鎵归噺淇濆瓨宸ュ簭锛堝鏋滄湁锛�
+ if (tempProcessList.value.length > 0) {
+ const processList = tempProcessList.value.map(item => ({
+ ...item,
+ workOrderId: formData.value!.id,
+ }));
+ await batchSaveWorkOrderProcess(processList);
+ }
+ // 3. 鏍¢獙鏄惁鏈夊伐搴�
+ const processes = await getWorkOrderProcessListByWorkOrderId(formData.value.id);
+ if (!processes || processes.length === 0) {
+ message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅鍚庡啀纭');
+ return;
+ }
+ // 4. 纭宸ュ崟
await confirmWorkOrder(formData.value.id);
message.success('璁㈠崟宸茬‘璁�');
await modalApi.close();
@@ -175,6 +214,12 @@
// ---- 瀹屾垚璁㈠崟妯″紡 ----
if (isFinish.value) {
if (!formData.value?.id) return;
+ // 鏍¢獙鏄惁鏈夊伐搴�
+ const processes = await getWorkOrderProcessListByWorkOrderId(formData.value.id);
+ if (!processes || processes.length === 0) {
+ message.warning('褰撳墠璁㈠崟鏈厤缃伐搴忥紝璇峰厛鍦ㄥ伐鍗曚腑缁存姢宸ュ簭淇℃伅鍚庡啀瀹屾垚');
+ return;
+ }
try {
await confirm('纭瑕佸畬鎴愯璁㈠崟鍚楋紵');
} catch {
@@ -202,13 +247,31 @@
const data = (await formApi.getValues()) as MesProWorkOrderApi.WorkOrder;
try {
if (formData.value?.id) {
+ // 缂栬緫妯″紡
await updateWorkOrder({ ...formData.value, ...data });
+ // 鎵归噺淇濆瓨宸ュ簭
+ if (tempProcessList.value.length > 0) {
+ const processList = tempProcessList.value.map(item => ({
+ ...item,
+ workOrderId: formData.value!.id,
+ }));
+ await batchSaveWorkOrderProcess(processList);
+ }
formData.value = { ...formData.value, ...data };
emit("success");
message.success($t("ui.actionMessage.operationSuccess"));
await modalApi.close();
} else {
+ // 鏂板妯″紡
const id = await createWorkOrder(data);
+ // 鎵归噺淇濆瓨宸ュ簭
+ if (tempProcessList.value.length > 0) {
+ const processList = tempProcessList.value.map(item => ({
+ ...item,
+ workOrderId: id,
+ }));
+ await batchSaveWorkOrderProcess(processList);
+ }
formData.value = {
...data,
id,
@@ -228,9 +291,10 @@
if (!isOpen) {
formData.value = undefined;
originalSnapshot.value = "";
+ currentProductId.value = undefined;
+ tempProcessList.value = [];
return;
}
- // 鍔犺浇鏁版嵁
const data = modalApi.getData<{
formType: FormType;
id?: number;
@@ -238,18 +302,18 @@
}>();
formType.value = data.formType;
subTabsName.value = "bom";
+ tempProcessList.value = [];
applySchema();
if (data?.id) {
modalApi.lock();
try {
formData.value = await getWorkOrder(data.id);
- // 璁剧疆鍒� values
await formApi.setValues(formData.value);
+ currentProductId.value = formData.value.productId;
} finally {
modalApi.unlock();
}
} else if (data?.parentRow) {
- // 鏂板瀛愯鍗曟椂锛岄濉埗璁㈠崟淇℃伅
const parent = data.parentRow;
await formApi.setValues({
clientId: parent.clientId,
@@ -270,20 +334,39 @@
<Modal :title="getTitle"
class="w-3/5">
<Form class="mx-4" />
- <!-- BOM / 鐗╂枡闇�姹� Tab锛氶潪鏂板缓鎬佸睍绀� -->
- <template v-if="formData?.id">
+ <!-- BOM / 宸ュ簭 / 鐗╂枡闇�姹� Tab -->
+ <template v-if="showTabs">
<Tabs v-model:active-key="subTabsName"
class="mx-4">
<Tabs.TabPane key="bom"
tab="璁㈠崟 BOM">
- <BomList :form-type="formType"
+ <BomList v-if="formData?.id"
+ :form-type="formType"
:work-order="formData"
:work-order-id="formData.id"
@generate-work-order="handleGenerateWorkOrder" />
+ <div v-else class="text-gray-400 text-center py-8">
+ 璇峰厛淇濆瓨宸ュ崟鍚庡啀閰嶇疆 BOM
+ </div>
+ </Tabs.TabPane>
+ <Tabs.TabPane key="process"
+ tab="宸ュ簭">
+ <ProcessList
+ ref="processListRef"
+ :form-type="formType"
+ :work-order="formData || {}"
+ :work-order-id="formData?.id"
+ :product-id="currentProductId"
+ @process-list-change="handleProcessListChange"
+ />
</Tabs.TabPane>
<Tabs.TabPane key="item"
tab="鐗╂枡闇�姹�">
- <ItemList :work-order-id="formData.id" />
+ <ItemList v-if="formData?.id"
+ :work-order-id="formData.id" />
+ <div v-else class="text-gray-400 text-center py-8">
+ 璇峰厛淇濆瓨宸ュ崟鍚庡啀鏌ョ湅鐗╂枡闇�姹�
+ </div>
</Tabs.TabPane>
</Tabs>
</template>
diff --git a/src/views/mes/pro/workorder/modules/process-form.vue b/src/views/mes/pro/workorder/modules/process-form.vue
new file mode 100644
index 0000000..7cfb93c
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/process-form.vue
@@ -0,0 +1,319 @@
+<script lang="ts" setup>
+import type { VbenFormSchema } from '#/adapter/form';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
+
+import { computed, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { useVbenForm } from '#/adapter/form';
+import { getProcessSimpleList } from '#/api/mes/pro/process';
+import {
+ createWorkOrderProcess,
+ getWorkOrderProcess,
+ updateWorkOrderProcess,
+} from '#/api/mes/pro/workorder/process';
+import { $t } from '#/locales';
+import { MdItemSelect } from '#/views/mes/md/item/components';
+
+const emit = defineEmits(['success']);
+const formData = ref<MesProWorkOrderProcessApi.WorkOrderProcess>();
+const workOrderId = ref<number>();
+const processList = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]);
+const isCreateMode = ref(false);
+
+const getTitle = computed(() => {
+ return formData.value?.id
+ ? $t('ui.actionTitle.edit', ['宸ュ簭'])
+ : $t('ui.actionTitle.create', ['宸ュ簭']);
+});
+
+/** 鍔犺浇宸ュ簭閫夐」 */
+async function loadProcessOptions() {
+ const list = await getProcessSimpleList();
+ return (list || []).map((item) => ({
+ label: item.name ?? '',
+ value: item.id!,
+ }));
+}
+
+/** 琛ㄥ崟 schema */
+async function loadSchema(): Promise<VbenFormSchema[]> {
+ const processOptions = await loadProcessOptions();
+ return [
+ {
+ fieldName: 'id',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'workOrderId',
+ component: 'Input',
+ dependencies: { triggerFields: [''], show: () => false },
+ },
+ {
+ fieldName: 'processId',
+ label: '宸ュ簭',
+ component: 'Select',
+ componentProps: {
+ options: processOptions,
+ placeholder: '璇烽�夋嫨宸ュ簭',
+ onChange: async (value: number) => {
+ const process = processOptions.find((item) => item.value === value);
+ if (process) {
+ await formApi.setFieldValue('processName', process.label);
+ }
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'processName',
+ label: '宸ュ簭鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'sort',
+ label: '搴忓彿',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 1,
+ placeholder: '璇疯緭鍏ュ簭鍙�',
+ precision: 0,
+ },
+ rules: 'required',
+ },
+ {
+ fieldName: 'linkType',
+ label: '宸ュ簭鍏崇郴',
+ component: 'Select',
+ componentProps: {
+ allowClear: true,
+ options: [
+ { label: '寮�濮�-寮�濮�', value: 0 },
+ { label: '缁撴潫-缁撴潫', value: 1 },
+ { label: '寮�濮�-缁撴潫', value: 2 },
+ { label: '缁撴潫-寮�濮�', value: 3 },
+ ],
+ placeholder: '璇烽�夋嫨宸ュ簭鍏崇郴',
+ },
+ },
+ {
+ fieldName: 'prepareTime',
+ label: '鍑嗗鏃堕棿(鍒嗛挓)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ placeholder: '璇疯緭鍏ュ噯澶囨椂闂�',
+ precision: 0,
+ },
+ },
+ {
+ fieldName: 'waitTime',
+ label: '绛夊緟鏃堕棿(鍒嗛挓)',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0,
+ placeholder: '璇疯緭鍏ョ瓑寰呮椂闂�',
+ precision: 0,
+ },
+ },
+ {
+ fieldName: 'colorCode',
+ label: '鐢樼壒鍥鹃鑹�',
+ component: 'Input',
+ componentProps: {
+ placeholder: '璇烽�夋嫨棰滆壊',
+ type: 'color',
+ },
+ },
+ {
+ fieldName: 'keyFlag',
+ label: '鏄惁鍏抽敭宸ュ簭',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '鍚�', value: false },
+ { label: '鏄�', value: true },
+ ],
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'checkFlag',
+ label: '鏄惁璐ㄦ宸ュ簭',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '鍚�', value: false },
+ { label: '鏄�', value: true },
+ ],
+ },
+ defaultValue: false,
+ },
+ {
+ fieldName: 'backflushFlag',
+ label: '鏄惁鍊掑啿',
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ { label: '鍚�', value: false },
+ { label: '鏄�', value: true },
+ ],
+ },
+ defaultValue: true,
+ },
+ {
+ fieldName: 'outputItemId',
+ label: '浜у嚭鐗╂枡',
+ component: MdItemSelect,
+ componentProps: {
+ placeholder: '璇烽�夋嫨浜у嚭鐗╂枡',
+ },
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: {
+ placeholder: '璇疯緭鍏ュ娉�',
+ rows: 2,
+ },
+ },
+ ];
+}
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 130,
+ },
+ layout: 'horizontal',
+ schema: [],
+ 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 MesProWorkOrderProcessApi.WorkOrderProcess;
+ data.workOrderId = workOrderId.value;
+
+ // 鏍¢獙搴忓彿鍞竴
+ const isSortDuplicate = processList.value.some(
+ (item) => item.sort === data.sort && item.processId !== data.processId
+ );
+ if (isSortDuplicate) {
+ message.error('搴忓彿宸插瓨鍦�');
+ modalApi.unlock();
+ return;
+ }
+
+ // 鏍¢獙鍏抽敭宸ュ簭鍞竴
+ if (data.keyFlag) {
+ const hasKeyProcess = processList.value.some(
+ (item) => item.keyFlag && item.processId !== data.processId
+ );
+ if (hasKeyProcess) {
+ message.error('宸插瓨鍦ㄥ叧閿伐搴忥紝姣忎釜宸ュ崟鍙兘鏈変竴涓叧閿伐搴�');
+ modalApi.unlock();
+ return;
+ }
+ }
+
+ try {
+ if (isCreateMode.value) {
+ // 鏂板妯″紡锛氱洿鎺ヨ繑鍥炴暟鎹粰鐖剁粍浠�
+ emit('success', data, !!formData.value?.processId);
+ await modalApi.close();
+ } else if (formData.value?.id) {
+ // 缂栬緫妯″紡锛氳皟鐢ㄦ洿鏂版帴鍙�
+ await updateWorkOrderProcess(data);
+ emit('success', data, true);
+ await modalApi.close();
+ message.success($t('ui.actionMessage.operationSuccess'));
+ } else {
+ // 鏂板妯″紡锛堟湁 workOrderId锛夛細璋冪敤鍒涘缓鎺ュ彛
+ await createWorkOrderProcess(data);
+ emit('success', data, false);
+ await modalApi.close();
+ message.success($t('ui.actionMessage.operationSuccess'));
+ }
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ formData.value = undefined;
+ processList.value = [];
+ isCreateMode.value = false;
+ return;
+ }
+ modalApi.lock();
+ try {
+ const schema = await loadSchema();
+ formApi.setState({ schema });
+ } finally {
+ modalApi.unlock();
+ }
+
+ const data = modalApi.getData<{
+ id?: number;
+ productId?: number;
+ workOrderId?: number;
+ row?: MesProWorkOrderProcessApi.WorkOrderProcess;
+ processList?: MesProWorkOrderProcessApi.WorkOrderProcess[];
+ }>();
+ if (!data) {
+ return;
+ }
+
+ workOrderId.value = data.workOrderId;
+ processList.value = data.processList || [];
+ isCreateMode.value = !data.workOrderId;
+
+ if (data.row) {
+ // 缂栬緫涓存椂鍒楄〃涓殑宸ュ簭
+ formData.value = data.row;
+ await formApi.setValues(data.row);
+ } else if (data.id) {
+ // 缂栬緫宸蹭繚瀛樼殑宸ュ簭
+ modalApi.lock();
+ try {
+ formData.value = await getWorkOrderProcess(data.id);
+ await formApi.setValues(formData.value);
+ } finally {
+ modalApi.unlock();
+ }
+ } else {
+ // 鏂板
+ formData.value = undefined;
+ const maxSort = Math.max(0, ...processList.value.map((item) => item.sort || 0));
+ await formApi.setValues({
+ colorCode: '#00AEF3',
+ sort: maxSort + 1,
+ });
+ }
+ },
+});
+</script>
+
+<template>
+ <Modal :title="getTitle" class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
\ No newline at end of file
diff --git a/src/views/mes/pro/workorder/modules/process-list.vue b/src/views/mes/pro/workorder/modules/process-list.vue
new file mode 100644
index 0000000..35ac698
--- /dev/null
+++ b/src/views/mes/pro/workorder/modules/process-list.vue
@@ -0,0 +1,304 @@
+<script lang="ts" setup>
+import type { FormType } from '../data';
+
+import type { VxeTableGridOptions } from '#/adapter/vxe-table';
+import type { MesProWorkOrderApi } from '#/api/mes/pro/workorder';
+import type { MesProWorkOrderProcessApi } from '#/api/mes/pro/workorder/process';
+
+import { computed, nextTick, onUnmounted, ref, watch } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+import { MesProWorkOrderStatusEnum } from '@vben/constants';
+import { IconifyIcon } from '@vben/icons';
+
+import { useSortable } from '@vueuse/integrations/useSortable';
+import { message, Tag } from 'ant-design-vue';
+
+import { TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
+import {
+ deleteWorkOrderProcess,
+ getWorkOrderProcessListByWorkOrderId,
+} from '#/api/mes/pro/workorder/process';
+import { getRouteProcessListByProduct } from '#/api/mes/pro/route/process';
+import { $t } from '#/locales';
+
+import { useProcessGridColumns } from '../data';
+import ProcessForm from './process-form.vue';
+
+const props = defineProps<{
+ formType: FormType;
+ workOrder: MesProWorkOrderApi.WorkOrder;
+ workOrderId?: number;
+ productId?: number;
+}>();
+
+const emit = defineEmits<{
+ processListChange: [list: MesProWorkOrderProcessApi.WorkOrderProcess[]];
+}>();
+
+// 宸ュ簭鍒楄〃鏁版嵁
+const list = ref<MesProWorkOrderProcessApi.WorkOrderProcess[]>([]);
+
+const isEditable = computed(() =>
+ ['create', 'update'].includes(props.formType) &&
+ (props.workOrder?.status === MesProWorkOrderStatusEnum.PREPARE ||
+ props.formType === 'create'),
+);
+
+const isCreateMode = computed(() => props.formType === 'create');
+
+// 鎷栨嫿鎺掑簭
+const sortableInstance = ref<any>(null);
+
+const [ProcessFormModal, processFormModalApi] = useVbenModal({
+ connectedComponent: ProcessForm,
+ destroyOnClose: true,
+});
+
+const [Grid, gridApi] = useVbenVxeGrid({
+ gridOptions: {
+ autoResize: true,
+ border: true,
+ columns: useProcessGridColumns(isEditable.value),
+ data: [],
+ minHeight: 240,
+ pagerConfig: { enabled: false },
+ rowConfig: { isHover: true, keyField: 'processId' },
+ showOverflow: true,
+ toolbarConfig: { enabled: false },
+ } as VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>,
+});
+
+/** 鍒濆鍖�/閿�姣佹嫋鎷芥帓搴� */
+async function initSortable() {
+ destroySortable();
+ if (!isEditable.value || list.value.length === 0) return;
+ await nextTick();
+ sortableInstance.value = useSortable(
+ '.workorder-process-list .vxe-table--body-wrapper:not(.fixed-right--wrapper) .vxe-table--body tbody',
+ list.value,
+ {
+ animation: 150,
+ draggable: '.vxe-body--row',
+ handle: '.drag-handle',
+ onEnd: ({ newDraggableIndex, oldDraggableIndex }) => {
+ if (
+ newDraggableIndex !== undefined &&
+ oldDraggableIndex !== undefined &&
+ oldDraggableIndex !== newDraggableIndex
+ ) {
+ // 閲嶆柊鎺掑簭鏁扮粍
+ list.value.splice(
+ newDraggableIndex,
+ 0,
+ list.value.splice(oldDraggableIndex, 1)[0]!,
+ );
+ // 鏇存柊 sort 瀛楁
+ list.value.forEach((item, index) => {
+ item.sort = index + 1;
+ });
+ gridApi.setGridOptions({ data: list.value });
+ emit('processListChange', list.value);
+ }
+ },
+ },
+ );
+}
+
+/** 閿�姣佹嫋鎷芥帓搴忓疄渚� */
+function destroySortable() {
+ if (sortableInstance.value) {
+ sortableInstance.value.stop?.();
+ sortableInstance.value = null;
+ }
+}
+
+/** 鍔犺浇宸ュ簭鍒楄〃 */
+async function getList() {
+ gridApi.setLoading(true);
+ try {
+ if (isCreateMode.value) {
+ // 鏂板妯″紡锛氫粠宸ヨ壓璺嚎鑾峰彇
+ if (!props.productId) {
+ list.value = [];
+ } else {
+ // 璋冪敤宸ヨ壓璺嚎鎺ュ彛鑾峰彇宸ュ簭鍒楄〃
+ const routeProcessList = await getRouteProcessListByProduct(props.productId);
+ // 杞崲涓哄伐鍗曞伐搴忔牸寮�
+ list.value = routeProcessList.map((item) => ({
+ processId: item.processId,
+ processCode: item.processCode,
+ processName: item.processName,
+ sort: item.sort,
+ nextProcessId: item.nextProcessId,
+ nextProcessName: item.nextProcessName,
+ linkType: item.linkType,
+ prepareTime: item.prepareTime,
+ waitTime: item.waitTime,
+ colorCode: item.colorCode,
+ keyFlag: item.keyFlag,
+ checkFlag: item.checkFlag,
+ backflushFlag: item.backflushFlag,
+ outputItemId: item.outputItemId,
+ outputItemCode: item.outputItemCode,
+ outputItemName: item.outputItemName,
+ remark: item.remark,
+ }));
+ }
+ } else {
+ // 缂栬緫妯″紡锛氫粠宸ュ崟宸ュ簭鑾峰彇
+ if (!props.workOrderId) {
+ list.value = [];
+ } else {
+ list.value = await getWorkOrderProcessListByWorkOrderId(props.workOrderId);
+ }
+ }
+ gridApi.setGridOptions({ data: list.value });
+ emit('processListChange', list.value);
+ // 鍒濆鍖栨嫋鎷芥帓搴�
+ initSortable();
+ } finally {
+ gridApi.setLoading(false);
+ }
+}
+
+/** 娣诲姞宸ュ簭 */
+function handleCreate() {
+ processFormModalApi
+ .setData({
+ workOrderId: props.workOrderId,
+ productId: props.productId || props.workOrder?.productId,
+ processList: list.value,
+ })
+ .open();
+}
+
+/** 缂栬緫宸ュ簭 */
+function handleEdit(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
+ processFormModalApi
+ .setData({
+ id: row.id,
+ workOrderId: props.workOrderId,
+ productId: props.productId || props.workOrder?.productId,
+ row,
+ processList: list.value,
+ })
+ .open();
+}
+
+/** 鏂板/缂栬緫鎴愬姛鍥炶皟 */
+function handleFormSuccess(data: MesProWorkOrderProcessApi.WorkOrderProcess, isEdit: boolean) {
+ if (isCreateMode.value) {
+ if (isEdit) {
+ const index = list.value.findIndex((item) => item.processId === data.processId);
+ if (index > -1) {
+ list.value[index] = data;
+ }
+ } else {
+ list.value.push(data);
+ }
+ gridApi.setGridOptions({ data: list.value });
+ emit('processListChange', list.value);
+ initSortable();
+ } else {
+ getList();
+ }
+}
+
+/** 鍒犻櫎宸ュ簭 */
+function handleDelete(row: MesProWorkOrderProcessApi.WorkOrderProcess) {
+ if (isCreateMode.value) {
+ // 鏂板妯″紡锛氫粠鍒楄〃鍒犻櫎
+ const index = list.value.findIndex((item) => item.processId === row.processId);
+ if (index > -1) {
+ list.value.splice(index, 1);
+ // 鏇存柊 sort 瀛楁
+ list.value.forEach((item, idx) => {
+ item.sort = idx + 1;
+ });
+ gridApi.setGridOptions({ data: list.value });
+ emit('processListChange', list.value);
+ initSortable();
+ }
+ } else {
+ // 缂栬緫妯″紡锛氳皟鐢ㄦ帴鍙e垹闄�
+ deleteWorkOrderProcess(row.id!).then(() => {
+ message.success($t('ui.actionMessage.deleteSuccess', [row.processName]));
+ getList();
+ });
+ }
+}
+
+// 缁勪欢鍗歌浇鏃堕攢姣佹帓搴忓疄渚�
+onUnmounted(() => {
+ destroySortable();
+});
+
+// 鐩戝惉浜у搧/宸ュ崟鍙樺寲
+watch(
+ () => [props.productId, props.workOrderId],
+ () => {
+ getList();
+ },
+ { immediate: true },
+);
+</script>
+
+<template>
+ <div class="workorder-process-list">
+ <ProcessFormModal @success="handleFormSuccess" />
+ <div v-if="isEditable" class="mb-3 flex items-center justify-start">
+ <TableAction
+ :actions="[
+ {
+ label: '娣诲姞宸ュ簭',
+ type: 'primary',
+ onClick: handleCreate,
+ },
+ ]"
+ />
+ </div>
+ <Grid class="w-full" table-title="宸ュ簭">
+ <template #dragHandle>
+ <IconifyIcon
+ icon="ic:round-drag-indicator"
+ class="drag-handle cursor-move text-xl text-gray-400"
+ />
+ </template>
+ <template #keyFlag="{ row }">
+ <Tag v-if="row.keyFlag" color="red">鍏抽敭</Tag>
+ <span v-else>-</span>
+ </template>
+ <template #checkFlag="{ row }">
+ <Tag v-if="row.checkFlag" color="blue">璐ㄦ</Tag>
+ <span v-else>-</span>
+ </template>
+ <template #backflushFlag="{ row }">
+ <Tag v-if="row.backflushFlag" color="green">鏄�</Tag>
+ <Tag v-else color="default">鍚�</Tag>
+ </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', [row.processName]),
+ confirm: handleDelete.bind(null, row),
+ },
+ },
+ ]"
+ />
+ </template>
+ </Grid>
+ </div>
+</template>
\ No newline at end of file
diff --git a/src/views/wls/materialstock/components/index.ts b/src/views/wls/materialstock/components/index.ts
index 2cb4faf..60d3bf1 100644
--- a/src/views/wls/materialstock/components/index.ts
+++ b/src/views/wls/materialstock/components/index.ts
@@ -1,3 +1,4 @@
export { default as WmMaterialStockSelectDialog } from './select-dialog.vue';
export { default as WmMaterialStockSelect } from './select.vue';
export { default as WmStockAdjustModal } from './stock-adjust-modal.vue';
+export { default as WmStockMoveModal } from './move-modal.vue';
diff --git a/src/views/wls/materialstock/components/move-modal.vue b/src/views/wls/materialstock/components/move-modal.vue
new file mode 100644
index 0000000..582726a
--- /dev/null
+++ b/src/views/wls/materialstock/components/move-modal.vue
@@ -0,0 +1,237 @@
+<script lang="ts" setup>
+import type { MesWmMaterialStockApi } from '#/api/mes/wm/materialstock';
+import type { VbenFormSchema } from '#/adapter/form';
+
+import { markRaw, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { message } from 'ant-design-vue';
+
+import { z } from '#/adapter/form';
+import { useVbenForm } from '#/adapter/form';
+import { moveMaterialStock } from '#/api/mes/wm/materialstock';
+import {
+ WmWarehouseAreaSelect,
+ WmWarehouseLocationSelect,
+ WmWarehouseSelect,
+} from '#/views/wls/warehouse/components';
+
+defineOptions({ name: 'WmStockMoveModal' });
+
+const emit = defineEmits<{ success: [] }>();
+
+const currentRow = ref<MesWmMaterialStockApi.MaterialStock>();
+
+function useFormSchema(): VbenFormSchema[] {
+ return [
+ {
+ fieldName: 'itemCode',
+ label: '鐗╂枡缂栫爜',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'itemName',
+ label: '鐗╂枡鍚嶇О',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'batchCode',
+ label: '鎵规鍙�',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'warehouseName',
+ label: '褰撳墠浠撳簱',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'locationName',
+ label: '褰撳墠搴撳尯',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'areaName',
+ label: '褰撳墠搴撲綅',
+ component: 'Input',
+ componentProps: { disabled: true },
+ },
+ {
+ fieldName: 'quantity',
+ label: '褰撳墠鏁伴噺',
+ component: 'InputNumber',
+ componentProps: { disabled: true, class: '!w-full', precision: 2 },
+ },
+ {
+ fieldName: 'toWarehouseId',
+ label: '鐩爣浠撳簱',
+ component: markRaw(WmWarehouseSelect),
+ componentProps: {
+ placeholder: '璇烽�夋嫨鐩爣浠撳簱',
+ onChange: async () => {
+ await formApi.setFieldValue('toLocationId', undefined);
+ await formApi.setFieldValue('toAreaId', undefined);
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'toLocationId',
+ label: '鐩爣搴撳尯',
+ component: markRaw(WmWarehouseLocationSelect),
+ componentProps: { placeholder: '璇烽�夋嫨鐩爣搴撳尯' },
+ dependencies: {
+ triggerFields: ['toWarehouseId'],
+ componentProps: (values) => ({
+ warehouseId: values.toWarehouseId,
+ placeholder: '璇烽�夋嫨鐩爣搴撳尯',
+ onChange: async () => {
+ await formApi.setFieldValue('toAreaId', undefined);
+ },
+ }),
+ trigger: (values, api) => {
+ if (values.toLocationId !== undefined) {
+ void api.setFieldValue('toLocationId', undefined);
+ void api.setFieldValue('toAreaId', undefined);
+ }
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'toAreaId',
+ label: '鐩爣搴撲綅',
+ component: markRaw(WmWarehouseAreaSelect),
+ componentProps: { placeholder: '璇烽�夋嫨鐩爣搴撲綅' },
+ dependencies: {
+ triggerFields: ['toLocationId'],
+ componentProps: (values) => ({
+ locationId: values.toLocationId,
+ placeholder: '璇烽�夋嫨鐩爣搴撲綅',
+ }),
+ trigger: (values, api) => {
+ if (values.toAreaId !== undefined) {
+ void api.setFieldValue('toAreaId', undefined);
+ }
+ },
+ },
+ rules: 'selectRequired',
+ },
+ {
+ fieldName: 'moveQuantity',
+ label: '绉诲簱鏁伴噺',
+ component: 'InputNumber',
+ componentProps: {
+ class: '!w-full',
+ min: 0.01,
+ placeholder: '璇疯緭鍏ョЩ搴撴暟閲�',
+ precision: 2,
+ },
+ rules: z.number().positive('绉诲簱鏁伴噺蹇呴』澶т簬0'),
+ },
+ {
+ fieldName: 'remark',
+ label: '澶囨敞',
+ component: 'Textarea',
+ formItemClass: 'col-span-2',
+ componentProps: { placeholder: '璇疯緭鍏ュ娉�', rows: 3 },
+ },
+ ];
+}
+
+const [Form, formApi] = useVbenForm({
+ commonConfig: {
+ componentProps: { class: 'w-full' },
+ formItemClass: 'col-span-1',
+ labelWidth: 100,
+ },
+ layout: 'horizontal',
+ schema: useFormSchema(),
+ showDefaultActions: false,
+ wrapperClass: 'grid-cols-2',
+});
+
+const [Modal, modalApi] = useVbenModal({
+ async onConfirm() {
+ const { valid } = await formApi.validate();
+ if (!valid) return;
+
+ const row = currentRow.value;
+ if (!row?.id) return;
+
+ const values = await formApi.getValues();
+ const moveQuantity = values.moveQuantity as number;
+ const quantity = (row.quantity || 0) as number;
+ const availableQuantity = (row.availableQuantity || 0) as number;
+
+ if (moveQuantity > quantity) {
+ message.error(`绉诲簱鏁伴噺涓嶈兘瓒呰繃褰撳墠搴撳瓨鏁伴噺锛�${quantity.toFixed(2)}锛塦);
+ return;
+ }
+
+ if (moveQuantity > availableQuantity) {
+ message.error(`绉诲簱鏁伴噺涓嶈兘瓒呰繃鍙敤閲忥紙${availableQuantity.toFixed(2)}锛夛紝鍙敤閲� = 鍦ㄥ簱鏁伴噺 - 鍐荤粨鏁伴噺 - 鍗犵敤閲廯);
+ return;
+ }
+
+ if (row.warehouseId === values.toWarehouseId && row.locationId === values.toLocationId && row.areaId === values.toAreaId) {
+ message.error('鐩爣搴撲綅涓庡綋鍓嶅簱浣嶇浉鍚岋紝涓嶅厑璁哥Щ搴�');
+ return;
+ }
+
+ modalApi.lock();
+ try {
+ await moveMaterialStock({
+ materialStockId: row.id,
+ quantity: moveQuantity,
+ remark: values.remark,
+ toAreaId: values.toAreaId,
+ toLocationId: values.toLocationId,
+ toWarehouseId: values.toWarehouseId,
+ });
+ message.success('绉诲簱鎴愬姛');
+ emit('success');
+ await modalApi.close();
+ } finally {
+ modalApi.unlock();
+ }
+ },
+ async onOpenChange(isOpen: boolean) {
+ if (!isOpen) {
+ currentRow.value = undefined;
+ return;
+ }
+ const data = modalApi.getData<MesWmMaterialStockApi.MaterialStock>();
+ if (!data?.id) return;
+ currentRow.value = data;
+
+ if (data.frozen) {
+ message.warning('璇ュ簱瀛樿褰曞凡鍐荤粨锛屼笉鍏佽绉诲簱');
+ await modalApi.close();
+ return;
+ }
+
+ await formApi.setValues({
+ areaName: data.areaName,
+ batchCode: data.batchCode,
+ itemCode: data.itemCode,
+ itemName: data.itemName,
+ locationName: data.locationName,
+ quantity: data.quantity,
+ warehouseName: data.warehouseName,
+ });
+ },
+ title: '搴撳瓨绉诲簱',
+});
+</script>
+
+<template>
+ <Modal class="w-3/5">
+ <Form class="mx-4" />
+ </Modal>
+</template>
diff --git a/src/views/wls/materialstock/data.ts b/src/views/wls/materialstock/data.ts
index 3efe144..8cc4eb3 100644
--- a/src/views/wls/materialstock/data.ts
+++ b/src/views/wls/materialstock/data.ts
@@ -83,6 +83,7 @@
newFrozen: boolean,
row: MesWmMaterialStockApi.MaterialStock,
) => Promise<boolean | undefined>,
+ onMove?: (row: MesWmMaterialStockApi.MaterialStock) => void,
): VxeTableGridOptions<MesWmMaterialStockApi.MaterialStock>['columns'] {
const { hasAccessByCodes } = useAccess();
return [
@@ -189,6 +190,16 @@
},
},
},
+ ...(onMove
+ ? [
+ {
+ title: '鎿嶄綔',
+ width: 80,
+ fixed: 'right' as const,
+ slots: { default: 'actions' } as const,
+ },
+ ]
+ : []),
];
}
diff --git a/src/views/wls/materialstock/index.vue b/src/views/wls/materialstock/index.vue
index 3ae7731..17e3625 100644
--- a/src/views/wls/materialstock/index.vue
+++ b/src/views/wls/materialstock/index.vue
@@ -19,7 +19,7 @@
import { MdItemTypeTree } from '#/views/mes/md/item/type/components';
import { WmBatchDetail } from '#/views/wls/batch/components';
import AreaForm from '#/views/wls/warehouse/area/modules/form.vue';
-import { WmStockAdjustModal } from './components';
+import { WmStockAdjustModal, WmStockMoveModal } from './components';
import { useGridColumns, useGridFormSchema } from './data';
@@ -30,6 +30,11 @@
const [StockAdjustModal, stockAdjustModalApi] = useVbenModal({
connectedComponent: WmStockAdjustModal,
+ destroyOnClose: true,
+});
+
+const [StockMoveModal, stockMoveModalApi] = useVbenModal({
+ connectedComponent: WmStockMoveModal,
destroyOnClose: true,
});
@@ -74,6 +79,16 @@
gridApi.query();
}
+/** 鎵撳紑绉诲簱寮圭獥 */
+function handleOpenStockMove(row: MesWmMaterialStockApi.MaterialStock) {
+ stockMoveModalApi.setData(row).open();
+}
+
+/** 绉诲簱鎴愬姛鍥炶皟 */
+function handleStockMoveSuccess() {
+ gridApi.query();
+}
+
/** 鎵撳紑鎵规璇︽儏寮圭獥 */
function handleOpenBatchDetail(row: MesWmMaterialStockApi.MaterialStock) {
if (!row.batchId) {
@@ -108,7 +123,7 @@
schema: useGridFormSchema(),
},
gridOptions: {
- columns: useGridColumns(handleFrozenChange),
+ columns: useGridColumns(handleFrozenChange, handleOpenStockMove),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -139,6 +154,7 @@
<Page auto-content-height><AreaModal />
<WmBatchDetail ref="batchDetailRef" />
<StockAdjustModal @success="handleStockAdjustSuccess" />
+ <StockMoveModal @success="handleStockMoveSuccess" />
<div class="flex h-full w-full">
<!-- 宸︿晶鐗╂枡鍒嗙被鏍� -->
@@ -192,6 +208,19 @@
</Button>
<span v-else>-</span>
</template>
+ <template #actions="{ row }">
+ <TableAction
+ :actions="[
+ {
+ label: '绉诲簱',
+ type: 'link',
+ auth: ['mes:wm-material-stock:move'],
+ ifShow: !row.frozen,
+ onClick: handleOpenStockMove.bind(null, row),
+ },
+ ]"
+ />
+ </template>
</Grid>
</div>
</div>
--
Gitblit v1.9.3