feat(workorder): 添加工单工序管理功能并优化API服务
- 添加工单工序相关API接口定义(批量保存工序、工单进度详情等)
- 实现工序列表组件及字段配置功能
- 集成工序管理到工单表单中,支持工序的增删改查
- 添加快速报工和批量报工功能模块
- 实现一键排产预览和执行功能
- 更新依赖包配置和锁定文件
- 重构API服务模式文档,规范接口定义方式
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | --- |
| | | paths: |
| | | - "**/*.ts" |
| | | - "**/*.vue" |
| | | --- |
| | | |
| | | # TypeScript å端å¼åè§å |
| | | |
| | | > åºäº ECC TypeScript è§åæ©å±ï¼éé
Vue Vben Admin 项ç®ã |
| | | |
| | | ## ç±»åå®ä¹ |
| | | |
| | | ### ç¦æ¢ any |
| | | |
| | | ```typescript |
| | | // â é误ï¼ä½¿ç¨ any |
| | | function handleData(data: any) { |
| | | return data.value; |
| | | } |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨å
·ä½ç±»å |
| | | interface DataType { |
| | | value: string; |
| | | } |
| | | |
| | | function handleData(data: DataType) { |
| | | return data.value; |
| | | } |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ unknown + ç±»åå®å« |
| | | function getErrorMessage(error: unknown): string { |
| | | if (error instanceof Error) { |
| | | return error.message; |
| | | } |
| | | return 'æªç¥é误'; |
| | | } |
| | | ``` |
| | | |
| | | ### import type |
| | | |
| | | ```typescript |
| | | // â
æ£ç¡®ï¼ç±»å导å
¥ä½¿ç¨ import type |
| | | import type { UserInfo } from '#/api/system/user'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | |
| | | // â éè¯¯ï¼æ®é导å
¥ç±»å |
| | | import { UserInfo } from '#/api/system/user'; |
| | | ``` |
| | | |
| | | ### æ¥å£å®ä¹ |
| | | |
| | | ```typescript |
| | | // â
æ£ç¡®ï¼å®ä½æ¥å£ |
| | | export namespace UserApi { |
| | | export interface User { |
| | | id?: number; |
| | | username: string; |
| | | nickname?: string; |
| | | status: number; |
| | | } |
| | | } |
| | | |
| | | // â
æ£ç¡®ï¼Props æ¥å£ |
| | | interface Props { |
| | | userId: number; |
| | | userName?: string; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | userName: '', |
| | | }); |
| | | ``` |
| | | |
| | | ## ä¸å¯åæ§ |
| | | |
| | | ```typescript |
| | | // â é误ï¼ç´æ¥ä¿®æ¹ props |
| | | props.userName = 'new name'; |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ emit æ´æ° |
| | | emit('update:userName', 'new name'); |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨å±å¼è¿ç®ç¬¦ |
| | | const newUser = { ...user, name: 'new name' }; |
| | | ``` |
| | | |
| | | ## é误å¤ç |
| | | |
| | | ```typescript |
| | | // â
æ£ç¡®ï¼async/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 |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ # å«å |
| | | import { getUser } from '#/api/system/user'; |
| | | import DictTag from '#/components/dict-tag/dict-tag.vue'; |
| | | |
| | | // â é误ï¼ä½¿ç¨ç¸å¯¹è·¯å¾ |
| | | import { getUser } from '../../../api/system/user'; |
| | | ``` |
| | | |
| | | ## ç»ä»¶å½å |
| | | |
| | | ```typescript |
| | | // â
æ£ç¡®ï¼ç»ä»¶æä»¶å PascalCase |
| | | UserList.vue |
| | | DictTag.vue |
| | | TableAction.vue |
| | | |
| | | // â
æ£ç¡®ï¼å®ä¹ç»ä»¶å |
| | | defineOptions({ name: 'UserList' }); |
| | | ``` |
| | | |
| | | ## æ§å¶å°è¾åº |
| | | |
| | | ```typescript |
| | | // â ç¦æ¢ï¼console.log å¨ç产代ç ä¸ |
| | | console.log('debug info'); |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨æ¥å¿åºæç§»é¤ |
| | | // ç产ç¯å¢ç§»é¤ææè°è¯è¾åº |
| | | ``` |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | --- |
| | | 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> |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ 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> |
| | | // â
æ£ç¡®ï¼ç±»åå 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> |
| | | <!-- â
æ£ç¡®ï¼ä¼å
ä½¿ç¨ 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; |
| | | } |
| | | } |
| | | |
| | | /* â é误ï¼ç¡¬ç¼ç é¢è² */ |
| | | .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> |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ # å«å |
| | | import { getUser } from '#/api/system/user'; |
| | | import DictTag from '#/components/dict-tag/dict-tag.vue'; |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | |
| | | // â
æ£ç¡®ï¼ç¸å¯¹è·¯å¾å¯¼å
¥åç®å½ |
| | | 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`** |
| | |
| | | "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 *)" |
| | | ] |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | --- |
| | | name: api-service-pattern |
| | | description: API æ¥å£å®ä¹æ¨¡å¼ï¼å
æ¬ç±»åå®ä¹ãæ¥å£å½æ°ãå页æ¥è¯¢ãå½å空é´è§èã |
| | | 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` | |
| | | |
| | | ## 忍¡åæ¥å£ |
| | | |
| | | 忍¡åæ¥å£æ¾å¨ç¶æ¨¡åç®å½ä¸ï¼ |
| | | |
| | | ``` |
| | | 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}` |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | --- |
| | | name: vben-table-form |
| | | description: Vue Vben Admin è¡¨æ ¼å表åå¼å模å¼ï¼å
æ¬ useVbenVxeGridãuseVbenModalãuseVbenForm ç使ç¨ã |
| | | origin: ECC |
| | | --- |
| | | |
| | | # Vue Vben Admin è¡¨æ ¼è¡¨åæ¨¡å¼ |
| | | |
| | | é¡¹ç®æ ¸å¿ç»ä»¶çä½¿ç¨æåï¼è¡¨æ ¼ï¼useVbenVxeGridï¼ã表åå¼¹çªï¼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']` |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | --- |
| | | name: vue-component-patterns |
| | | description: Vue 3 ç»ä»¶å¼å模å¼ï¼å
æ¬ Composition APIãProps/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 |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ import type 导å
¥ç±»å |
| | | import type { UserInfo } from '#/api/system/user'; |
| | | |
| | | // â
æ£ç¡®ï¼å®ä¹å
·ä½æ¥å£ |
| | | interface Props { |
| | | userId: number; |
| | | userName?: string; |
| | | } |
| | | |
| | | // â é误ï¼ä½¿ç¨ any |
| | | const data: any = {}; |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ 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/*`** |
| | |
| | | |
| | | ## å¯ç¨æè½ |
| | | |
| | | - `vue3-development` - Vue 3 å¼åè§è |
| | | - `antd-vue-components` - Ant Design Vue ç»ä»¶æ¨¡å¼ |
| | | - `frontend-code-review` - å端代ç å®¡æ¥æå |
| | | - `vue-component-patterns` - Vue 3 ç»ä»¶å¼å模å¼ï¼Composition APIãProps/Emitsï¼ |
| | | - `vben-table-form` - è¡¨æ ¼è¡¨åç»ä»¶ä½¿ç¨ï¼useVbenVxeGridãuseVbenModalãuseVbenFormï¼ |
| | | - `api-service-pattern` - API æ¥å£å®ä¹æ¨¡å¼ï¼namespaceãç±»åå®ä¹ãCRUD 彿°ï¼ |
| | | |
| | | ## 注æäºé¡¹ |
| | | |
| | |
| | | "ora": "catalog:", |
| | | "pkg-types": "catalog:", |
| | | "rimraf": "catalog:" |
| | | }, |
| | | "devDependencies": { |
| | | "unrun": "^0.3.1" |
| | | } |
| | | } |
| | |
| | | "tsx": "^4.22.4", |
| | | "typescript": "catalog:", |
| | | "vite": "catalog:" |
| | | } |
| | | } |
| | | }, |
| | | "packageManager": "pnpm@11.0.9+sha512.34ce82e6780233cf9cad8685029a8f81d2e06196c5a9bad98879f7424940c6817c4e4524fb7d38b8553ceed48b9758b8ebaf1abd3600c232c4c8cf7366086f38" |
| | | } |
| | |
| | | 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 |
| | |
| | | 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: |
| | |
| | | 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==} |
| | | |
| | |
| | | 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==} |
| | |
| | | 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 |
| | |
| | | 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==} |
| | |
| | | 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==} |
| | | |
| | |
| | | |
| | | 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==} |
| | |
| | | |
| | | lodash.snakecase@4.1.1: {} |
| | | |
| | | lodash.sortby@4.7.0: {} |
| | | |
| | | lodash.startcase@4.4.0: {} |
| | | |
| | | lodash.truncate@4.4.2: {} |
| | |
| | | |
| | | 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: |
| | |
| | | |
| | | tr46@0.0.3: {} |
| | | |
| | | tr46@1.0.1: |
| | | dependencies: |
| | | punycode: 2.3.1 |
| | | |
| | | tree-kill@1.2.2: {} |
| | | |
| | | trim-newlines@5.0.0: {} |
| | |
| | | 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 |
| | |
| | | optionalDependencies: |
| | | tsx: 4.22.4 |
| | | typescript: 6.0.3 |
| | | unrun: 0.3.1(synckit@0.11.13) |
| | | transitivePeerDependencies: |
| | | - '@ts-macro/tsc' |
| | | - '@typescript/native-preview' |
| | |
| | | '@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: |
| | |
| | | |
| | | webidl-conversions@3.0.0: {} |
| | | |
| | | webidl-conversions@4.0.2: {} |
| | | |
| | | webpack-virtual-modules@0.6.2: {} |
| | | |
| | | webrtc-adapter@9.0.5: |
| | |
| | | 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: |
| | |
| | | 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 |
| | |
| | | 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[]>( |
| | |
| | | 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, |
| | |
| | | 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}`, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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}`); |
| | | } |
| | |
| | | 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<{ |
| | |
| | | |
| | | 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, |
| | | }); |
| | | |
| | |
| | | downloadFileFromBlobPart({ fileName: 'ç产æ¥å·¥.xls', source: data }); |
| | | } |
| | | |
| | | /** å¿«éæ¥å·¥ */ |
| | | function handleQuickCreate() { |
| | | quickFeedbackModalApi.setData({}).open(); |
| | | } |
| | | |
| | | /** æ¹éæ¥å·¥ */ |
| | | function handleBatchCreate() { |
| | | batchFeedbackModalApi.setData({}).open(); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { schema: useGridFormSchema() }, |
| | | gridOptions: { |
| | |
| | | </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 |
| | |
| | | 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, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | 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, |
| | | }); |
| | | |
| | |
| | | /** æäº§ */ |
| | | 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('æäº§å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | /** æå¼çç¹å¾ç¼è¾é¡µé¢ */ |
| | |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height><ScheduleModal @success="handleRefresh" /> |
| | | <Page auto-content-height> |
| | | <ScheduleModal @success="handleRefresh" /> |
| | | <SchedulePreviewModal @success="handleRefresh" /> |
| | | |
| | | <!-- æäº§çç¹å¾é¢è§ --> |
| | | <Card title="æäº§çç¹å¾" class="mb-4"> |
| | |
| | | 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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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' }, |
| | | }, |
| | | ]; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| | |
| | | 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'; |
| | | |
| | |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | /** 订åå·¥åºå表çåæ®µ */ |
| | | export function useProcessGridColumns(): VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>['columns'] { |
| | | return [ |
| | | { |
| | | 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' }, |
| | | }, |
| | | ]; |
| | | } |
| | |
| | | |
| | | 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"; |
| | | |
| | |
| | | getWorkOrder, |
| | | updateWorkOrder, |
| | | } from "#/api/mes/pro/workorder"; |
| | | import { batchSaveWorkOrderProcess } 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 |
| | | ); |
| | |
| | | } |
| | | }); |
| | | |
| | | // 夿æ¯å¦æ¾ç¤º Tabï¼æ°å¢æ¨¡å¼æå·²æ IDï¼ |
| | | const showTabs = computed(() => isCreateMode.value || formData.value?.id); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { |
| | |
| | | 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 å¹¶æè¡¨åææ§å¶åºé¨æé® */ |
| | |
| | | ); |
| | | } |
| | | |
| | | /** ä» BOM ç©æè¡é¢å¡«å订åï¼å¤ç¨å½å订åä¸ä¸æï¼è¿å
¥æ°å¢æ */ |
| | | /** ä» BOM ç©æè¡é¢å¡«å订å */ |
| | | async function handleGenerateWorkOrder( |
| | | bomRow: MesProWorkOrderBomApi.WorkOrderBom |
| | | ) { |
| | |
| | | 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({ |
| | |
| | | } |
| | | modalApi.lock(); |
| | | try { |
| | | // ç¼è¾ææä¿®æ¹æ¶å
ä¿å |
| | | // 1. ä¿åå·¥åä¿¡æ¯ï¼å¦ææåæ´ï¼ |
| | | if (isEditable.value) { |
| | | const current = JSON.stringify(await formApi.getValues()); |
| | | if (current !== originalSnapshot.value) { |
| | |
| | | originalSnapshot.value = current; |
| | | } |
| | | } |
| | | // 2. æ¹éä¿åå·¥åºï¼å¦ææï¼ |
| | | if (tempProcessList.value.length > 0) { |
| | | const processList = tempProcessList.value.map(item => ({ |
| | | ...item, |
| | | workOrderId: formData.value!.id, |
| | | })); |
| | | await batchSaveWorkOrderProcess(processList); |
| | | } |
| | | // 3. 确认工å |
| | | await confirmWorkOrder(formData.value.id); |
| | | message.success('订å已确认'); |
| | | await modalApi.close(); |
| | |
| | | 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, |
| | |
| | | if (!isOpen) { |
| | | formData.value = undefined; |
| | | originalSnapshot.value = ""; |
| | | currentProductId.value = undefined; |
| | | tempProcessList.value = []; |
| | | return; |
| | | } |
| | | // å è½½æ°æ® |
| | | const data = modalApi.getData<{ |
| | | formType: FormType; |
| | | id?: number; |
| | |
| | | }>(); |
| | | 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, |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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, ref, watch } from 'vue'; |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { MesProWorkOrderStatusEnum } from '@vben/constants'; |
| | | |
| | | 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 [ProcessFormModal, processFormModalApi] = useVbenModal({ |
| | | connectedComponent: ProcessForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | autoResize: true, |
| | | border: true, |
| | | columns: useProcessGridColumns(), |
| | | data: [], |
| | | minHeight: 240, |
| | | pagerConfig: { enabled: false }, |
| | | rowConfig: { isHover: true, keyField: 'processId' }, |
| | | showOverflow: true, |
| | | toolbarConfig: { enabled: false }, |
| | | } as VxeTableGridOptions<MesProWorkOrderProcessApi.WorkOrderProcess>, |
| | | }); |
| | | |
| | | /** å 载工åºå表 */ |
| | | 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); |
| | | } 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 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); |
| | | gridApi.setGridOptions({ data: list.value }); |
| | | emit('processListChange', list.value); |
| | | } |
| | | } else { |
| | | // ç¼è¾æ¨¡å¼ï¼è°ç¨æ¥å£å é¤ |
| | | deleteWorkOrderProcess(row.id!).then(() => { |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.processName])); |
| | | getList(); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | /** æ°å¢/ç¼è¾æååè° */ |
| | | 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); |
| | | } else { |
| | | getList(); |
| | | } |
| | | } |
| | | |
| | | // çå¬äº§å/å·¥ååå |
| | | watch( |
| | | () => [props.productId, props.workOrderId], |
| | | () => { |
| | | getList(); |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <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 #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> |