| | |
| | | export function approveFeedback(id: number) { |
| | | return requestClient.put<boolean>(`/mes/pro/feedback/approve?id=${id}`); |
| | | } |
| | | |
| | | /** æä»»å¡æ¥è¯¢æ¥å·¥åå² */ |
| | | export function getFeedbackListByTask(taskId: number) { |
| | | return requestClient.get<MesProFeedbackApi.Feedback[]>( |
| | | `/mes/pro/feedback/list-by-task?taskId=${taskId}`, |
| | | ); |
| | | } |
| | |
| | | statuses?: number[]; |
| | | status?: number; |
| | | } |
| | | |
| | | /** å¾
æ¥å·¥å°è´¦ä»»å¡ï¼pending-feedback-page ååºï¼ */ |
| | | export interface PendingFeedbackTask extends Task { |
| | | pendingQuantity?: number; // å¾
æ¥å·¥æ°éï¼= æäº§ - å·²çäº§ï¼ |
| | | inTransitQuantity?: number; // å¨éæ¥å·¥æ°éï¼è稿/审æ¹ä¸/å¾
æ£éªåè®¡ï¼ |
| | | reportableQuantity?: number; // 坿¥å·¥æ°éï¼= æäº§ - å·²ç产 - å¨éï¼ |
| | | feedbackRate?: number; // æ¥å·¥å®æç(%) |
| | | } |
| | | |
| | | /** å¾
æ¥å·¥å°è´¦å页æ¥è¯¢åæ° */ |
| | | export interface PendingFeedbackPageParams extends PageParam { |
| | | processId?: number; |
| | | routeId?: number; |
| | | workOrderId?: number; |
| | | workstationId?: number; |
| | | code?: string; |
| | | name?: string; |
| | | checkFlag?: boolean; |
| | | createTime?: string[]; |
| | | } |
| | | } |
| | | |
| | | /** æ¥è¯¢ç产任å¡å页 */ |
| | |
| | | export function getGanttTaskList(params: any) { |
| | | return requestClient.get<any[]>('/mes/pro/task/gantt-list', { params }); |
| | | } |
| | | |
| | | /** æ¥è¯¢å¾
æ¥å·¥å°è´¦ï¼åé¡µï¼ */ |
| | | export function getPendingFeedbackPage( |
| | | params: MesProTaskApi.PendingFeedbackPageParams, |
| | | ) { |
| | | return requestClient.get<PageResult<MesProTaskApi.PendingFeedbackTask>>( |
| | | '/mes/pro/task/pending-feedback-page', |
| | | { params }, |
| | | ); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export namespace MesProWorkOrderParamRecordApi { |
| | | /** åæ°è®°å½æç» */ |
| | | export interface ParamRecordDetail { |
| | | id?: number; |
| | | templateDetailId?: number; |
| | | paramCode?: string; |
| | | paramName?: string; |
| | | paramType?: number; |
| | | unitMeasureId?: number; |
| | | unitMeasureName?: string; |
| | | paramValue?: string; |
| | | remark?: string; |
| | | } |
| | | |
| | | /** å·¥ååæ°è®°å½ */ |
| | | export interface WorkOrderParamRecord { |
| | | id?: number; |
| | | workOrderId?: number; |
| | | templateId?: number; |
| | | templateCode?: string; |
| | | templateName?: string; |
| | | recordNo?: string; |
| | | remark?: string; |
| | | createTime?: string; |
| | | details?: ParamRecordDetail[]; |
| | | } |
| | | |
| | | /** å建/æ´æ°è¯·æ±åæ° */ |
| | | export interface SaveParamRecord { |
| | | id?: number; |
| | | templateId?: number; |
| | | recordNo?: string; |
| | | remark?: string; |
| | | details: { |
| | | templateDetailId: number; |
| | | paramValue?: string; |
| | | }[]; |
| | | } |
| | | } |
| | | |
| | | /** æå·¥åå页æ¥è¯¢åæ°è®°å½ */ |
| | | export function getWorkOrderParamRecordPage( |
| | | params: PageParam & { workOrderId: number; recordNo?: string; templateName?: string; createTime?: string[] }, |
| | | ) { |
| | | return requestClient.get<PageResult<MesProWorkOrderParamRecordApi.WorkOrderParamRecord>>( |
| | | '/mes/pro/work-order-param-record/page-by-work-order', |
| | | { params }, |
| | | ); |
| | | } |
| | | |
| | | /** è·ååæ°è®°å½è¯¦æ
*/ |
| | | export function getWorkOrderParamRecord(id: number) { |
| | | return requestClient.get<MesProWorkOrderParamRecordApi.WorkOrderParamRecord>( |
| | | `/mes/pro/work-order-param-record/get?id=${id}`, |
| | | ); |
| | | } |
| | | |
| | | /** åå»ºåæ°è®°å½ */ |
| | | export function createWorkOrderParamRecord( |
| | | data: MesProWorkOrderParamRecordApi.SaveParamRecord, |
| | | workOrderId: number, |
| | | ) { |
| | | return requestClient.post<number>( |
| | | `/mes/pro/work-order-param-record/create?workOrderId=${workOrderId}`, |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** æ´æ°åæ°è®°å½ */ |
| | | export function updateWorkOrderParamRecord( |
| | | data: MesProWorkOrderParamRecordApi.SaveParamRecord, |
| | | ) { |
| | | return requestClient.put<boolean>( |
| | | '/mes/pro/work-order-param-record/update', |
| | | data, |
| | | ); |
| | | } |
| | | |
| | | /** å é¤åæ°è®°å½ */ |
| | | export function deleteWorkOrderParamRecord(id: number) { |
| | | return requestClient.delete<boolean>( |
| | | `/mes/pro/work-order-param-record/delete?id=${id}`, |
| | | ); |
| | | } |
| | |
| | | /> |
| | | </div> |
| | | </template> --> |
| | | <template #header-right-170> |
| | | <button |
| | | type="button" |
| | | class="flex cursor-pointer items-center gap-1.5 rounded-md border-none bg-primary/15 px-3 py-1.5 text-sm font-medium text-primary transition-colors hover:bg-primary/25" |
| | | @click="router.push({ name: 'MesWorkbench' })" |
| | | > |
| | | <IconifyIcon icon="lucide:factory" class="size-4" /> |
| | | <span>å·¥ä½å°</span> |
| | | </button> |
| | | </template> |
| | | <template #extra> |
| | | <AuthenticationLoginExpiredModal |
| | | v-model:open="accessStore.loginExpired" |
| | |
| | | ], |
| | | }, |
| | | /** |
| | | * ç产工ä½å° - å
¨å±ç¬ç«é¡µé¢ï¼ä¸ä½¿ç¨ BasicLayout |
| | | */ |
| | | { |
| | | component: () => import('#/views/mes/workbench/index.vue'), |
| | | meta: { |
| | | hideInBreadcrumb: true, |
| | | hideInMenu: true, |
| | | hideInTab: true, |
| | | ignoreAccess: true, |
| | | title: 'ç产工ä½å°', |
| | | }, |
| | | name: 'MesWorkbench', |
| | | path: '/mes/workbench', |
| | | }, |
| | | /** |
| | | * ç¨äº bpm ç§»å¨ç«¯æµç¨è¡¨å web-view çåµå
¥ |
| | | */ |
| | | { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { PendingTask } from '../../types'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { Tag } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'WorkbenchTaskListPanel' }); |
| | | |
| | | const props = defineProps<{ |
| | | list: PendingTask[]; |
| | | selectedId?: number; |
| | | loading?: boolean; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | select: [task: PendingTask]; |
| | | }>(); |
| | | |
| | | const searchKeyword = ref(''); |
| | | |
| | | const filteredList = computed(() => { |
| | | const kw = searchKeyword.value.trim().toLowerCase(); |
| | | if (!kw) return props.list; |
| | | return props.list.filter((row) => { |
| | | const fields = [ |
| | | row.code, |
| | | row.workOrderCode, |
| | | row.workOrderName, |
| | | row.itemCode, |
| | | row.itemName, |
| | | row.processName, |
| | | ]; |
| | | return fields.some((f) => String(f ?? '').toLowerCase().includes(kw)); |
| | | }); |
| | | }); |
| | | |
| | | function getProgressColor(rate?: number): string { |
| | | if (rate === undefined || rate === null) return '#1890ff'; |
| | | if (rate >= 100) return '#52c41a'; |
| | | if (rate >= 60) return '#1890ff'; |
| | | return '#faad14'; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <section class="task-list-panel"> |
| | | <div class="task-list-panel__head"> |
| | | <span class="task-list-panel__title">å·¥åå表</span> |
| | | <Tag color="blue">{{ list.length }}</Tag> |
| | | </div> |
| | | |
| | | <div class="task-list-panel__search"> |
| | | <input |
| | | v-model="searchKeyword" |
| | | class="task-list-panel__input" |
| | | placeholder="订å/产å/å·¥åº" |
| | | /> |
| | | </div> |
| | | |
| | | <div v-if="loading" class="task-list-panel__empty">å è½½ä¸...</div> |
| | | <div v-else-if="!list.length" class="task-list-panel__empty"> |
| | | ææ å¾
æ¥å·¥ä»»å¡ |
| | | </div> |
| | | <div v-else-if="!filteredList.length" class="task-list-panel__empty"> |
| | | æ å¹é
å·¥å |
| | | </div> |
| | | |
| | | <div v-else class="task-list-panel__scroll"> |
| | | <div |
| | | v-for="item in filteredList" |
| | | :key="item.id" |
| | | class="task-card" |
| | | :class="{ 'task-card--active': item.id === selectedId }" |
| | | @click="emit('select', item)" |
| | | > |
| | | <div class="task-card__accent" /> |
| | | <div class="task-card__inner"> |
| | | <div class="task-card__header"> |
| | | <span class="task-card__code">{{ item.code }}</span> |
| | | <Tag |
| | | v-if="item.checkFlag" |
| | | color="orange" |
| | | class="task-card__check-tag" |
| | | > |
| | | è´¨æ£ |
| | | </Tag> |
| | | <span |
| | | class="task-card__rate" |
| | | :style="{ color: getProgressColor(item.feedbackRate) }" |
| | | > |
| | | {{ item.feedbackRate ?? 0 }}% |
| | | </span> |
| | | </div> |
| | | |
| | | <div class="task-card__progress-row"> |
| | | <span class="task-card__progress-label">æ¥å·¥è¿åº¦</span> |
| | | <span class="task-card__progress-qty"> |
| | | {{ item.producedQuantity ?? 0 }} / {{ item.quantity ?? 0 }} |
| | | </span> |
| | | </div> |
| | | <div class="task-card__progress-bar"> |
| | | <div |
| | | class="task-card__progress-fill" |
| | | :style="{ |
| | | width: `${Math.min(item.feedbackRate ?? 0, 100)}%`, |
| | | backgroundColor: getProgressColor(item.feedbackRate), |
| | | }" |
| | | /> |
| | | </div> |
| | | |
| | | <div class="task-card__info"> |
| | | <div class="task-card__info-row"> |
| | | <span class="task-card__info-label">订å</span> |
| | | <span class="task-card__info-value"> |
| | | {{ item.workOrderCode }} {{ item.workOrderName }} |
| | | </span> |
| | | </div> |
| | | <div class="task-card__info-row"> |
| | | <span class="task-card__info-label">å·¥åº</span> |
| | | <span class="task-card__info-value">{{ item.processName }}</span> |
| | | </div> |
| | | <div class="task-card__info-row"> |
| | | <span class="task-card__info-label">产å</span> |
| | | <span class="task-card__info-value" :title="item.itemName"> |
| | | {{ item.itemCode }} {{ item.itemName }} |
| | | </span> |
| | | </div> |
| | | <div class="task-card__info-row"> |
| | | <span class="task-card__info-label">坿¥å·¥</span> |
| | | <span class="task-card__info-value task-card__info-value--highlight"> |
| | | {{ item.reportableQuantity ?? 0 }} {{ item.unitMeasureName }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .task-list-panel { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | border: 1px solid #f0f0f0; |
| | | box-shadow: 0 1px 4px rgb(0 0 0 / 6%); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .task-list-panel__head { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 12px 14px; |
| | | flex-shrink: 0; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .task-list-panel__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .task-list-panel__search { |
| | | flex-shrink: 0; |
| | | padding: 10px 14px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .task-list-panel__input { |
| | | width: 100%; |
| | | height: 36px; |
| | | padding: 0 12px; |
| | | border: 1px solid #d9d9d9; |
| | | border-radius: 6px; |
| | | font-size: 14px; |
| | | outline: none; |
| | | transition: border-color 0.2s; |
| | | |
| | | &:focus { |
| | | border-color: var(--ant-primary-color, #1890ff); |
| | | box-shadow: 0 0 0 2px rgb(24 144 255 / 10%); |
| | | } |
| | | |
| | | &::placeholder { |
| | | color: #bfbfbf; |
| | | } |
| | | } |
| | | |
| | | .task-list-panel__empty { |
| | | padding: 32px 0; |
| | | text-align: center; |
| | | color: #8c8c8c; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .task-list-panel__scroll { |
| | | flex: 1; |
| | | min-height: 0; |
| | | overflow-y: auto; |
| | | padding: 10px; |
| | | } |
| | | |
| | | .task-card { |
| | | position: relative; |
| | | margin-bottom: 10px; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | border: 1px solid #f0f0f0; |
| | | background: #fff; |
| | | transition: all 0.15s ease; |
| | | |
| | | &:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | &:hover { |
| | | border-color: var(--ant-primary-color, #1890ff); |
| | | } |
| | | |
| | | &--active { |
| | | border-color: var(--ant-primary-color, #1890ff); |
| | | box-shadow: 0 1px 8px rgb(24 144 255 / 15%); |
| | | |
| | | .task-card__accent { |
| | | opacity: 1; |
| | | } |
| | | |
| | | .task-card__inner { |
| | | background: #f0f7ff; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .task-card__accent { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 4px; |
| | | background: var(--ant-primary-color, #1890ff); |
| | | opacity: 0; |
| | | border-radius: 8px 0 0 8px; |
| | | } |
| | | |
| | | .task-card__inner { |
| | | padding: 12px 14px 12px 16px; |
| | | } |
| | | |
| | | .task-card__header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .task-card__code { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-weight: 600; |
| | | font-size: 14px; |
| | | color: var(--ant-primary-color, #1890ff); |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .task-card__check-tag { |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .task-card__rate { |
| | | flex-shrink: 0; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | .task-card__progress-row { |
| | | display: flex; |
| | | align-items: baseline; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | margin-bottom: 6px; |
| | | } |
| | | |
| | | .task-card__progress-label { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .task-card__progress-qty { |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | font-variant-numeric: tabular-nums; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .task-card__progress-bar { |
| | | height: 8px; |
| | | background: #f0f0f0; |
| | | border-radius: 4px; |
| | | overflow: hidden; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .task-card__progress-fill { |
| | | height: 100%; |
| | | border-radius: 4px; |
| | | transition: width 0.3s ease; |
| | | } |
| | | |
| | | .task-card__info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .task-card__info-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 13px; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .task-card__info-label { |
| | | flex-shrink: 0; |
| | | width: 48px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .task-card__info-value { |
| | | flex: 1; |
| | | min-width: 0; |
| | | color: #1a1a1a; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | |
| | | &--highlight { |
| | | color: var(--ant-primary-color, #1890ff); |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { DICT_TYPE } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { Input, Modal, Spin, Tag } from 'ant-design-vue'; |
| | | |
| | | import { getTemplatePage } from '#/api/mes/pro/processdesign/template'; |
| | | |
| | | defineOptions({ name: 'WorkbenchTemplateSelectDialog' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | select: [template: MesPdTemplateApi.Template]; |
| | | }>(); |
| | | |
| | | const visible = ref(false); |
| | | const loading = ref(false); |
| | | const templates = ref<MesPdTemplateApi.Template[]>([]); |
| | | const searchKeyword = ref(''); |
| | | |
| | | /** æå¼å¼¹çª */ |
| | | async function open() { |
| | | visible.value = true; |
| | | searchKeyword.value = ''; |
| | | await loadTemplates(); |
| | | } |
| | | |
| | | /** å 载模æ¿å表 */ |
| | | async function loadTemplates() { |
| | | loading.value = true; |
| | | try { |
| | | const result = await getTemplatePage({ pageNo: 1, pageSize: 200 }); |
| | | templates.value = result.list ?? []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** è¿æ»¤åçæ¨¡æ¿ */ |
| | | const filtered = computed(() => { |
| | | const kw = searchKeyword.value.trim().toLowerCase(); |
| | | if (!kw) return templates.value; |
| | | return templates.value.filter((t) => { |
| | | const fields = [t.templateCode, t.templateName, t.remark]; |
| | | return fields.some((f) => String(f ?? '').toLowerCase().includes(kw)); |
| | | }); |
| | | }); |
| | | |
| | | /** 模æ¿ç±»ååå
¸ */ |
| | | const templateTypeOptions = getDictOptions(DICT_TYPE.MES_PD_TEMPLATE_TYPE, 'number') ?? []; |
| | | |
| | | function getTemplateTypeLabel(type?: number) { |
| | | const opt = templateTypeOptions.find((o: any) => o.value === type); |
| | | return opt?.label ?? ''; |
| | | } |
| | | |
| | | function getTemplateTypeColor(type?: number) { |
| | | const colors: Record<number, string> = { 1: 'blue', 2: 'green', 3: 'orange' }; |
| | | return type ? (colors[type] ?? 'default') : 'default'; |
| | | } |
| | | |
| | | /** éæ©æ¨¡æ¿ */ |
| | | function handleSelect(t: MesPdTemplateApi.Template) { |
| | | emit('select', t); |
| | | visible.value = false; |
| | | } |
| | | |
| | | defineExpose({ open }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="visible" |
| | | title="鿩工èºåæ°æ¨¡æ¿" |
| | | :footer="null" |
| | | width="900px" |
| | | :body-style="{ padding: '16px 20px', maxHeight: '70vh', overflow: 'auto' }" |
| | | centered |
| | | destroy-on-close |
| | | > |
| | | <!-- æç´¢æ¡ --> |
| | | <div class="tpl-select__search"> |
| | | <Input |
| | | v-model:value="searchKeyword" |
| | | placeholder="æç´¢æ¨¡æ¿ç¼ç /åç§°" |
| | | allow-clear |
| | | > |
| | | <template #prefix> |
| | | <IconifyIcon icon="lucide:search" class="text-gray-400" /> |
| | | </template> |
| | | </Input> |
| | | </div> |
| | | |
| | | <Spin :spinning="loading"> |
| | | <!-- ç©ºç¶æ --> |
| | | <div v-if="!filtered.length && !loading" class="tpl-select__empty"> |
| | | <IconifyIcon icon="lucide:inbox" class="tpl-select__empty-icon" /> |
| | | <p>ææ å·¥èºåæ°æ¨¡æ¿</p> |
| | | </div> |
| | | |
| | | <!-- å¡çç½æ ¼ --> |
| | | <div v-else class="tpl-select__grid"> |
| | | <button |
| | | v-for="t in filtered" |
| | | :key="t.id" |
| | | type="button" |
| | | class="tpl-card" |
| | | @click="handleSelect(t)" |
| | | > |
| | | <div class="tpl-card__head"> |
| | | <span class="tpl-card__code">{{ t.templateCode }}</span> |
| | | <Tag v-if="t.templateType" :color="getTemplateTypeColor(t.templateType)"> |
| | | {{ getTemplateTypeLabel(t.templateType) }} |
| | | </Tag> |
| | | </div> |
| | | <div class="tpl-card__name">{{ t.templateName }}</div> |
| | | <div v-if="t.details?.length" class="tpl-card__params"> |
| | | <IconifyIcon icon="lucide:settings-2" class="tpl-card__params-icon" /> |
| | | <span>{{ t.details.length }} ä¸ªåæ°</span> |
| | | </div> |
| | | <div v-if="t.remark" class="tpl-card__remark">{{ t.remark }}</div> |
| | | <div class="tpl-card__action"> |
| | | <IconifyIcon icon="lucide:pointer" /> |
| | | ç¹å»éæ© |
| | | </div> |
| | | </button> |
| | | </div> |
| | | </Spin> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .tpl-select__search { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .tpl-select__empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 48px 0; |
| | | color: #8c8c8c; |
| | | |
| | | p { |
| | | margin: 0; |
| | | font-size: 15px; |
| | | } |
| | | } |
| | | |
| | | .tpl-select__empty-icon { |
| | | font-size: 40px; |
| | | opacity: 0.5; |
| | | } |
| | | |
| | | .tpl-select__grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .tpl-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | padding: 16px; |
| | | border: 2px solid #e8e8e8; |
| | | border-radius: 12px; |
| | | text-align: left; |
| | | cursor: pointer; |
| | | background: linear-gradient( |
| | | 145deg, |
| | | #f0fdf4 0%, |
| | | #dcfce7 52%, |
| | | #bbf7d0 100% |
| | | ); |
| | | transition: |
| | | transform 0.18s ease, |
| | | box-shadow 0.18s ease, |
| | | border-color 0.18s ease; |
| | | |
| | | &:hover { |
| | | transform: translateY(-2px); |
| | | border-color: #22c55e; |
| | | box-shadow: 0 8px 24px rgb(34 197 94 / 20%); |
| | | } |
| | | |
| | | &:active { |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | .tpl-card__head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .tpl-card__code { |
| | | font-size: 18px; |
| | | font-weight: 800; |
| | | letter-spacing: 0.03em; |
| | | color: #166534; |
| | | line-height: 1.1; |
| | | } |
| | | |
| | | .tpl-card__name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #14532d; |
| | | line-height: 1.3; |
| | | } |
| | | |
| | | .tpl-card__params { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 13px; |
| | | color: #475569; |
| | | } |
| | | |
| | | .tpl-card__params-icon { |
| | | font-size: 14px; |
| | | color: #64748b; |
| | | } |
| | | |
| | | .tpl-card__remark { |
| | | font-size: 12px; |
| | | color: #6b7280; |
| | | line-height: 1.4; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | display: -webkit-box; |
| | | -webkit-line-clamp: 2; |
| | | -webkit-box-orient: vertical; |
| | | } |
| | | |
| | | .tpl-card__action { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | padding: 8px; |
| | | border-radius: 8px; |
| | | background: rgb(34 197 94 / 12%); |
| | | border: 1px solid rgb(34 197 94 / 20%); |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #16a34a; |
| | | transition: all 0.2s; |
| | | margin-top: auto; |
| | | |
| | | .tpl-card:hover & { |
| | | background: rgb(34 197 94 / 20%); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | defineOptions({ name: 'WorkbenchWorkstationPanel' }); |
| | | |
| | | defineProps<{ |
| | | workstation?: MesMdWorkstationApi.Workstation; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | change: []; |
| | | }>(); |
| | | </script> |
| | | |
| | | <template> |
| | | <section |
| | | class="ws-panel" |
| | | :class="{ 'ws-panel--empty': !workstation }" |
| | | > |
| | | <div class="ws-panel__head"> |
| | | <div class="ws-panel__title-row"> |
| | | <IconifyIcon icon="lucide:map-pin" class="ws-panel__icon" /> |
| | | <span class="ws-panel__title">å·¥ä½ç«</span> |
| | | </div> |
| | | <button |
| | | v-if="workstation" |
| | | type="button" |
| | | class="ws-panel__switch" |
| | | @click="emit('change')" |
| | | > |
| | | 忢 |
| | | </button> |
| | | </div> |
| | | |
| | | <!-- å·²éå·¥ä½ç« --> |
| | | <div v-if="workstation" class="ws-panel__device"> |
| | | <span class="ws-panel__code">{{ workstation.code }}</span> |
| | | <span class="ws-panel__name">{{ workstation.name }}</span> |
| | | <span v-if="workstation.processName" class="ws-panel__process"> |
| | | {{ workstation.processName }} |
| | | </span> |
| | | </div> |
| | | |
| | | <!-- æªéå·¥ä½ç« --> |
| | | <div v-else class="ws-panel__placeholder"> |
| | | <IconifyIcon icon="lucide:monitor" class="ws-panel__placeholder-icon" /> |
| | | <p>å°æªéæ©å·¥ä½ç«</p> |
| | | <button type="button" class="ws-panel__select-btn" @click="emit('change')"> |
| | | 鿩工ä½ç« |
| | | </button> |
| | | </div> |
| | | |
| | | <div v-if="workstation" class="ws-panel__meta"> |
| | | <div class="ws-panel__meta-item"> |
| | | <span class="ws-panel__meta-label">车é´</span> |
| | | <span class="ws-panel__meta-value">{{ workstation.workshopName ?? 'â' }}</span> |
| | | </div> |
| | | <div class="ws-panel__meta-item"> |
| | | <span class="ws-panel__meta-label">å°ç¹</span> |
| | | <span class="ws-panel__meta-value">{{ workstation.address ?? 'â' }}</span> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | $radius: 10px; |
| | | $shadow: 0 2px 12px rgb(0 0 0 / 8%); |
| | | |
| | | .ws-panel { |
| | | flex-shrink: 0; |
| | | padding: 14px 16px; |
| | | border-radius: $radius; |
| | | color: #fff; |
| | | background: linear-gradient( |
| | | 128deg, |
| | | #3b82f6 0%, |
| | | #2563eb 38%, |
| | | #1d4ed8 100% |
| | | ); |
| | | box-shadow: $shadow; |
| | | border: 1px solid rgb(255 255 255 / 14%); |
| | | |
| | | &--empty { |
| | | .ws-panel__meta { |
| | | opacity: 0.72; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .ws-panel__head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .ws-panel__title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .ws-panel__icon { |
| | | font-size: 20px; |
| | | opacity: 0.95; |
| | | } |
| | | |
| | | .ws-panel__title { |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | letter-spacing: 0.02em; |
| | | } |
| | | |
| | | .ws-panel__switch { |
| | | flex-shrink: 0; |
| | | padding: 6px 14px; |
| | | border: 1px solid rgb(255 255 255 / 36%); |
| | | border-radius: 8px; |
| | | background: rgb(255 255 255 / 14%); |
| | | color: #fff; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | transition: background 0.2s; |
| | | |
| | | &:hover { |
| | | background: rgb(255 255 255 / 24%); |
| | | } |
| | | } |
| | | |
| | | .ws-panel__device { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | padding: 12px 16px; |
| | | border-radius: 12px; |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgb(255 255 255 / 24%) 0%, |
| | | rgb(255 255 255 / 10%) 100% |
| | | ); |
| | | border: 2px solid rgb(255 255 255 / 28%); |
| | | box-shadow: |
| | | inset 0 1px 0 rgb(255 255 255 / 22%), |
| | | 0 8px 20px rgb(0 0 0 / 12%); |
| | | } |
| | | |
| | | .ws-panel__code { |
| | | flex-shrink: 0; |
| | | font-size: 28px; |
| | | font-weight: 800; |
| | | letter-spacing: 0.04em; |
| | | line-height: 1; |
| | | color: #fff176; |
| | | text-shadow: |
| | | 0 1px 2px rgb(0 0 0 / 35%), |
| | | 0 0 12px rgb(255 241 118 / 45%); |
| | | } |
| | | |
| | | .ws-panel__name { |
| | | flex: 1; |
| | | min-width: 0; |
| | | font-size: 17px; |
| | | font-weight: 700; |
| | | line-height: 1.2; |
| | | color: #fff9c4; |
| | | text-shadow: 0 1px 3px rgb(0 0 0 / 22%); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .ws-panel__process { |
| | | flex-shrink: 0; |
| | | padding: 4px 12px; |
| | | border-radius: 999px; |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | background: rgb(255 193 7 / 92%); |
| | | color: #5c3d00; |
| | | box-shadow: 0 2px 8px rgb(0 0 0 / 12%); |
| | | } |
| | | |
| | | .ws-panel__placeholder { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | padding: 18px 12px; |
| | | border-radius: 12px; |
| | | background: rgb(0 0 0 / 12%); |
| | | border: 1px dashed rgb(255 255 255 / 28%); |
| | | text-align: center; |
| | | |
| | | p { |
| | | margin: 0; |
| | | font-size: 15px; |
| | | opacity: 0.92; |
| | | } |
| | | } |
| | | |
| | | .ws-panel__placeholder-icon { |
| | | font-size: 32px; |
| | | opacity: 0.85; |
| | | } |
| | | |
| | | .ws-panel__select-btn { |
| | | padding: 10px 24px; |
| | | border: 1px solid rgb(255 255 255 / 40%); |
| | | border-radius: 8px; |
| | | background: rgb(255 255 255 / 18%); |
| | | color: #fff; |
| | | font-size: 15px; |
| | | font-weight: 700; |
| | | cursor: pointer; |
| | | transition: all 0.2s; |
| | | min-height: 44px; |
| | | |
| | | &:hover { |
| | | background: rgb(255 255 255 / 28%); |
| | | border-color: rgb(255 255 255 / 55%); |
| | | } |
| | | } |
| | | |
| | | .ws-panel__meta { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .ws-panel__meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | padding: 8px 12px; |
| | | border-radius: 10px; |
| | | background: rgb(255 255 255 / 16%); |
| | | border: 1px solid rgb(255 255 255 / 22%); |
| | | } |
| | | |
| | | .ws-panel__meta-label { |
| | | flex-shrink: 0; |
| | | font-size: 13px; |
| | | font-weight: 600; |
| | | opacity: 0.88; |
| | | } |
| | | |
| | | .ws-panel__meta-value { |
| | | min-width: 0; |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | font-variant-numeric: tabular-nums; |
| | | color: #fff9c4; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Input, Modal, Spin } from 'ant-design-vue'; |
| | | |
| | | import { getWorkstationPage } from '#/api/mes/md/workstation'; |
| | | |
| | | defineOptions({ name: 'WorkbenchWorkstationSelectDialog' }); |
| | | |
| | | const emit = defineEmits<{ |
| | | select: [workstation: MesMdWorkstationApi.Workstation]; |
| | | }>(); |
| | | |
| | | const visible = ref(false); |
| | | const loading = ref(false); |
| | | const workstations = ref<MesMdWorkstationApi.Workstation[]>([]); |
| | | const searchKeyword = ref(''); |
| | | |
| | | /** æå¼å¼¹çª */ |
| | | async function open() { |
| | | visible.value = true; |
| | | searchKeyword.value = ''; |
| | | await loadWorkstations(); |
| | | } |
| | | |
| | | /** å 载工ä½ç«å表 */ |
| | | async function loadWorkstations() { |
| | | loading.value = true; |
| | | try { |
| | | const result = await getWorkstationPage({ pageNo: 1, pageSize: 200 }); |
| | | workstations.value = result.list ?? []; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | |
| | | /** è¿æ»¤åçå·¥ä½ç« */ |
| | | const filtered = computed(() => { |
| | | const kw = searchKeyword.value.trim().toLowerCase(); |
| | | if (!kw) return workstations.value; |
| | | return workstations.value.filter((ws) => { |
| | | const fields = [ws.code, ws.name, ws.processName, ws.workshopName, ws.address]; |
| | | return fields.some((f) => String(f ?? '').toLowerCase().includes(kw)); |
| | | }); |
| | | }); |
| | | |
| | | /** 鿩工ä½ç« */ |
| | | function handleSelect(ws: MesMdWorkstationApi.Workstation) { |
| | | emit('select', ws); |
| | | visible.value = false; |
| | | } |
| | | |
| | | defineExpose({ open }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal |
| | | v-model:open="visible" |
| | | title="鿩工ä½ç«" |
| | | :footer="null" |
| | | width="900px" |
| | | :body-style="{ padding: '16px 20px', maxHeight: '70vh', overflow: 'auto' }" |
| | | centered |
| | | destroy-on-close |
| | | > |
| | | <!-- æç´¢æ¡ --> |
| | | <div class="ws-select__search"> |
| | | <Input |
| | | v-model:value="searchKeyword" |
| | | placeholder="æç´¢å·¥ä½ç«ç¼ç /åç§°/å·¥åº/车é´" |
| | | allow-clear |
| | | > |
| | | <template #prefix> |
| | | <IconifyIcon icon="lucide:search" class="text-gray-400" /> |
| | | </template> |
| | | </Input> |
| | | </div> |
| | | |
| | | <Spin :spinning="loading"> |
| | | <!-- ç©ºç¶æ --> |
| | | <div v-if="!filtered.length && !loading" class="ws-select__empty"> |
| | | <IconifyIcon icon="lucide:inbox" class="ws-select__empty-icon" /> |
| | | <p>ææ å·¥ä½ç«æ°æ®</p> |
| | | </div> |
| | | |
| | | <!-- å¡çç½æ ¼ --> |
| | | <div v-else class="ws-select__grid"> |
| | | <button |
| | | v-for="ws in filtered" |
| | | :key="ws.id" |
| | | type="button" |
| | | class="ws-card" |
| | | @click="handleSelect(ws)" |
| | | > |
| | | <div class="ws-card__head"> |
| | | <span class="ws-card__code">{{ ws.code }}</span> |
| | | <span v-if="ws.processName" class="ws-card__process"> |
| | | {{ ws.processName }} |
| | | </span> |
| | | </div> |
| | | <div class="ws-card__name">{{ ws.name }}</div> |
| | | <div class="ws-card__info"> |
| | | <div v-if="ws.workshopName" class="ws-card__info-row"> |
| | | <IconifyIcon icon="lucide:building-2" class="ws-card__info-icon" /> |
| | | <span>{{ ws.workshopName }}</span> |
| | | </div> |
| | | <div v-if="ws.address" class="ws-card__info-row"> |
| | | <IconifyIcon icon="lucide:map-pin" class="ws-card__info-icon" /> |
| | | <span>{{ ws.address }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="ws-card__action"> |
| | | <IconifyIcon icon="lucide:pointer" /> |
| | | ç¹å»éæ© |
| | | </div> |
| | | </button> |
| | | </div> |
| | | </Spin> |
| | | </Modal> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .ws-select__search { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .ws-select__empty { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 48px 0; |
| | | color: #8c8c8c; |
| | | |
| | | p { |
| | | margin: 0; |
| | | font-size: 15px; |
| | | } |
| | | } |
| | | |
| | | .ws-select__empty-icon { |
| | | font-size: 40px; |
| | | opacity: 0.5; |
| | | } |
| | | |
| | | .ws-select__grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .ws-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | padding: 16px; |
| | | border: 2px solid #e8e8e8; |
| | | border-radius: 12px; |
| | | text-align: left; |
| | | cursor: pointer; |
| | | background: linear-gradient( |
| | | 145deg, |
| | | #eff6ff 0%, |
| | | #dbeafe 52%, |
| | | #bfdbfe 100% |
| | | ); |
| | | transition: |
| | | transform 0.18s ease, |
| | | box-shadow 0.18s ease, |
| | | border-color 0.18s ease; |
| | | |
| | | &:hover { |
| | | transform: translateY(-2px); |
| | | border-color: #3b82f6; |
| | | box-shadow: 0 8px 24px rgb(59 130 246 / 20%); |
| | | } |
| | | |
| | | &:active { |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | .ws-card__head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .ws-card__code { |
| | | font-size: 22px; |
| | | font-weight: 800; |
| | | letter-spacing: 0.03em; |
| | | color: #1e40af; |
| | | line-height: 1.1; |
| | | } |
| | | |
| | | .ws-card__process { |
| | | flex-shrink: 0; |
| | | padding: 4px 10px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | background: #3b82f6; |
| | | color: #fff; |
| | | } |
| | | |
| | | .ws-card__name { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1e3a5f; |
| | | line-height: 1.3; |
| | | } |
| | | |
| | | .ws-card__info { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | margin-top: auto; |
| | | } |
| | | |
| | | .ws-card__info-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-size: 13px; |
| | | color: #475569; |
| | | } |
| | | |
| | | .ws-card__info-icon { |
| | | font-size: 14px; |
| | | color: #64748b; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .ws-card__action { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 6px; |
| | | padding: 8px; |
| | | border-radius: 8px; |
| | | background: rgb(59 130 246 / 12%); |
| | | border: 1px solid rgb(59 130 246 / 20%); |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #2563eb; |
| | | transition: all 0.2s; |
| | | |
| | | .ws-card:hover & { |
| | | background: rgb(59 130 246 / 20%); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesProFeedbackApi } from '#/api/mes/pro/feedback'; |
| | | |
| | | import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; |
| | | |
| | | import { |
| | | DICT_TYPE, |
| | | MesProFeedbackStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | |
| | | import { Button, message, Popconfirm, Space } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { |
| | | approveFeedback, |
| | | deleteFeedback, |
| | | getFeedbackListByTask, |
| | | rejectFeedback, |
| | | submitFeedback, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import FeedbackForm from '../../../pro/feedback/modules/form.vue'; |
| | | |
| | | defineOptions({ name: 'WorkbenchFeedbackHistoryTab' }); |
| | | |
| | | const props = defineProps<{ |
| | | taskId?: number; |
| | | /** å¤é¨è§¦åå·æ° */ |
| | | refreshKey?: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ reported: [] }>(); |
| | | |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: FeedbackForm, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | /** æ¥ç详æ
*/ |
| | | function handleDetail(row: MesProFeedbackApi.Feedback) { |
| | | formModalApi.setData({ formType: 'detail', id: row.id }).open(); |
| | | } |
| | | |
| | | /** ç¼è¾ï¼ä»
èç¨¿ï¼ */ |
| | | function handleEdit(row: MesProFeedbackApi.Feedback) { |
| | | formModalApi.setData({ formType: 'update', id: row.id }).open(); |
| | | } |
| | | |
| | | /** å é¤ï¼ä»
èç¨¿ï¼ */ |
| | | async function handleDelete(row: MesProFeedbackApi.Feedback) { |
| | | await deleteFeedback(row.id!); |
| | | message.success('å 餿å'); |
| | | refreshGrid(); |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** æäº¤ï¼ä»
èç¨¿ï¼ */ |
| | | async function handleSubmit(row: MesProFeedbackApi.Feedback) { |
| | | await submitFeedback(row.id!); |
| | | message.success('æ¥å·¥åå·²æäº¤'); |
| | | refreshGrid(); |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** 审æ¹éè¿ï¼ä»
审æ¹ä¸ï¼ */ |
| | | async function handleApprove(row: MesProFeedbackApi.Feedback) { |
| | | const finished = await approveFeedback(row.id!); |
| | | message.success( |
| | | finished ? 'æ¥å·¥å已审æ¹å®æ' : 'æ¥å·¥æåï¼è¯·çå¾
è´¨éæ£éªå®æï¼', |
| | | ); |
| | | refreshGrid(); |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** 驳åï¼ä»
审æ¹ä¸ï¼ */ |
| | | async function handleReject(row: MesProFeedbackApi.Feedback) { |
| | | await rejectFeedback(row.id!); |
| | | message.success('æ¥å·¥å已驳å'); |
| | | refreshGrid(); |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** ç¶æå¸¸é便¨¡æ¿ä½¿ç¨ */ |
| | | const STATUS_PREPARE = MesProFeedbackStatusEnum.PREPARE; |
| | | const STATUS_APPROVING = MesProFeedbackStatusEnum.APPROVING; |
| | | |
| | | const gridHeight = ref(300); |
| | | const gridContainerRef = ref<HTMLElement>(); |
| | | let resizeObserver: ResizeObserver | null = null; |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: [ |
| | | { |
| | | field: 'code', |
| | | title: 'æ¥è®¢åå·', |
| | | width: 160, |
| | | slots: { default: 'code' }, |
| | | }, |
| | | { field: 'workOrderCode', title: 'å·¥åç¼ç ', width: 140 }, |
| | | { field: 'processName', title: 'å·¥åº', width: 100 }, |
| | | { field: 'itemName', title: '产å', minWidth: 120 }, |
| | | { field: 'feedbackQuantity', title: 'æ¥å·¥æ°é', width: 100 }, |
| | | { field: 'qualifiedQuantity', title: 'åæ ¼å', width: 90 }, |
| | | { field: 'unqualifiedQuantity', title: 'ä¸è¯å', width: 90 }, |
| | | { field: 'feedbackUserNickname', title: 'æ¥å·¥äºº', width: 100 }, |
| | | { field: 'approveUserNickname', title: 'å®¡æ ¸äºº', width: 100 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_PRO_FEEDBACK_STATUS }, |
| | | }, |
| | | }, |
| | | { |
| | | field: 'feedbackTime', |
| | | title: 'æ¥å·¥æ¶é´', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | title: 'æä½', |
| | | width: 260, |
| | | fixed: 'right', |
| | | slots: { default: 'actions' }, |
| | | }, |
| | | ], |
| | | height: gridHeight.value, |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async () => { |
| | | if (!props.taskId) { |
| | | return []; |
| | | } |
| | | return await getFeedbackListByTask(props.taskId); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { isHover: true, keyField: 'id' }, |
| | | toolbarConfig: { refresh: true }, |
| | | } as VxeTableGridOptions<MesProFeedbackApi.Feedback>, |
| | | }); |
| | | |
| | | function updateGridHeight() { |
| | | if (gridContainerRef.value) { |
| | | const h = gridContainerRef.value.clientHeight; |
| | | if (h > 100 && h !== gridHeight.value) { |
| | | gridHeight.value = h; |
| | | gridApi.setGridOptions({ height: h }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => { |
| | | updateGridHeight(); |
| | | if (gridContainerRef.value) { |
| | | resizeObserver = new ResizeObserver(() => updateGridHeight()); |
| | | resizeObserver.observe(gridContainerRef.value); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | resizeObserver?.disconnect(); |
| | | resizeObserver = null; |
| | | }); |
| | | |
| | | function refreshGrid() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | // çå¬ taskId ååå·æ° |
| | | watch( |
| | | () => props.taskId, |
| | | () => { |
| | | if (props.taskId) { |
| | | gridApi.query(); |
| | | } |
| | | }, |
| | | ); |
| | | |
| | | // çå¬å¤é¨ refreshKey ååå·æ° |
| | | watch( |
| | | () => props.refreshKey, |
| | | () => { |
| | | gridApi.query(); |
| | | }, |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="feedback-history-tab"> |
| | | <FormModal @success="refreshGrid" /> |
| | | |
| | | <div v-if="!taskId" class="feedback-history-tab__empty"> |
| | | 请ä»å·¦ä¾§å·¥ååè¡¨éæ©ä¸æ¡ä»»å¡æ¥çæ¥å·¥è®°å½ |
| | | </div> |
| | | |
| | | <div v-else ref="gridContainerRef" class="feedback-history-tab__grid"> |
| | | <Grid table-title="æ¥å·¥è®°å½"> |
| | | <template #code="{ row }"> |
| | | <Button type="link" @click="handleDetail(row)"> |
| | | {{ row.code }} |
| | | </Button> |
| | | </template> |
| | | <template #actions="{ row }"> |
| | | <Space :size="4" wrap> |
| | | <Button type="link" size="small" @click="handleDetail(row)"> |
| | | {{ $t('common.view') }} |
| | | </Button> |
| | | <!-- è稿ï¼ç¼è¾ãå é¤ãæäº¤ --> |
| | | <Button |
| | | v-if="row.status === STATUS_PREPARE" |
| | | type="link" |
| | | size="small" |
| | | @click="handleEdit(row)" |
| | | > |
| | | {{ $t('common.edit') }} |
| | | </Button> |
| | | <Popconfirm |
| | | v-if="row.status === STATUS_PREPARE" |
| | | title="确认æäº¤è¯¥æ¥å·¥åï¼æäº¤åå°ä¸è½ä¿®æ¹ã" |
| | | @confirm="handleSubmit(row)" |
| | | > |
| | | <Button type="link" size="small"> |
| | | {{ $t('common.submit') }} |
| | | </Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="row.status === STATUS_PREPARE" |
| | | title="确认å é¤è¯¥æ¥å·¥åï¼" |
| | | @confirm="handleDelete(row)" |
| | | > |
| | | <Button type="link" size="small" danger> |
| | | {{ $t('common.delete') }} |
| | | </Button> |
| | | </Popconfirm> |
| | | <!-- 审æ¹ä¸ï¼å®¡æ¹éè¿ã驳å --> |
| | | <Popconfirm |
| | | v-if="row.status === STATUS_APPROVING" |
| | | title="确认审æ¹éè¿è¯¥æ¥å·¥åï¼" |
| | | @confirm="handleApprove(row)" |
| | | > |
| | | <Button type="link" size="small"> |
| | | {{ $t('common.approve') }} |
| | | </Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="row.status === STATUS_APPROVING" |
| | | title="确认驳å该æ¥å·¥åï¼" |
| | | @confirm="handleReject(row)" |
| | | > |
| | | <Button type="link" size="small" danger> |
| | | 驳å |
| | | </Button> |
| | | </Popconfirm> |
| | | </Space> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .feedback-history-tab { |
| | | padding: 16px 20px; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .feedback-history-tab__grid { |
| | | flex: 1; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .feedback-history-tab__empty { |
| | | padding: 64px 0; |
| | | text-align: center; |
| | | color: #8c8c8c; |
| | | font-size: 16px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { PendingTask } from '../../../types'; |
| | | import type { FormType } from '../../../pro/feedback/data'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { |
| | | MesAutoCodeRuleCode, |
| | | MesProFeedbackStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { useUserStore } from '@vben/stores'; |
| | | |
| | | import { Button, message, Popconfirm } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { |
| | | approveFeedback, |
| | | createFeedback, |
| | | getFeedback, |
| | | rejectFeedback, |
| | | submitFeedback, |
| | | updateFeedback, |
| | | } from '#/api/mes/pro/feedback'; |
| | | import { getRouteProcessByRouteAndProcess } from '#/api/mes/pro/route/process'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import { useFormSchema } from '../../../pro/feedback/data'; |
| | | |
| | | defineOptions({ name: 'WorkbenchFeedbackTab' }); |
| | | |
| | | const props = defineProps<{ |
| | | task?: PendingTask | null; |
| | | /** å¤é¨è§¦åå·æ°ï¼å¦æ¥å·¥æååå·æ°åè¡¨ï¼ */ |
| | | refreshKey?: number; |
| | | }>(); |
| | | |
| | | const emit = defineEmits<{ |
| | | reported: []; |
| | | }>(); |
| | | |
| | | const userStore = useUserStore(); |
| | | const formType = ref<FormType>('create'); |
| | | const feedbackId = ref<number>(); |
| | | const feedbackStatus = ref<number>(); |
| | | const originalSnapshot = ref(''); |
| | | |
| | | const isEditable = computed(() => |
| | | ['create', 'submit', 'update'].includes(formType.value), |
| | | ); |
| | | const canSubmit = computed( |
| | | () => |
| | | isEditable.value && |
| | | feedbackStatus.value === MesProFeedbackStatusEnum.PREPARE, |
| | | ); |
| | | const canApprove = computed(() => formType.value === 'approve'); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 110, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-3', |
| | | }); |
| | | |
| | | /** æ ¹æ® checkFlag 坹齿°é */ |
| | | function alignQuantity(data: any) { |
| | | if (data.checkFlag) { |
| | | data.uncheckQuantity = data.feedbackQuantity; |
| | | data.qualifiedQuantity = 0; |
| | | data.unqualifiedQuantity = 0; |
| | | data.laborScrapQuantity = 0; |
| | | data.materialScrapQuantity = 0; |
| | | data.otherScrapQuantity = 0; |
| | | } else { |
| | | data.feedbackQuantity = |
| | | (data.qualifiedQuantity || 0) + (data.unqualifiedQuantity || 0); |
| | | data.uncheckQuantity = 0; |
| | | } |
| | | } |
| | | |
| | | async function getAlignedData() { |
| | | const data = (await formApi.getValues()) as any; |
| | | alignQuantity(data); |
| | | return data; |
| | | } |
| | | |
| | | /** å è½½ checkFlag */ |
| | | async function resolveCheckFlag(routeId?: number, processId?: number) { |
| | | if (!routeId || !processId) return true; |
| | | try { |
| | | const rp = await getRouteProcessByRouteAndProcess(routeId, processId); |
| | | return rp?.checkFlag ?? false; |
| | | } catch { |
| | | return true; |
| | | } |
| | | } |
| | | |
| | | /** ä»»å¡åæ´æ¶é置表å */ |
| | | async function resetForTask(task: PendingTask) { |
| | | formType.value = 'create'; |
| | | feedbackId.value = undefined; |
| | | feedbackStatus.value = undefined; |
| | | originalSnapshot.value = ''; |
| | | |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | |
| | | const code = await generateAutoCode(MesAutoCodeRuleCode.PRO_FEEDBACK_CODE); |
| | | const checkFlag = await resolveCheckFlag(task.routeId, task.processId); |
| | | |
| | | await formApi.setValues({ |
| | | code, |
| | | type: 1, |
| | | feedbackTime: Date.now(), |
| | | feedbackUserId: userStore.userInfo?.id, |
| | | taskId: task.id, |
| | | workOrderId: task.workOrderId, |
| | | routeId: task.routeId, |
| | | processId: task.processId, |
| | | workstationId: task.workstationId, |
| | | itemId: task.itemId, |
| | | itemCode: task.itemCode, |
| | | itemName: task.itemName, |
| | | itemSpecification: task.itemSpecification, |
| | | unitMeasureName: task.unitMeasureName, |
| | | scheduledQuantity: task.quantity, |
| | | checkFlag, |
| | | }); |
| | | |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | } |
| | | |
| | | /** ä¿å */ |
| | | async function handleSave() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | |
| | | const data = await getAlignedData(); |
| | | |
| | | if (formType.value === 'create') { |
| | | const id = await createFeedback(data); |
| | | feedbackId.value = id; |
| | | feedbackStatus.value = MesProFeedbackStatusEnum.PREPARE; |
| | | formType.value = 'update'; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | await formApi.setFieldValue('id', id); |
| | | message.success($t('common.createSuccess')); |
| | | } else { |
| | | await updateFeedback(data); |
| | | message.success($t('common.updateSuccess')); |
| | | } |
| | | originalSnapshot.value = JSON.stringify(data); |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** æäº¤ */ |
| | | async function handleSubmit() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | |
| | | const data = await getAlignedData(); |
| | | if (JSON.stringify(data) !== originalSnapshot.value) { |
| | | await updateFeedback(data); |
| | | originalSnapshot.value = JSON.stringify(data); |
| | | } |
| | | await submitFeedback(feedbackId.value!); |
| | | message.success('æ¥è®¢åå·²æäº¤'); |
| | | // é置表å为æ°å»ºæ¨¡å¼ |
| | | if (props.task) { |
| | | await resetForTask(props.task); |
| | | } |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** 审æ¹éè¿ */ |
| | | async function handleApprove() { |
| | | if (!feedbackId.value) return; |
| | | const finished = await approveFeedback(feedbackId.value); |
| | | message.success( |
| | | finished ? 'æ¥è®¢å已审æ¹å®æ' : 'æ¥å·¥æåï¼è¯·çå¾
è´¨éæ£éªå®æï¼', |
| | | ); |
| | | if (props.task) { |
| | | await resetForTask(props.task); |
| | | } |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** 审æ¹é©³å */ |
| | | async function handleReject() { |
| | | if (!feedbackId.value) return; |
| | | await rejectFeedback(feedbackId.value); |
| | | message.success('æ¥è®¢å已驳å'); |
| | | if (props.task) { |
| | | await resetForTask(props.task); |
| | | } |
| | | emit('reported'); |
| | | } |
| | | |
| | | /** å è½½å·²ææ¥å·¥åè¿è¡ç¼è¾ */ |
| | | async function loadFeedback(id: number) { |
| | | const data = await getFeedback(id); |
| | | feedbackId.value = data.id; |
| | | feedbackStatus.value = data.status; |
| | | formType.value = 'update'; |
| | | formApi.setState({ schema: useFormSchema(formType.value, formApi) }); |
| | | const checkFlag = await resolveCheckFlag(data.routeId, data.processId); |
| | | await formApi.setValues({ ...data, checkFlag }); |
| | | originalSnapshot.value = JSON.stringify(await getAlignedData()); |
| | | } |
| | | |
| | | // çå¬ä»»å¡åæ´ |
| | | watch( |
| | | () => props.task, |
| | | (task) => { |
| | | if (task) { |
| | | resetForTask(task); |
| | | } |
| | | }, |
| | | { immediate: true }, |
| | | ); |
| | | |
| | | // æ´é²æ¹æ³ä¾å¤é¨è°ç¨ |
| | | defineExpose({ loadFeedback, handleSave }); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="feedback-tab"> |
| | | <div v-if="!task" class="feedback-tab__empty"> |
| | | 请ä»å·¦ä¾§å·¥ååè¡¨éæ©ä¸æ¡ä»»å¡ |
| | | </div> |
| | | <template v-else> |
| | | <div class="feedback-tab__header"> |
| | | <h3 class="feedback-tab__title"> |
| | | {{ formType === 'create' ? 'æ°å»ºæ¥å·¥' : 'ç¼è¾æ¥å·¥' }} |
| | | </h3> |
| | | <span v-if="task.workOrderCode" class="feedback-tab__sub"> |
| | | å·¥å: {{ task.workOrderCode }} {{ task.workOrderName }} |
| | | </span> |
| | | </div> |
| | | |
| | | <Form class="feedback-tab__form" /> |
| | | |
| | | <div class="feedback-tab__actions"> |
| | | <Button |
| | | v-if="isEditable" |
| | | type="primary" |
| | | @click="handleSave" |
| | | > |
| | | ä¿å |
| | | </Button> |
| | | <Popconfirm |
| | | v-if="canSubmit" |
| | | title="确认æäº¤è¯¥æ¥è®¢åï¼æäº¤åå°ä¸è½ä¿®æ¹ã" |
| | | @confirm="handleSubmit" |
| | | > |
| | | <Button type="primary">{{ $t('common.submit') }}</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="canApprove" |
| | | title="确认审æ¹éè¿è¯¥æ¥è®¢åï¼" |
| | | @confirm="handleApprove" |
| | | > |
| | | <Button type="primary">éè¿</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="canApprove" |
| | | title="确认驳å该æ¥è®¢åï¼" |
| | | @confirm="handleReject" |
| | | > |
| | | <Button danger>ä¸éè¿</Button> |
| | | </Popconfirm> |
| | | </div> |
| | | </template> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .feedback-tab { |
| | | padding: 20px 24px; |
| | | min-height: 100%; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .feedback-tab__empty { |
| | | padding: 64px 0; |
| | | text-align: center; |
| | | color: #8c8c8c; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .feedback-tab__header { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 16px; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 16px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .feedback-tab__title { |
| | | margin: 0; |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .feedback-tab__sub { |
| | | font-size: 14px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .feedback-tab__form { |
| | | flex: 1; |
| | | } |
| | | |
| | | .feedback-tab__actions { |
| | | display: flex; |
| | | gap: 12px; |
| | | justify-content: flex-end; |
| | | padding-top: 16px; |
| | | border-top: 1px solid #f0f0f0; |
| | | margin-top: 16px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesWmProductIssueApi } from '#/api/mes/wm/productissue'; |
| | | |
| | | import { h, markRaw, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'; |
| | | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmProductIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { Button, Divider, message, Popconfirm, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { |
| | | cancelProductIssue, |
| | | createProductIssue, |
| | | deleteProductIssue, |
| | | getProductIssuePage, |
| | | submitProductIssue, |
| | | updateProductIssue, |
| | | } from '#/api/mes/wm/productissue'; |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | defineOptions({ name: 'WorkbenchProductIssueTab' }); |
| | | |
| | | const props = defineProps<{ |
| | | /** å½åå·¥ä½ç« ID */ |
| | | workstationId?: number; |
| | | /** å½åå·¥ä½ç«åç§° */ |
| | | workstationName?: string; |
| | | }>(); |
| | | |
| | | // --- 表åç¶æ --- |
| | | const editingId = ref<number>(); |
| | | const isCreating = ref(false); |
| | | |
| | | /** æå»ºè¡¨å schema */ |
| | | function buildFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: '颿åç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é¢æåç¼å·' }, |
| | | rules: 'required', |
| | | suffix: () => |
| | | h( |
| | | Button, |
| | | { |
| | | type: 'default', |
| | | size: 'small', |
| | | onClick: async () => { |
| | | const code = await generateAutoCode( |
| | | MesAutoCodeRuleCode.WM_PRODUCT_ISSUE_CODE, |
| | | ); |
| | | await formApi.setFieldValue('code', code); |
| | | }, |
| | | }, |
| | | { default: () => 'çæ' }, |
| | | ), |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: '颿ååç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥é¢æååç§°' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'workOrderId', |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'workstationId', |
| | | label: 'å·¥ä½ç«', |
| | | component: markRaw(MdWorkstationSelect), |
| | | componentProps: { placeholder: 'è¯·éæ©å·¥ä½ç«' }, |
| | | }, |
| | | { |
| | | fieldName: 'requiredTime', |
| | | label: 'éæ±æ¶é´', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©éæ±æ¶é´', |
| | | showTime: true, |
| | | valueFormat: 'x', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2 }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: buildFormSchema(), |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-3', |
| | | }); |
| | | |
| | | /** æ°å»ºé¢æå */ |
| | | async function handleCreate() { |
| | | isCreating.value = true; |
| | | editingId.value = undefined; |
| | | await formApi.resetForm(); |
| | | // é¢å¡«å·¥ä½ç« |
| | | if (props.workstationId) { |
| | | await formApi.setFieldValue('workstationId', props.workstationId); |
| | | } |
| | | } |
| | | |
| | | /** ä¿å颿å */ |
| | | async function handleSave() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | const data = |
| | | (await formApi.getValues()) as MesWmProductIssueApi.ProductIssue; |
| | | try { |
| | | if (editingId.value) { |
| | | await updateProductIssue({ ...data, id: editingId.value }); |
| | | message.success('ä¿®æ¹æå'); |
| | | } else { |
| | | const id = await createProductIssue(data); |
| | | editingId.value = id; |
| | | await formApi.setFieldValue('id', id); |
| | | message.success('å建æå'); |
| | | } |
| | | isCreating.value = false; |
| | | gridApi.query(); |
| | | } catch { |
| | | message.error('ä¿å失败'); |
| | | } |
| | | } |
| | | |
| | | /** åæ¶ç¼è¾ */ |
| | | function handleCancelEdit() { |
| | | isCreating.value = false; |
| | | editingId.value = undefined; |
| | | formApi.resetForm(); |
| | | } |
| | | |
| | | /** æäº¤ */ |
| | | async function handleSubmitRow(row: MesWmProductIssueApi.ProductIssue) { |
| | | await submitProductIssue(row.id!); |
| | | message.success('æäº¤æå'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å é¤ */ |
| | | async function handleDeleteRow(row: MesWmProductIssueApi.ProductIssue) { |
| | | await deleteProductIssue(row.id!); |
| | | message.success('å 餿å'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** åæ¶ */ |
| | | async function handleCancelRow(row: MesWmProductIssueApi.ProductIssue) { |
| | | await cancelProductIssue(row.id!); |
| | | message.success('åæ¶æå'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | // --- å表 --- |
| | | const gridColumns: VxeTableGridOptions<MesWmProductIssueApi.ProductIssue>['columns'] = [ |
| | | { field: 'code', title: '颿åç¼å·', minWidth: 150 }, |
| | | { field: 'name', title: '颿ååç§°', minWidth: 130 }, |
| | | { field: 'workOrderCode', title: 'ç产订å', minWidth: 130 }, |
| | | { |
| | | field: 'requiredTime', |
| | | title: 'éæ±æ¶é´', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_WM_PRODUCT_ISSUE_STATUS }, |
| | | }, |
| | | }, |
| | | { title: 'æä½', width: 200, fixed: 'right', slots: { default: 'actions' } }, |
| | | ]; |
| | | |
| | | const gridHeight = ref(300); |
| | | const listRef = ref<HTMLElement>(); |
| | | let resizeObserver: ResizeObserver | null = null; |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: gridColumns, |
| | | height: gridHeight.value, |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getProductIssuePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | workstationId: props.workstationId, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true }, |
| | | pagerConfig: { pageSize: 10 }, |
| | | } as VxeTableGridOptions<MesWmProductIssueApi.ProductIssue>, |
| | | }); |
| | | |
| | | function updateGridHeight() { |
| | | if (listRef.value) { |
| | | const h = listRef.value.clientHeight; |
| | | if (h > 100 && h !== gridHeight.value) { |
| | | gridHeight.value = h; |
| | | gridApi.setGridOptions({ height: h }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => { |
| | | updateGridHeight(); |
| | | if (listRef.value) { |
| | | resizeObserver = new ResizeObserver(() => updateGridHeight()); |
| | | resizeObserver.observe(listRef.value); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | resizeObserver?.disconnect(); |
| | | resizeObserver = null; |
| | | }); |
| | | |
| | | // å·¥ä½ç«ååæ¶å·æ°å表 |
| | | watch( |
| | | () => props.workstationId, |
| | | () => gridApi.query(), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="issue-tab"> |
| | | <!-- é¡¶é¨ï¼è¡¨å / æ°å»ºæé® --> |
| | | <div class="issue-tab__header"> |
| | | <div class="issue-tab__header-left"> |
| | | <IconifyIcon icon="lucide:clipboard-list" class="issue-tab__icon" /> |
| | | <span class="issue-tab__title">颿å</span> |
| | | <Tag v-if="isCreating" color="blue">æ°å»ºä¸</Tag> |
| | | </div> |
| | | <div class="issue-tab__header-actions"> |
| | | <template v-if="!isCreating"> |
| | | <Button type="primary" size="small" @click="handleCreate"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æ°å»ºé¢æå |
| | | </Button> |
| | | </template> |
| | | <template v-else> |
| | | <Button type="primary" size="small" @click="handleSave"> |
| | | <template #icon><IconifyIcon icon="lucide:save" /></template> |
| | | ä¿å |
| | | </Button> |
| | | <Button size="small" @click="handleCancelEdit">åæ¶</Button> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表ååºåï¼ä»
æ°å»ºæ¶å±ç¤ºï¼ --> |
| | | <div v-if="isCreating" class="issue-tab__form"> |
| | | <Form /> |
| | | </div> |
| | | |
| | | <Divider style="margin: 12px 0" /> |
| | | |
| | | <!-- åºé¨ï¼é¢æè®°å½å表 --> |
| | | <div ref="listRef" class="issue-tab__list"> |
| | | <Grid table-title="é¢æè®°å½"> |
| | | <template #actions="{ row }"> |
| | | <Popconfirm |
| | | v-if="row.status === MesWmProductIssueStatusEnum.PREPARE" |
| | | title="确认æäº¤ï¼æäº¤åä¸è½ä¿®æ¹" |
| | | @confirm="handleSubmitRow(row)" |
| | | > |
| | | <Button type="link" size="small">æäº¤</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="row.status === MesWmProductIssueStatusEnum.PREPARE" |
| | | :title="`确认å é¤ ${row.code}ï¼`" |
| | | @confirm="handleDeleteRow(row)" |
| | | > |
| | | <Button type="link" size="small" danger>å é¤</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if=" |
| | | row.status === MesWmProductIssueStatusEnum.APPROVING || |
| | | row.status === MesWmProductIssueStatusEnum.APPROVED |
| | | " |
| | | title="ç¡®è®¤åæ¶ï¼åæ¶åä¸å¯æ¢å¤" |
| | | @confirm="handleCancelRow(row)" |
| | | > |
| | | <Button type="link" size="small" danger>åæ¶</Button> |
| | | </Popconfirm> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .issue-tab { |
| | | padding: 20px 24px; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .issue-tab__header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .issue-tab__header-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .issue-tab__icon { |
| | | font-size: 22px; |
| | | color: #1890ff; |
| | | } |
| | | |
| | | .issue-tab__title { |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .issue-tab__header-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .issue-tab__form { |
| | | margin-top: 16px; |
| | | padding: 16px; |
| | | background: #fafbfc; |
| | | border-radius: 10px; |
| | | border: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .issue-tab__list { |
| | | flex: 1; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { MesPdTemplateApi } from '#/api/mes/pro/processdesign/template'; |
| | | import type { MesProWorkOrderParamRecordApi } from '#/api/mes/pro/workorderparamrecord'; |
| | | |
| | | import { nextTick, ref, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button, Divider, message, Popconfirm, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm, z } from '#/adapter/form'; |
| | | import { getTemplate } from '#/api/mes/pro/processdesign/template'; |
| | | import { |
| | | createWorkOrderParamRecord, |
| | | deleteWorkOrderParamRecord, |
| | | getWorkOrderParamRecord, |
| | | getWorkOrderParamRecordPage, |
| | | updateWorkOrderParamRecord, |
| | | } from '#/api/mes/pro/workorderparamrecord'; |
| | | |
| | | import TemplateSelectDialog from '../TemplateSelectDialog.vue'; |
| | | |
| | | defineOptions({ name: 'WorkbenchRecordParamTab' }); |
| | | |
| | | const props = defineProps<{ |
| | | /** å½åå·¥åç¼å· */ |
| | | workOrderId?: number; |
| | | }>(); |
| | | |
| | | // --- æ¨¡å¼ --- |
| | | type Mode = 'list' | 'create' | 'edit'; |
| | | const mode = ref<Mode>('list'); |
| | | const editingRecordId = ref<number>(); |
| | | |
| | | // --- 模æ¿ç¶æ --- |
| | | const selectedTemplate = ref<MesPdTemplateApi.Template>(); |
| | | const templateDetails = ref<MesPdTemplateApi.TemplateDetail[]>([]); |
| | | const tplSelectDialogRef = ref<InstanceType<typeof TemplateSelectDialog>>(); |
| | | |
| | | function openTemplateSelect() { |
| | | tplSelectDialogRef.value?.open(); |
| | | } |
| | | |
| | | /** éæ©æ¨¡æ¿åå 载详æ
*/ |
| | | async function handleTemplateSelect(tpl: MesPdTemplateApi.Template) { |
| | | if (!tpl.id) return; |
| | | try { |
| | | const detail = await getTemplate(tpl.id); |
| | | selectedTemplate.value = detail; |
| | | templateDetails.value = detail.details ?? []; |
| | | await buildParamForm(templateDetails.value); |
| | | } catch { |
| | | message.error('å 载模æ¿è¯¦æ
失败'); |
| | | } |
| | | } |
| | | |
| | | // --- å¨æåæ°è¡¨å --- |
| | | const [ParamForm, paramFormApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 160, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: [], |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-2', |
| | | }); |
| | | |
| | | /** æ ¹æ®æ¨¡æ¿æç»æå»ºåæ°è¡¨å */ |
| | | async function buildParamForm( |
| | | details: MesPdTemplateApi.TemplateDetail[], |
| | | existingValues?: Map<number, string>, |
| | | ) { |
| | | // å
æå»ºåºç¡å段ï¼recordNo, remarkï¼ |
| | | const baseSchemas: VbenFormSchema[] = [ |
| | | { |
| | | fieldName: '_recordNo', |
| | | label: 'è®°å½åºå·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥è®°å½åºå·/æ¹æ¬¡å·ï¼å¯éï¼' }, |
| | | }, |
| | | { |
| | | fieldName: '_remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | formItemClass: 'col-span-2', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨ï¼å¯éï¼', rows: 2 }, |
| | | }, |
| | | ]; |
| | | |
| | | if (!details || details.length === 0) { |
| | | await paramFormApi.setState({ schema: baseSchemas }); |
| | | return; |
| | | } |
| | | |
| | | const paramSchemas: VbenFormSchema[] = details |
| | | .filter((d) => d != null) |
| | | .map((d) => { |
| | | const paramType = d.paramType ?? 1; |
| | | const isRequired = d.required ?? false; |
| | | const unitSuffix = d.unitMeasureName ? ` (${d.unitMeasureName})` : ''; |
| | | const paramName = d.paramName ?? ''; |
| | | const templateDetailId = d.id; |
| | | |
| | | let component = 'Input'; |
| | | const componentProps: Record<string, any> = { |
| | | placeholder: `请è¾å
¥${paramName}${unitSuffix}`, |
| | | maxLength: 100, |
| | | }; |
| | | |
| | | switch (paramType) { |
| | | case 1: |
| | | case 2: |
| | | component = 'InputNumber'; |
| | | componentProps.controls = false; |
| | | componentProps.style = 'width: 100%'; |
| | | break; |
| | | case 3: |
| | | component = 'Select'; |
| | | componentProps.allowClear = true; |
| | | componentProps.options = []; |
| | | break; |
| | | case 4: |
| | | component = 'TimePicker'; |
| | | componentProps.format = 'HH:mm:ss'; |
| | | componentProps.valueFormat = 'HH:mm:ss'; |
| | | break; |
| | | case 5: |
| | | component = 'DatePicker'; |
| | | componentProps.format = 'YYYY-MM-DD'; |
| | | componentProps.valueFormat = 'YYYY-MM-DD'; |
| | | break; |
| | | } |
| | | |
| | | let rules: any; |
| | | if (isRequired) { |
| | | if (paramType === 1 || paramType === 2) { |
| | | rules = z.number({ message: `${paramName}ä¸è½ä¸ºç©º` }); |
| | | } else { |
| | | rules = z.string({ message: `${paramName}ä¸è½ä¸ºç©º` }).min(1, `${paramName}ä¸è½ä¸ºç©º`); |
| | | } |
| | | } |
| | | |
| | | return { |
| | | fieldName: `param_${templateDetailId}`, |
| | | label: `${paramName}${unitSuffix}`, |
| | | component, |
| | | componentProps, |
| | | rules, |
| | | defaultValue: existingValues?.get(templateDetailId!), |
| | | } as VbenFormSchema; |
| | | }); |
| | | |
| | | await paramFormApi.resetForm(); |
| | | await nextTick(); |
| | | await paramFormApi.setState({ schema: [...baseSchemas, ...paramSchemas] }); |
| | | } |
| | | |
| | | /** æ°å¢åæ°è®°å½ */ |
| | | async function handleCreate() { |
| | | mode.value = 'create'; |
| | | editingRecordId.value = undefined; |
| | | selectedTemplate.value = undefined; |
| | | templateDetails.value = []; |
| | | await paramFormApi.setState({ schema: [] }); |
| | | await paramFormApi.resetForm(); |
| | | } |
| | | |
| | | /** æå¼æ¨¡æ¿éæ© */ |
| | | function handleSelectTemplate() { |
| | | tplSelectDialogRef.value?.open(); |
| | | } |
| | | |
| | | /** åæ¶æ°å¢/ç¼è¾ */ |
| | | function handleCancelForm() { |
| | | mode.value = 'list'; |
| | | editingRecordId.value = undefined; |
| | | selectedTemplate.value = undefined; |
| | | templateDetails.value = []; |
| | | paramFormApi.resetForm(); |
| | | } |
| | | |
| | | /** ä¿ååæ°è®°å½ */ |
| | | async function handleSave() { |
| | | if (!selectedTemplate.value || !props.workOrderId) return; |
| | | const { valid } = await paramFormApi.validate(); |
| | | if (!valid) { |
| | | message.warning('è¯·æ£æ¥å¿
å¡«åæ°'); |
| | | return; |
| | | } |
| | | const values = await paramFormApi.getValues(); |
| | | |
| | | // æå»º details æ°ç» |
| | | const details = templateDetails.value |
| | | .filter((d) => d != null && d.id != null) |
| | | .map((d) => ({ |
| | | templateDetailId: d.id!, |
| | | paramValue: values[`param_${d.id}`] != null ? String(values[`param_${d.id}`]) : undefined, |
| | | })); |
| | | |
| | | // è·å表åä¸ç recordNo å remark |
| | | const recordNo = values._recordNo as string | undefined; |
| | | const remark = values._remark as string | undefined; |
| | | |
| | | try { |
| | | if (mode.value === 'edit' && editingRecordId.value) { |
| | | await updateWorkOrderParamRecord({ |
| | | id: editingRecordId.value, |
| | | templateId: selectedTemplate.value.id, |
| | | recordNo, |
| | | remark, |
| | | details, |
| | | }); |
| | | message.success('æ´æ°æå'); |
| | | } else { |
| | | await createWorkOrderParamRecord( |
| | | { templateId: selectedTemplate.value.id, recordNo, remark, details }, |
| | | props.workOrderId, |
| | | ); |
| | | message.success('å建æå'); |
| | | } |
| | | handleCancelForm(); |
| | | refreshRecordList(); |
| | | } catch { |
| | | message.error('ä¿å失败'); |
| | | } |
| | | } |
| | | |
| | | /** ç¼è¾å·²æè®°å½ */ |
| | | async function handleEditRecord(recordId: number) { |
| | | try { |
| | | const record = await getWorkOrderParamRecord(recordId); |
| | | if (!record.templateId) return; |
| | | mode.value = 'edit'; |
| | | editingRecordId.value = record.id; |
| | | |
| | | // å è½½æ¨¡æ¿ |
| | | const tpl = await getTemplate(record.templateId); |
| | | selectedTemplate.value = tpl; |
| | | templateDetails.value = tpl.details ?? []; |
| | | |
| | | // æå»ºå·²æå¼æ å° |
| | | const valueMap = new Map<number, string>(); |
| | | for (const d of record.details ?? []) { |
| | | if (d.templateDetailId != null && d.paramValue != null) { |
| | | valueMap.set(d.templateDetailId, d.paramValue); |
| | | } |
| | | } |
| | | |
| | | await buildParamForm(templateDetails.value, valueMap); |
| | | |
| | | // 设置 recordNo å remark |
| | | await paramFormApi.setFieldValue('_recordNo', record.recordNo); |
| | | await paramFormApi.setFieldValue('_remark', record.remark); |
| | | } catch { |
| | | message.error('å 载记å½è¯¦æ
失败'); |
| | | } |
| | | } |
| | | |
| | | /** å é¤è®°å½ */ |
| | | async function handleDeleteRecord(recordId: number) { |
| | | try { |
| | | await deleteWorkOrderParamRecord(recordId); |
| | | message.success('å 餿å'); |
| | | refreshRecordList(); |
| | | } catch { |
| | | message.error('å é¤å¤±è´¥'); |
| | | } |
| | | } |
| | | |
| | | // --- è®°å½å表 --- |
| | | const recordList = ref<MesProWorkOrderParamRecordApi.WorkOrderParamRecord[]>([]); |
| | | const recordLoading = ref(false); |
| | | |
| | | async function refreshRecordList() { |
| | | if (!props.workOrderId) { |
| | | recordList.value = []; |
| | | return; |
| | | } |
| | | recordLoading.value = true; |
| | | try { |
| | | const result = await getWorkOrderParamRecordPage({ |
| | | workOrderId: props.workOrderId, |
| | | pageNo: 1, |
| | | pageSize: 100, |
| | | }); |
| | | recordList.value = result.list ?? []; |
| | | } finally { |
| | | recordLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | // å·¥ååæ´æ¶å è½½è®°å½ |
| | | watch( |
| | | () => props.workOrderId, |
| | | () => { |
| | | handleCancelForm(); |
| | | refreshRecordList(); |
| | | }, |
| | | ); |
| | | |
| | | /** æ ¼å¼ååæ°å¼å±ç¤º */ |
| | | function formatParamDisplay(detail: MesProWorkOrderParamRecordApi.ParamRecordDetail) { |
| | | const unit = detail.unitMeasureName ? ` ${detail.unitMeasureName}` : ''; |
| | | return `${detail.paramName}: ${detail.paramValue ?? '-'}${unit}`; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="record-param-tab"> |
| | | <!-- 顶鍿使 --> |
| | | <div class="record-param-tab__toolbar"> |
| | | <div class="record-param-tab__toolbar-left"> |
| | | <IconifyIcon icon="lucide:file-cog" class="record-param-tab__toolbar-icon" /> |
| | | <span class="record-param-tab__toolbar-title">ç产记å½åæ°</span> |
| | | </div> |
| | | <Button |
| | | v-if="mode === 'list'" |
| | | type="primary" |
| | | size="small" |
| | | :disabled="!workOrderId" |
| | | @click="handleCreate" |
| | | > |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æ°å¢åæ°è®°å½ |
| | | </Button> |
| | | <div v-else class="record-param-tab__toolbar-actions"> |
| | | <Tag :color="mode === 'create' ? 'blue' : 'orange'"> |
| | | {{ mode === 'create' ? 'æ°å»º' : 'ç¼è¾' }} |
| | | </Tag> |
| | | <Button type="primary" size="small" @click="handleSave"> |
| | | <template #icon><IconifyIcon icon="lucide:save" /></template> |
| | | ä¿å |
| | | </Button> |
| | | <Button size="small" @click="handleCancelForm">åæ¶</Button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表ååºåï¼æ°å¢/ç¼è¾æ¨¡å¼ï¼ --> |
| | | <div v-if="mode !== 'list'" class="record-param-tab__form-card"> |
| | | <!-- 模æ¿éæ© --> |
| | | <div class="record-param-tab__template-row"> |
| | | <span class="record-param-tab__template-label">åæ°æ¨¡æ¿ï¼</span> |
| | | <template v-if="selectedTemplate"> |
| | | <Tag color="green">{{ selectedTemplate.templateName }}</Tag> |
| | | <span class="record-param-tab__template-code">{{ selectedTemplate.templateCode }}</span> |
| | | <Button |
| | | v-if="mode === 'create'" |
| | | size="small" |
| | | type="link" |
| | | @click="handleSelectTemplate" |
| | | > |
| | | æ´æ¢æ¨¡æ¿ |
| | | </Button> |
| | | </template> |
| | | <Button v-else type="dashed" size="small" @click="handleSelectTemplate"> |
| | | <template #icon><IconifyIcon icon="lucide:folder-open" /></template> |
| | | éæ©æ¨¡æ¿ |
| | | </Button> |
| | | </div> |
| | | |
| | | <!-- è®°å½ç¼å· + 夿³¨ --> |
| | | <div class="record-param-tab__meta-row"> |
| | | <ParamForm /> |
| | | </div> |
| | | |
| | | <!-- æ æ¨¡æ¿æç¤º --> |
| | | <div v-if="!selectedTemplate" class="record-param-tab__no-template"> |
| | | <IconifyIcon icon="lucide:arrow-up-left" class="record-param-tab__no-template-icon" /> |
| | | <span>请å
éæ©ä¸ä¸ªåæ°æ¨¡æ¿</span> |
| | | </div> |
| | | |
| | | <!-- åæ°è¡¨åå·²å è½½ --> |
| | | <div |
| | | v-else-if="templateDetails.length > 0" |
| | | class="record-param-tab__param-info" |
| | | > |
| | | <span class="record-param-tab__param-count"> |
| | | å
± {{ templateDetails.length }} é¡¹åæ°ï¼ |
| | | {{ templateDetails.filter((d) => d.required).length }} 项å¿
å¡« |
| | | </span> |
| | | </div> |
| | | <div v-else class="record-param-tab__no-template"> |
| | | <span>è¯¥æ¨¡æ¿ææ åæ°æç»</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <Divider style="margin: 12px 0" /> |
| | | |
| | | <!-- è®°å½å表 --> |
| | | <div class="record-param-tab__list"> |
| | | <div class="record-param-tab__list-header"> |
| | | <span class="record-param-tab__list-title"> |
| | | åæ°è®°å½ |
| | | <span class="record-param-tab__list-count">({{ recordList.length }})</span> |
| | | </span> |
| | | <Button |
| | | v-if="workOrderId" |
| | | size="small" |
| | | :loading="recordLoading" |
| | | @click="refreshRecordList" |
| | | > |
| | | <template #icon><IconifyIcon icon="lucide:refresh-cw" /></template> |
| | | å·æ° |
| | | </Button> |
| | | </div> |
| | | |
| | | <!-- æ å·¥åæç¤º --> |
| | | <div v-if="!workOrderId" class="record-param-tab__empty"> |
| | | <IconifyIcon icon="lucide:alert-circle" class="record-param-tab__empty-icon" /> |
| | | <p>请å
ä»å·¦ä¾§éæ©ä¸æ¡å·¥åä»»å¡</p> |
| | | </div> |
| | | |
| | | <!-- 空å表 --> |
| | | <div v-else-if="recordList.length === 0" class="record-param-tab__empty"> |
| | | <IconifyIcon icon="lucide:inbox" class="record-param-tab__empty-icon" /> |
| | | <p>ææ åæ°è®°å½ï¼ç¹å»ä¸æ¹"æ°å¢åæ°è®°å½"å¼å§</p> |
| | | </div> |
| | | |
| | | <!-- è®°å½å¡çå表 --> |
| | | <div v-else class="record-param-tab__cards"> |
| | | <div |
| | | v-for="record in recordList" |
| | | :key="record.id" |
| | | class="record-param-tab__card" |
| | | > |
| | | <div class="record-param-tab__card-head"> |
| | | <div class="record-param-tab__card-meta"> |
| | | <Tag color="green">{{ record.templateName }}</Tag> |
| | | <span v-if="record.recordNo" class="record-param-tab__card-no"> |
| | | {{ record.recordNo }} |
| | | </span> |
| | | <span class="record-param-tab__card-time">{{ record.createTime }}</span> |
| | | </div> |
| | | <div class="record-param-tab__card-actions"> |
| | | <Button type="link" size="small" @click="handleEditRecord(record.id!)"> |
| | | ç¼è¾ |
| | | </Button> |
| | | <Popconfirm |
| | | title="确认å é¤è¯¥åæ°è®°å½ï¼å é¤åä¸å¯æ¢å¤" |
| | | @confirm="handleDeleteRecord(record.id!)" |
| | | > |
| | | <Button type="link" size="small" danger>å é¤</Button> |
| | | </Popconfirm> |
| | | </div> |
| | | </div> |
| | | <div v-if="record.remark" class="record-param-tab__card-remark"> |
| | | 夿³¨ï¼{{ record.remark }} |
| | | </div> |
| | | <div class="record-param-tab__card-params"> |
| | | <span |
| | | v-for="d in record.details" |
| | | :key="d.id" |
| | | class="record-param-tab__param-tag" |
| | | > |
| | | {{ formatParamDisplay(d) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 模æ¿éæ©å¼¹çª --> |
| | | <TemplateSelectDialog |
| | | ref="tplSelectDialogRef" |
| | | @select="handleTemplateSelect" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .record-param-tab { |
| | | padding: 20px 24px; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: auto; |
| | | } |
| | | |
| | | // --- é¡¶é¨å·¥å
·æ --- |
| | | .record-param-tab__toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .record-param-tab__toolbar-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .record-param-tab__toolbar-icon { |
| | | font-size: 22px; |
| | | color: #22c55e; |
| | | } |
| | | |
| | | .record-param-tab__toolbar-title { |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .record-param-tab__toolbar-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | // --- 表åå¡ç --- |
| | | .record-param-tab__form-card { |
| | | margin-top: 16px; |
| | | padding: 16px 20px; |
| | | background: #fafbfc; |
| | | border-radius: 10px; |
| | | border: 1px solid #e8e8e8; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .record-param-tab__template-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .record-param-tab__template-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #595959; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .record-param-tab__template-code { |
| | | font-size: 12px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .record-param-tab__meta-row { |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .record-param-tab__no-template { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 20px 0; |
| | | color: #8c8c8c; |
| | | font-size: 14px; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .record-param-tab__no-template-icon { |
| | | font-size: 18px; |
| | | opacity: 0.6; |
| | | } |
| | | |
| | | .record-param-tab__param-info { |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .record-param-tab__param-count { |
| | | font-size: 13px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | // --- è®°å½å表 --- |
| | | .record-param-tab__list { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .record-param-tab__list-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-bottom: 12px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .record-param-tab__list-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .record-param-tab__list-count { |
| | | font-size: 13px; |
| | | font-weight: 400; |
| | | color: #8c8c8c; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | // --- ç©ºç¶æ --- |
| | | .record-param-tab__empty { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | gap: 8px; |
| | | color: #8c8c8c; |
| | | |
| | | p { |
| | | margin: 0; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | |
| | | .record-param-tab__empty-icon { |
| | | font-size: 36px; |
| | | opacity: 0.4; |
| | | } |
| | | |
| | | // --- è®°å½å¡ç --- |
| | | .record-param-tab__cards { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .record-param-tab__card { |
| | | padding: 12px 16px; |
| | | background: #fff; |
| | | border: 1px solid #f0f0f0; |
| | | border-radius: 8px; |
| | | transition: box-shadow 0.2s; |
| | | |
| | | &:hover { |
| | | box-shadow: 0 2px 8px rgb(0 0 0 / 8%); |
| | | } |
| | | } |
| | | |
| | | .record-param-tab__card-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .record-param-tab__card-meta { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .record-param-tab__card-no { |
| | | font-size: 13px; |
| | | font-weight: 500; |
| | | color: #595959; |
| | | padding: 2px 8px; |
| | | background: #f5f5f5; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .record-param-tab__card-time { |
| | | font-size: 12px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .record-param-tab__card-actions { |
| | | display: flex; |
| | | gap: 4px; |
| | | flex-shrink: 0; |
| | | } |
| | | |
| | | .record-param-tab__card-remark { |
| | | margin-top: 6px; |
| | | font-size: 13px; |
| | | color: #8c8c8c; |
| | | } |
| | | |
| | | .record-param-tab__card-params { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 6px; |
| | | margin-top: 8px; |
| | | } |
| | | |
| | | .record-param-tab__param-tag { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | padding: 3px 10px; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | background: #f6ffed; |
| | | color: #389e0d; |
| | | border: 1px solid #d9f7be; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { VbenFormSchema } from '#/adapter/form'; |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { MesWmReturnIssueApi } from '#/api/mes/wm/returnissue'; |
| | | |
| | | import { h, markRaw, onBeforeUnmount, onMounted, ref, watch, nextTick } from 'vue'; |
| | | |
| | | import { |
| | | DICT_TYPE, |
| | | MesAutoCodeRuleCode, |
| | | MesProWorkOrderStatusEnum, |
| | | MesWmReturnIssueStatusEnum, |
| | | } from '@vben/constants'; |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | import { getDictOptions } from '@vben/hooks'; |
| | | |
| | | import { Button, Divider, message, Popconfirm, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { generateAutoCode } from '#/api/mes/md/autocode/record'; |
| | | import { |
| | | cancelReturnIssue, |
| | | createReturnIssue, |
| | | deleteReturnIssue, |
| | | getReturnIssuePage, |
| | | submitReturnIssue, |
| | | updateReturnIssue, |
| | | } from '#/api/mes/wm/returnissue'; |
| | | import { MdWorkstationSelect } from '#/views/mes/md/workstation/components'; |
| | | import { ProWorkOrderSelect } from '#/views/mes/pro/workorder/components'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | defineOptions({ name: 'WorkbenchReturnIssueTab' }); |
| | | |
| | | const props = defineProps<{ |
| | | /** å½åå·¥ä½ç« ID */ |
| | | workstationId?: number; |
| | | /** å½åå·¥ä½ç«åç§° */ |
| | | workstationName?: string; |
| | | }>(); |
| | | |
| | | // --- 表åç¶æ --- |
| | | const editingId = ref<number>(); |
| | | const isCreating = ref(false); |
| | | |
| | | /** æå»ºè¡¨å schema */ |
| | | function buildFormSchema(): VbenFormSchema[] { |
| | | return [ |
| | | { |
| | | fieldName: 'code', |
| | | label: 'éæåç¼å·', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥éæåç¼å·' }, |
| | | rules: 'required', |
| | | suffix: () => |
| | | h( |
| | | Button, |
| | | { |
| | | type: 'default', |
| | | size: 'small', |
| | | onClick: async () => { |
| | | const code = await generateAutoCode( |
| | | MesAutoCodeRuleCode.WM_RETURN_ISSUE_CODE, |
| | | ); |
| | | await formApi.setFieldValue('code', code); |
| | | }, |
| | | }, |
| | | { default: () => 'çæ' }, |
| | | ), |
| | | }, |
| | | { |
| | | fieldName: 'name', |
| | | label: 'éæååç§°', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥éæååç§°' }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'type', |
| | | label: 'éæç±»å', |
| | | component: 'Select', |
| | | componentProps: { |
| | | allowClear: true, |
| | | options: getDictOptions(DICT_TYPE.MES_WM_RETURN_ISSUE_TYPE, 'number'), |
| | | placeholder: 'è¯·éæ©éæç±»å', |
| | | }, |
| | | rules: 'required', |
| | | }, |
| | | { |
| | | fieldName: 'workOrderId', |
| | | label: 'ç产订å', |
| | | component: markRaw(ProWorkOrderSelect), |
| | | componentProps: { |
| | | status: MesProWorkOrderStatusEnum.CONFIRMED, |
| | | }, |
| | | rules: 'selectRequired', |
| | | }, |
| | | { |
| | | fieldName: 'workstationId', |
| | | label: 'å·¥ä½ç«', |
| | | component: markRaw(MdWorkstationSelect), |
| | | componentProps: { placeholder: 'è¯·éæ©å·¥ä½ç«' }, |
| | | }, |
| | | { |
| | | fieldName: 'returnDate', |
| | | label: 'éææ¥æ', |
| | | component: 'DatePicker', |
| | | componentProps: { |
| | | class: '!w-full', |
| | | format: 'YYYY-MM-DD HH:mm:ss', |
| | | placeholder: 'è¯·éæ©éææ¥æ', |
| | | showTime: true, |
| | | valueFormat: 'x', |
| | | }, |
| | | }, |
| | | { |
| | | fieldName: 'remark', |
| | | label: '夿³¨', |
| | | component: 'Textarea', |
| | | componentProps: { placeholder: '请è¾å
¥å¤æ³¨', rows: 2 }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | commonConfig: { |
| | | componentProps: { class: 'w-full' }, |
| | | formItemClass: 'col-span-1', |
| | | labelWidth: 100, |
| | | }, |
| | | layout: 'horizontal', |
| | | schema: buildFormSchema(), |
| | | showDefaultActions: false, |
| | | wrapperClass: 'grid-cols-3', |
| | | }); |
| | | |
| | | /** æ°å»ºéæå */ |
| | | async function handleCreate() { |
| | | isCreating.value = true; |
| | | editingId.value = undefined; |
| | | await formApi.resetForm(); |
| | | if (props.workstationId) { |
| | | await formApi.setFieldValue('workstationId', props.workstationId); |
| | | } |
| | | } |
| | | |
| | | /** ä¿åéæå */ |
| | | async function handleSave() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) return; |
| | | const data = |
| | | (await formApi.getValues()) as MesWmReturnIssueApi.ReturnIssue; |
| | | try { |
| | | if (editingId.value) { |
| | | await updateReturnIssue({ ...data, id: editingId.value }); |
| | | message.success('ä¿®æ¹æå'); |
| | | } else { |
| | | const id = await createReturnIssue(data); |
| | | editingId.value = id; |
| | | await formApi.setFieldValue('id', id); |
| | | message.success('å建æå'); |
| | | } |
| | | isCreating.value = false; |
| | | gridApi.query(); |
| | | } catch { |
| | | message.error('ä¿å失败'); |
| | | } |
| | | } |
| | | |
| | | /** åæ¶ç¼è¾ */ |
| | | function handleCancelEdit() { |
| | | isCreating.value = false; |
| | | editingId.value = undefined; |
| | | formApi.resetForm(); |
| | | } |
| | | |
| | | /** æäº¤ */ |
| | | async function handleSubmitRow(row: MesWmReturnIssueApi.ReturnIssue) { |
| | | await submitReturnIssue(row.id!); |
| | | message.success('æäº¤æå'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** å é¤ */ |
| | | async function handleDeleteRow(row: MesWmReturnIssueApi.ReturnIssue) { |
| | | await deleteReturnIssue(row.id!); |
| | | message.success('å 餿å'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | /** åæ¶ */ |
| | | async function handleCancelRow(row: MesWmReturnIssueApi.ReturnIssue) { |
| | | await cancelReturnIssue(row.id!); |
| | | message.success('åæ¶æå'); |
| | | gridApi.query(); |
| | | } |
| | | |
| | | // --- å表 --- |
| | | const gridColumns: VxeTableGridOptions<MesWmReturnIssueApi.ReturnIssue>['columns'] = [ |
| | | { field: 'code', title: 'éæåç¼å·', minWidth: 150 }, |
| | | { field: 'name', title: 'éæååç§°', minWidth: 130 }, |
| | | { |
| | | field: 'type', |
| | | title: 'éæç±»å', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_WM_RETURN_ISSUE_TYPE }, |
| | | }, |
| | | }, |
| | | { field: 'workOrderCode', title: 'ç产订å', minWidth: 130 }, |
| | | { |
| | | field: 'returnDate', |
| | | title: 'éææ¥æ', |
| | | width: 170, |
| | | formatter: 'formatDateTime', |
| | | }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: DICT_TYPE.MES_WM_RETURN_ISSUE_STATUS }, |
| | | }, |
| | | }, |
| | | { title: 'æä½', width: 200, fixed: 'right', slots: { default: 'actions' } }, |
| | | ]; |
| | | |
| | | const gridHeight = ref(300); |
| | | const listRef = ref<HTMLElement>(); |
| | | let resizeObserver: ResizeObserver | null = null; |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | gridOptions: { |
| | | columns: gridColumns, |
| | | height: gridHeight.value, |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getReturnIssuePage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | workstationId: props.workstationId, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true }, |
| | | pagerConfig: { pageSize: 10 }, |
| | | } as VxeTableGridOptions<MesWmReturnIssueApi.ReturnIssue>, |
| | | }); |
| | | |
| | | function updateGridHeight() { |
| | | if (listRef.value) { |
| | | const h = listRef.value.clientHeight; |
| | | if (h > 100 && h !== gridHeight.value) { |
| | | gridHeight.value = h; |
| | | gridApi.setGridOptions({ height: h }); |
| | | } |
| | | } |
| | | } |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => { |
| | | updateGridHeight(); |
| | | if (listRef.value) { |
| | | resizeObserver = new ResizeObserver(() => updateGridHeight()); |
| | | resizeObserver.observe(listRef.value); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | resizeObserver?.disconnect(); |
| | | resizeObserver = null; |
| | | }); |
| | | |
| | | // å·¥ä½ç«ååæ¶å·æ°å表 |
| | | watch( |
| | | () => props.workstationId, |
| | | () => gridApi.query(), |
| | | ); |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="issue-tab"> |
| | | <!-- é¡¶é¨ï¼è¡¨å / æ°å»ºæé® --> |
| | | <div class="issue-tab__header"> |
| | | <div class="issue-tab__header-left"> |
| | | <IconifyIcon icon="lucide:rotate-ccw" class="issue-tab__icon" /> |
| | | <span class="issue-tab__title">éæå</span> |
| | | <Tag v-if="isCreating" color="orange">æ°å»ºä¸</Tag> |
| | | </div> |
| | | <div class="issue-tab__header-actions"> |
| | | <template v-if="!isCreating"> |
| | | <Button type="primary" size="small" @click="handleCreate"> |
| | | <template #icon><IconifyIcon icon="lucide:plus" /></template> |
| | | æ°å»ºéæå |
| | | </Button> |
| | | </template> |
| | | <template v-else> |
| | | <Button type="primary" size="small" @click="handleSave"> |
| | | <template #icon><IconifyIcon icon="lucide:save" /></template> |
| | | ä¿å |
| | | </Button> |
| | | <Button size="small" @click="handleCancelEdit">åæ¶</Button> |
| | | </template> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 表ååºåï¼ä»
æ°å»ºæ¶å±ç¤ºï¼ --> |
| | | <div v-if="isCreating" class="issue-tab__form"> |
| | | <Form /> |
| | | </div> |
| | | |
| | | <Divider style="margin: 12px 0" /> |
| | | |
| | | <!-- åºé¨ï¼éæè®°å½å表 --> |
| | | <div ref="listRef" class="issue-tab__list"> |
| | | <Grid table-title="éæè®°å½"> |
| | | <template #actions="{ row }"> |
| | | <Popconfirm |
| | | v-if="row.status === MesWmReturnIssueStatusEnum.PREPARE" |
| | | title="确认æäº¤ï¼æäº¤åä¸è½ä¿®æ¹" |
| | | @confirm="handleSubmitRow(row)" |
| | | > |
| | | <Button type="link" size="small">æäº¤</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if="row.status === MesWmReturnIssueStatusEnum.PREPARE" |
| | | :title="`确认å é¤ ${row.code}ï¼`" |
| | | @confirm="handleDeleteRow(row)" |
| | | > |
| | | <Button type="link" size="small" danger>å é¤</Button> |
| | | </Popconfirm> |
| | | <Popconfirm |
| | | v-if=" |
| | | row.status === MesWmReturnIssueStatusEnum.CONFIRMED || |
| | | row.status === MesWmReturnIssueStatusEnum.APPROVING || |
| | | row.status === MesWmReturnIssueStatusEnum.APPROVED |
| | | " |
| | | title="ç¡®è®¤åæ¶ï¼åæ¶åä¸å¯æ¢å¤" |
| | | @confirm="handleCancelRow(row)" |
| | | > |
| | | <Button type="link" size="small" danger>åæ¶</Button> |
| | | </Popconfirm> |
| | | </template> |
| | | </Grid> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .issue-tab { |
| | | padding: 20px 24px; |
| | | height: 100%; |
| | | display: flex; |
| | | flex-direction: column; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .issue-tab__header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .issue-tab__header-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .issue-tab__icon { |
| | | font-size: 22px; |
| | | color: #fa8c16; |
| | | } |
| | | |
| | | .issue-tab__title { |
| | | font-size: 17px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .issue-tab__header-actions { |
| | | display: flex; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .issue-tab__form { |
| | | margin-top: 16px; |
| | | padding: 16px; |
| | | background: #fafbfc; |
| | | border-radius: 10px; |
| | | border: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | .issue-tab__list { |
| | | flex: 1; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { PendingTask } from '../../../types'; |
| | | |
| | | import { Descriptions, Progress, Tag } from 'ant-design-vue'; |
| | | |
| | | defineOptions({ name: 'WorkbenchTaskDetailTab' }); |
| | | |
| | | defineProps<{ |
| | | task?: PendingTask | null; |
| | | }>(); |
| | | |
| | | function getProgressStatus(rate?: number): 'exception' | 'normal' | 'success' { |
| | | if (rate === undefined || rate === null) return 'normal'; |
| | | if (rate >= 100) return 'success'; |
| | | if (rate < 0) return 'exception'; |
| | | return 'normal'; |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="task-detail-tab"> |
| | | <div v-if="!task" class="task-detail-tab__empty"> |
| | | 请ä»å·¦ä¾§å·¥ååè¡¨éæ©ä¸æ¡ä»»å¡ |
| | | </div> |
| | | <template v-else> |
| | | <!-- è¿åº¦æ¦è§ --> |
| | | <div class="detail-progress"> |
| | | <div class="detail-progress__head"> |
| | | <span class="detail-progress__title">æ¥å·¥è¿åº¦</span> |
| | | <span class="detail-progress__rate">{{ task.feedbackRate ?? 0 }}%</span> |
| | | <span class="detail-progress__qty"> |
| | | {{ task.producedQuantity ?? 0 }} / {{ task.quantity ?? 0 }} |
| | | {{ task.unitMeasureName }} |
| | | </span> |
| | | </div> |
| | | <Progress |
| | | :percent="task.feedbackRate ?? 0" |
| | | :status="getProgressStatus(task.feedbackRate)" |
| | | :stroke-color="{ from: '#108ee9', to: '#87d068' }" |
| | | :show-info="false" |
| | | :stroke-width="14" |
| | | /> |
| | | <div class="detail-progress__stats"> |
| | | <div class="stat-item"> |
| | | <span class="stat-item__label">æäº§æ°é</span> |
| | | <span class="stat-item__value">{{ task.quantity ?? 0 }}</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-item__label">å·²ç产</span> |
| | | <span class="stat-item__value">{{ task.producedQuantity ?? 0 }}</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-item__label">å¨éæ¥å·¥</span> |
| | | <span class="stat-item__value">{{ task.inTransitQuantity ?? 0 }}</span> |
| | | </div> |
| | | <div class="stat-item"> |
| | | <span class="stat-item__label">坿¥å·¥</span> |
| | | <span class="stat-item__value stat-item__value--primary"> |
| | | {{ task.reportableQuantity ?? 0 }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 详ç»ä¿¡æ¯ --> |
| | | <Descriptions |
| | | bordered |
| | | :column="2" |
| | | size="middle" |
| | | class="detail-desc" |
| | | > |
| | | <Descriptions.Item label="ä»»å¡ç¼ç ">{{ task.code ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä»»å¡åç§°">{{ task.name ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å·¥åç¼ç ">{{ task.workOrderCode ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å·¥ååç§°">{{ task.workOrderName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å·¥ä½ç«">{{ task.workstationName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="å·¥åº">{{ task.processName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="产åç¼ç ">{{ task.itemCode ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="产ååç§°">{{ task.itemName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="è§æ ¼åå·">{{ task.itemSpecification ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="计éåä½">{{ task.unitMeasureName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="åæ ¼åæ°é">{{ task.qualifyQuantity ?? 0 }}</Descriptions.Item> |
| | | <Descriptions.Item label="ä¸è¯åæ°é">{{ task.unqualifyQuantity ?? 0 }}</Descriptions.Item> |
| | | <Descriptions.Item label="客æ·åç§°">{{ task.clientName ?? 'â' }}</Descriptions.Item> |
| | | <Descriptions.Item label="æ¯å¦è´¨æ£å·¥åº"> |
| | | <Tag v-if="task.checkFlag" color="orange">æ¯</Tag> |
| | | <Tag v-else>å¦</Tag> |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="éæ±æ¥æ" :span="2"> |
| | | {{ task.requestDate ?? 'â' }} |
| | | </Descriptions.Item> |
| | | <Descriptions.Item label="夿³¨" :span="2">{{ task.remark ?? 'â' }}</Descriptions.Item> |
| | | </Descriptions> |
| | | </template> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | .task-detail-tab { |
| | | padding: 20px 24px; |
| | | min-height: 100%; |
| | | height: 100%; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .task-detail-tab__empty { |
| | | padding: 64px 0; |
| | | text-align: center; |
| | | color: #8c8c8c; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .detail-progress { |
| | | margin-bottom: 20px; |
| | | padding: 16px 20px; |
| | | border-radius: 8px; |
| | | background: #f6f8fa; |
| | | border: 1px solid #e8e8e8; |
| | | } |
| | | |
| | | .detail-progress__head { |
| | | display: flex; |
| | | align-items: baseline; |
| | | flex-wrap: wrap; |
| | | gap: 8px 16px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .detail-progress__title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | .detail-progress__rate { |
| | | font-size: 22px; |
| | | font-weight: 700; |
| | | color: var(--ant-primary-color, #1890ff); |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | .detail-progress__qty { |
| | | margin-left: auto; |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #595959; |
| | | } |
| | | |
| | | .detail-progress__stats { |
| | | display: flex; |
| | | gap: 24px; |
| | | margin-top: 14px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .stat-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | |
| | | &__label { |
| | | font-size: 12px; |
| | | color: #8c8c8c; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | &__value { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #1a1a1a; |
| | | font-variant-numeric: tabular-nums; |
| | | |
| | | &--primary { |
| | | color: var(--ant-primary-color, #1890ff); |
| | | } |
| | | } |
| | | } |
| | | |
| | | .detail-desc { |
| | | :deep(.ant-descriptions-item-label) { |
| | | width: 140px; |
| | | font-weight: 600; |
| | | background: #fafafa; |
| | | } |
| | | |
| | | :deep(.ant-descriptions-item-content) { |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { MesMdWorkstationApi } from '#/api/mes/md/workstation'; |
| | | import type { PendingTask, WorkbenchTabItem, WorkbenchTabKey } from './types'; |
| | | |
| | | import { computed, ref, watch } from 'vue'; |
| | | |
| | | import { IconifyIcon } from '@vben/icons'; |
| | | |
| | | import { Button } from 'ant-design-vue'; |
| | | |
| | | import { getPendingFeedbackPage } from '#/api/mes/pro/task'; |
| | | import { router } from '#/router'; |
| | | |
| | | import TaskListPanel from './components/TaskListPanel.vue'; |
| | | import WorkstationPanel from './components/WorkstationPanel.vue'; |
| | | import WorkstationSelectDialog from './components/WorkstationSelectDialog.vue'; |
| | | import FeedbackHistoryTab from './components/tabs/FeedbackHistoryTab.vue'; |
| | | import FeedbackTab from './components/tabs/FeedbackTab.vue'; |
| | | import ProductIssueTab from './components/tabs/ProductIssueTab.vue'; |
| | | import RecordParamTab from './components/tabs/RecordParamTab.vue'; |
| | | import ReturnIssueTab from './components/tabs/ReturnIssueTab.vue'; |
| | | import TaskDetailTab from './components/tabs/TaskDetailTab.vue'; |
| | | |
| | | defineOptions({ name: 'MesWorkbench' }); |
| | | |
| | | // --- Tab é
ç½® --- |
| | | const tabItems: WorkbenchTabItem[] = [ |
| | | { key: 'detail', label: 'å·¥å详æ
', icon: 'lucide:file-text' }, |
| | | { key: 'feedback', label: 'æ¥å·¥', icon: 'lucide:edit-3' }, |
| | | { key: 'history', label: 'æ¥å·¥è®°å½', icon: 'lucide:history' }, |
| | | { key: 'record-param', label: 'ç产记å½åæ°', icon: 'lucide:file-cog' }, |
| | | { key: 'product-issue', label: '颿', icon: 'lucide:clipboard-list' }, |
| | | { key: 'return-issue', label: 'éæ', icon: 'lucide:rotate-ccw' }, |
| | | ]; |
| | | |
| | | const activeTab = ref<WorkbenchTabKey>('detail'); |
| | | const activeTabLabel = computed( |
| | | () => tabItems.find((t) => t.key === activeTab.value)?.label ?? '', |
| | | ); |
| | | |
| | | // --- å·¥ä½ç« --- |
| | | const workstation = ref<MesMdWorkstationApi.Workstation>(); |
| | | const workstationId = computed(() => workstation.value?.id); |
| | | const workstationName = computed(() => workstation.value?.name ?? ''); |
| | | const wsSelectDialogRef = ref<InstanceType<typeof WorkstationSelectDialog>>(); |
| | | |
| | | function openWorkstationSelect() { |
| | | wsSelectDialogRef.value?.open(); |
| | | } |
| | | |
| | | function handleWorkstationSelect(ws: MesMdWorkstationApi.Workstation) { |
| | | workstation.value = ws; |
| | | } |
| | | |
| | | // --- ä»»å¡å表 --- |
| | | const taskList = ref<PendingTask[]>([]); |
| | | const taskLoading = ref(false); |
| | | const selectedTask = ref<PendingTask>(); |
| | | |
| | | async function loadTaskList() { |
| | | if (!workstationId.value) { |
| | | taskList.value = []; |
| | | selectedTask.value = undefined; |
| | | return; |
| | | } |
| | | taskLoading.value = true; |
| | | try { |
| | | const result = await getPendingFeedbackPage({ |
| | | workstationId: workstationId.value, |
| | | pageNo: 1, |
| | | pageSize: 200, |
| | | }); |
| | | taskList.value = result.list ?? []; |
| | | // èªå¨éä¸å¹¶åæ¥ selectedTask å°ææ°æ°æ® |
| | | if (taskList.value.length) { |
| | | if (selectedTask.value) { |
| | | // å°è¯å¨æ°åè¡¨ä¸æ¾å°å ID çä»»å¡ï¼æ´æ°å¼ç¨ä»¥å·æ°æ°æ® |
| | | const updated = taskList.value.find( |
| | | (t) => t.id === selectedTask.value!.id, |
| | | ); |
| | | selectedTask.value = updated ?? taskList.value[0]; |
| | | } else { |
| | | selectedTask.value = taskList.value[0]; |
| | | } |
| | | } else { |
| | | selectedTask.value = undefined; |
| | | } |
| | | } finally { |
| | | taskLoading.value = false; |
| | | } |
| | | } |
| | | |
| | | // å·¥ä½ç«åæ´æ¶éæ°å 载任å¡å表 |
| | | watch(workstationId, () => { |
| | | loadTaskList(); |
| | | }); |
| | | |
| | | function handleSelectTask(task: PendingTask) { |
| | | selectedTask.value = task; |
| | | } |
| | | |
| | | // --- æ¥å·¥å·æ° --- |
| | | const reportRefreshKey = ref(0); |
| | | |
| | | function onReported() { |
| | | reportRefreshKey.value += 1; |
| | | // å·æ°ä»»å¡åè¡¨ä»¥æ´æ°è¿åº¦ |
| | | loadTaskList(); |
| | | } |
| | | |
| | | // --- è¿åç³»ç» --- |
| | | function goBack() { |
| | | router.push('/'); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="workbench-page"> |
| | | <!-- é¡¶é¨å¯¼èªæ --> |
| | | <header class="workbench-topbar"> |
| | | <div class="workbench-topbar__brand"> |
| | | <IconifyIcon icon="lucide:factory" class="workbench-topbar__logo" /> |
| | | <div class="workbench-topbar__brand-text"> |
| | | <span class="workbench-topbar__title">ç产工ä½å°</span> |
| | | <span v-if="workstationName" class="workbench-topbar__sub"> |
| | | {{ workstationName }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <nav class="workbench-nav"> |
| | | <button |
| | | v-for="tab in tabItems" |
| | | :key="tab.key" |
| | | type="button" |
| | | class="workbench-nav__item" |
| | | :class="{ 'workbench-nav__item--active': activeTab === tab.key }" |
| | | @click="activeTab = tab.key" |
| | | > |
| | | <IconifyIcon :icon="tab.icon" class="workbench-nav__icon" /> |
| | | {{ tab.label }} |
| | | </button> |
| | | </nav> |
| | | |
| | | <Button class="workbench-back-btn" @click="goBack"> |
| | | <template #icon> |
| | | <IconifyIcon icon="lucide:arrow-left" /> |
| | | </template> |
| | | è¿åç³»ç» |
| | | </Button> |
| | | </header> |
| | | |
| | | <!-- 主ä½åºå --> |
| | | <div class="workbench-body"> |
| | | <!-- 左侧边æ --> |
| | | <aside class="workbench-aside"> |
| | | <!-- å·¥ä½ç«éæ©é¢æ¿ --> |
| | | <WorkstationPanel |
| | | :workstation="workstation" |
| | | @change="openWorkstationSelect" |
| | | /> |
| | | |
| | | <!-- å·¥åå表 --> |
| | | <TaskListPanel |
| | | :list="taskList" |
| | | :selected-id="selectedTask?.id" |
| | | :loading="taskLoading" |
| | | @select="handleSelectTask" |
| | | /> |
| | | </aside> |
| | | |
| | | <!-- åå²çº¿ææ½æé®ï¼è§è§è£
é¥°ï¼ --> |
| | | <div class="workbench-split"> |
| | | <div class="workbench-split__line" /> |
| | | </div> |
| | | |
| | | <!-- å³ä¾§ä¸»åºå --> |
| | | <main class="workbench-main"> |
| | | <div class="workbench-main__head"> |
| | | <span class="workbench-main__tab-title">{{ activeTabLabel }}</span> |
| | | <span v-if="selectedTask?.workOrderCode" class="workbench-main__context"> |
| | | å·¥å {{ selectedTask.workOrderCode }} |
| | | </span> |
| | | </div> |
| | | <div class="workbench-main__body"> |
| | | <TaskDetailTab |
| | | v-if="activeTab === 'detail'" |
| | | :task="selectedTask" |
| | | /> |
| | | <FeedbackTab |
| | | v-else-if="activeTab === 'feedback'" |
| | | :task="selectedTask" |
| | | :refresh-key="reportRefreshKey" |
| | | @reported="onReported" |
| | | /> |
| | | <FeedbackHistoryTab |
| | | v-else-if="activeTab === 'history'" |
| | | :task-id="selectedTask?.id" |
| | | :refresh-key="reportRefreshKey" |
| | | @reported="onReported" |
| | | /> |
| | | <RecordParamTab |
| | | v-else-if="activeTab === 'record-param'" |
| | | :work-order-id="selectedTask?.workOrderId" |
| | | /> |
| | | <ProductIssueTab |
| | | v-else-if="activeTab === 'product-issue'" |
| | | :workstation-id="workstationId" |
| | | :workstation-name="workstationName" |
| | | /> |
| | | <ReturnIssueTab |
| | | v-else-if="activeTab === 'return-issue'" |
| | | :workstation-id="workstationId" |
| | | :workstation-name="workstationName" |
| | | /> |
| | | </div> |
| | | </main> |
| | | </div> |
| | | |
| | | <!-- å·¥ä½ç«éæ©å¼¹çª --> |
| | | <WorkstationSelectDialog |
| | | ref="wsSelectDialogRef" |
| | | @select="handleWorkstationSelect" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | $radius: 10px; |
| | | $shadow: 0 2px 12px rgb(0 0 0 / 8%); |
| | | $shadow-sm: 0 1px 4px rgb(0 0 0 / 6%); |
| | | $aside-width: 420px; |
| | | |
| | | .workbench-page { |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 100%; |
| | | height: 100vh; |
| | | overflow: hidden; |
| | | background: #f5f5f5; |
| | | } |
| | | |
| | | // --- é¡¶é¨å¯¼èªæ --- |
| | | .workbench-topbar { |
| | | display: flex; |
| | | align-items: flex-end; |
| | | gap: 20px; |
| | | flex-shrink: 0; |
| | | min-height: 56px; |
| | | padding: 0 20px; |
| | | background: linear-gradient( |
| | | 105deg, |
| | | #1a3a5c 0%, |
| | | #1890ff 42%, |
| | | #40a9ff 100% |
| | | ); |
| | | color: #fff; |
| | | box-shadow: $shadow; |
| | | z-index: 10; |
| | | } |
| | | |
| | | .workbench-topbar__brand { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | flex-shrink: 0; |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .workbench-topbar__logo { |
| | | font-size: 28px; |
| | | filter: brightness(1.1); |
| | | } |
| | | |
| | | .workbench-topbar__brand-text { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 2px; |
| | | } |
| | | |
| | | .workbench-topbar__title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | letter-spacing: 0.02em; |
| | | } |
| | | |
| | | .workbench-topbar__sub { |
| | | font-size: 13px; |
| | | opacity: 0.85; |
| | | } |
| | | |
| | | .workbench-nav { |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | gap: 4px; |
| | | min-width: 0; |
| | | padding-bottom: 0; |
| | | overflow-x: auto; |
| | | scrollbar-width: none; |
| | | |
| | | &::-webkit-scrollbar { |
| | | display: none; |
| | | } |
| | | } |
| | | |
| | | .workbench-nav__item { |
| | | flex-shrink: 0; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | padding: 10px 20px; |
| | | border: none; |
| | | border-radius: 8px 8px 0 0; |
| | | background: transparent; |
| | | color: rgb(255 255 255 / 75%); |
| | | font-size: 15px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | |
| | | &:hover:not(&--active) { |
| | | color: #fff; |
| | | background: rgb(255 255 255 / 12%); |
| | | } |
| | | |
| | | &--active { |
| | | color: #1890ff; |
| | | background: #f5f5f5; |
| | | font-weight: 600; |
| | | box-shadow: 0 -2px 8px rgb(0 0 0 / 6%); |
| | | } |
| | | } |
| | | |
| | | .workbench-nav__icon { |
| | | font-size: 18px; |
| | | } |
| | | |
| | | .workbench-back-btn { |
| | | flex-shrink: 0; |
| | | margin-bottom: 8px; |
| | | border-color: rgb(255 255 255 / 35%) !important; |
| | | background: rgb(255 255 255 / 12%) !important; |
| | | color: #fff !important; |
| | | |
| | | &:hover { |
| | | border-color: rgb(255 255 255 / 55%) !important; |
| | | background: rgb(255 255 255 / 22%) !important; |
| | | } |
| | | } |
| | | |
| | | // --- 主ä½å¸å± --- |
| | | .workbench-body { |
| | | flex: 1; |
| | | min-height: 0; |
| | | display: flex; |
| | | align-items: stretch; |
| | | padding: 12px; |
| | | gap: 0; |
| | | } |
| | | |
| | | .workbench-aside { |
| | | flex-shrink: 0; |
| | | width: $aside-width; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | |
| | | .workbench-split { |
| | | flex-shrink: 0; |
| | | width: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .workbench-split__line { |
| | | width: 1px; |
| | | height: 100%; |
| | | background: #e8e8e8; |
| | | } |
| | | |
| | | .workbench-main { |
| | | flex: 1; |
| | | margin-left: 12px; |
| | | min-width: 0; |
| | | min-height: 0; |
| | | display: flex; |
| | | flex-direction: column; |
| | | background: #fff; |
| | | border-radius: $radius; |
| | | border: 1px solid #f0f0f0; |
| | | box-shadow: $shadow-sm; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .workbench-main__head { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 14px; |
| | | flex-shrink: 0; |
| | | padding: 14px 24px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); |
| | | } |
| | | |
| | | .workbench-main__tab-title { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1890ff; |
| | | } |
| | | |
| | | .workbench-main__context { |
| | | font-size: 14px; |
| | | color: #595959; |
| | | padding: 4px 12px; |
| | | background: #f0f0f0; |
| | | border-radius: 6px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .workbench-main__body { |
| | | flex: 1; |
| | | min-height: 0; |
| | | overflow: hidden; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { MesProTaskApi } from '#/api/mes/pro/task'; |
| | | |
| | | /** å·¥ä½å° Tab é¡µé® */ |
| | | export type WorkbenchTabKey = 'detail' | 'feedback' | 'history' | 'record-param' | 'product-issue' | 'return-issue'; |
| | | |
| | | /** å·¥ä½å° Tab 项 */ |
| | | export interface WorkbenchTabItem { |
| | | key: WorkbenchTabKey; |
| | | label: string; |
| | | icon: string; |
| | | } |
| | | |
| | | /** å¾
æ¥å·¥ä»»å¡ï¼ä» API ç±»åé导åºï¼ä¾¿äºç»ä»¶å¼ç¨ï¼ */ |
| | | export type PendingTask = MesProTaskApi.PendingFeedbackTask; |