| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup lang="ts"> |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | import { useAccess } from '@vben/access'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { Button, DatePicker, Form, Input, InputNumber, message, Modal, Popconfirm, Select, Space, Table, Upload } from 'ant-design-vue'; |
| | | import dayjs from 'dayjs'; |
| | | import { requestClient } from '#/api/request'; |
| | | import { getSimpleUserList } from '#/api/system/user'; |
| | | import { getItem } from '#/api/mdm/item'; |
| | | import { bindAttachments, listAttachments, uploadFile } from '#/api/system/storage'; |
| | | import { MdmItemSelect } from '#/views/basicData/mdm/components'; |
| | | import QcAttachments from '../components/qc-attachments.vue'; |
| | | |
| | | defineOptions({ name: 'MesQcPatrol' }); |
| | | interface Patrol { id?: number; code?: string; name?: string; itemId?: number; reelNo?: string; batchCode?: string; qualifiedQuantity?: number; unqualifiedQuantity?: number; inspectDate?: string; inspectorUserId?: number; remark?: string } |
| | | const { hasAccessByCodes, hasAccessByRoles } = useAccess(); |
| | | const can = (action: string) => hasAccessByRoles(['super_admin']) || hasAccessByCodes([`mes:qc-patrol:${action}`]); |
| | | const itemNames = ref<Record<number, string>>({}); |
| | | const rows = ref<Patrol[]>([]); const users = ref<any[]>([]); const loading = ref(false); const saving = ref(false); const open = ref(false); |
| | | const query = reactive({ pageNo: 1, pageSize: 10, total: 0, code: '', reelNo: '' }); |
| | | const form = reactive<Patrol>({}); |
| | | const pendingFiles = ref<{ id: number; name: string }[]>([]); |
| | | const uploading = ref(false); |
| | | const attachmentVersion = ref(0); |
| | | async function addPendingFile(file: File) { |
| | | if (uploading.value || pendingFiles.value.length >= 50) { if (pendingFiles.value.length >= 50) message.warning('æå¤ä¸ä¼ 50 份éä»¶'); return Upload.LIST_IGNORE; } |
| | | uploading.value = true; |
| | | try { const result = await uploadFile([file]); const blob = result?.[0]; if (!blob?.id) throw new Error('ä¸ä¼ ç»æç¼ºå°æä»¶ç¼å·'); pendingFiles.value.push({ id: blob.id, name: file.name }); message.success('éä»¶å·²ä¸ä¼ '); } finally { uploading.value = false; } |
| | | return Upload.LIST_IGNORE; |
| | | } |
| | | async function bindPendingFiles() { |
| | | if (pendingFiles.value.length && form.id) { |
| | | const params = { recordType: 'mes_qc_patrol', recordId: form.id, application: 'file' }; |
| | | const existing = await listAttachments(params); |
| | | await bindAttachments({ ...params, blobItems: [...existing.map(file => ({ blobId: file.id })), ...pendingFiles.value.map(file => ({ blobId: file.id }))] }); |
| | | pendingFiles.value = []; |
| | | } |
| | | attachmentVersion.value++; |
| | | } |
| | | const columns = [ { title: 'å·¡æ£åå·', dataIndex: 'code' }, { title: 'å·¡æ£åç§°', dataIndex: 'name' }, { title: 'ç©æåç§°', dataIndex: 'itemId', key: 'itemId' }, { title: 'çå·', dataIndex: 'reelNo' }, { title: 'æ¹æ¬¡å·', dataIndex: 'batchCode' }, { title: 'åæ ¼æ°', dataIndex: 'qualifiedQuantity' }, { title: 'ä¸åæ ¼æ°', dataIndex: 'unqualifiedQuantity' }, { title: 'å·¡æ£æ¥æ', dataIndex: 'inspectDate' }, { title: 'æä½', key: 'action', width: 145 } ]; |
| | | async function load() { loading.value=true; try { const res = await requestClient.get('/mes/qc/patrol/page', { params: { pageNo: query.pageNo, pageSize: query.pageSize, code: query.code || undefined, reelNo: query.reelNo || undefined } }); rows.value=res.list; query.total=res.total; await Promise.all([...new Set<number>(res.list.map((row: Patrol) => row.itemId).filter(Boolean))].map(async id => { if (!itemNames.value[id]) { const item = await getItem(id); itemNames.value[id] = item?.name || String(id); } })); } finally { loading.value=false; } } |
| | | async function edit(row?: Patrol) { pendingFiles.value = []; Object.keys(form).forEach(k => delete (form as any)[k]); if (row?.id) { const data = await requestClient.get('/mes/qc/patrol/get', {params:{id:row.id}}); Object.assign(form, data, { inspectDate: dayjs(data.inspectDate).format('YYYY-MM-DDTHH:mm:ss') }); } else Object.assign(form, {qualifiedQuantity:0, unqualifiedQuantity:0, inspectDate:dayjs().format('YYYY-MM-DDTHH:mm:ss')}); open.value=true; } |
| | | async function save() { if (!form.code?.trim() || !form.name?.trim() || !form.itemId || !form.inspectorUserId || !form.inspectDate || form.qualifiedQuantity == null || form.unqualifiedQuantity == null || form.qualifiedQuantity + form.unqualifiedQuantity <= 0) { message.warning('请填ååå·ãåç§°ãç©æãå·¡æ£äººåãæ¥æåæææ°é'); return; } saving.value=true; try { if(form.id) await requestClient.put('/mes/qc/patrol/update',form); else { form.id = await requestClient.post('/mes/qc/patrol/create',form); } await bindPendingFiles(); message.success('ä¿åæå'); await load(); } finally { saving.value=false; } } |
| | | async function remove(id: number) { await requestClient.delete('/mes/qc/patrol/delete',{params:{id}}); message.success('å 餿å'); await load(); } |
| | | onMounted(async () => { await Promise.all([load(), getSimpleUserList().then(data => { users.value = data; })]); }); |
| | | </script> |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="patrol-page"> |
| | | <section class="patrol-search" aria-label="å·¡æ£æ¥è¯¢æ¡ä»¶"> |
| | | <div class="search-fields"> |
| | | <label class="search-field"><span>å·¡æ£åå·</span><Input v-model:value="query.code" placeholder="请è¾å
¥å·¡æ£åå·" allow-clear @press-enter="query.pageNo = 1; load()" /></label> |
| | | <label class="search-field"><span>çå·</span><Input v-model:value="query.reelNo" placeholder="请è¾å
¥çå·" allow-clear @press-enter="query.pageNo = 1; load()" /></label> |
| | | </div> |
| | | <Space class="search-actions"> |
| | | <Button @click="query.code = ''; query.reelNo = ''; query.pageNo = 1; load()">éç½®</Button> |
| | | <Button type="primary" :loading="loading" @click="query.pageNo = 1; load()">æ¥è¯¢</Button> |
| | | </Space> |
| | | </section> |
| | | |
| | | <section class="patrol-list" aria-label="å·¡æ£è®°å½å表"> |
| | | <div class="list-heading"> |
| | | <div><h2>å·¡æ£è®°å½</h2><p>å
± {{ query.total }} æ¡è®°å½</p></div> |
| | | <Space><Button :loading="loading" @click="load()">å·æ°</Button><Button v-if="can('create')" type="primary" @click="edit()">æ°å¢å·¡æ£</Button></Space> |
| | | </div> |
| | | <Table row-key="id" :columns="columns" :data-source="rows" :loading="loading" :scroll="{ x: 1100 }" :pagination="{current:query.pageNo,pageSize:query.pageSize,total:query.total,showSizeChanger:true,showTotal:(total:number)=>`å
± ${total} æ¡è®°å½`}" @change="(p:any) => {query.pageNo=p.current; query.pageSize=p.pageSize; load();}"> |
| | | <template #bodyCell="{column,record}"> |
| | | <template v-if="column.key==='itemId'">{{ itemNames[record.itemId] || record.itemId }}</template> |
| | | <template v-else-if="column.dataIndex==='inspectDate'">{{ record.inspectDate ? dayjs(record.inspectDate).format('YYYY-MM-DD HH:mm') : 'â' }}</template> |
| | | <template v-else-if="column.dataIndex==='reelNo' || column.dataIndex==='batchCode'">{{ record[column.dataIndex] || 'â' }}</template> |
| | | <template v-else-if="column.key==='action'"><Space :size="0"><Button v-if="can('update')" type="link" @click="edit(record)">ä¿®æ¹</Button><Popconfirm v-if="can('delete')" title="确认å 餿¤å·¡æ£è®°å½ï¼" @confirm="remove(record.id)"><Button type="link" danger>å é¤</Button></Popconfirm></Space></template> |
| | | </template> |
| | | <template #emptyText><div class="empty-state"><p>ææ å·¡æ£è®°å½</p><span>æ°å¢å·¡æ£åï¼è®°å½ä¼æ¾ç¤ºå¨è¿é</span><Button v-if="can('create')" type="link" @click="edit()">æ°å¢å·¡æ£</Button></div></template> |
| | | </Table> |
| | | </section> |
| | | </div> |
| | | <Modal v-model:open="open" :title="form.id ? 'ä¿®æ¹å·¡æ£è®°å½' : 'æ°å¢å·¡æ£è®°å½'" :width="850" :confirm-loading="saving" :closable="!uploading && !saving" :mask-closable="!uploading && !saving" :cancel-button-props="{ disabled: uploading || saving }" :ok-button-props="{ disabled: uploading }" ok-text="ä¿å" @ok="save"> |
| | | <div class="patrol-modal"> |
| | | <div class="form-heading"><h3>åºæ¬ä¿¡æ¯</h3><p>å¡«åå·¡æ£å¯¹è±¡åè®°å½ä¿¡æ¯</p></div> |
| | | <Form layout="vertical" class="patrol-form"> |
| | | <Form.Item label="å·¡æ£åå·" required><Input v-model:value="form.code" :maxlength="100" placeholder="请è¾å
¥å·¡æ£åå·" /></Form.Item> |
| | | <Form.Item label="å·¡æ£åç§°" required><Input v-model:value="form.name" :maxlength="255" placeholder="请è¾å
¥å·¡æ£åç§°" /></Form.Item> |
| | | <Form.Item label="ç©æ" required><MdmItemSelect v-model="form.itemId" /></Form.Item> |
| | | <Form.Item label="çå·"><Input v-model:value="form.reelNo" :maxlength="128" placeholder="请è¾å
¥çå·" /></Form.Item> |
| | | <Form.Item label="æ¹æ¬¡å·"><Input v-model:value="form.batchCode" :maxlength="128" placeholder="请è¾å
¥æ¹æ¬¡å·" /></Form.Item> |
| | | <Form.Item label="å·¡æ£äººå" required><Select v-model:value="form.inspectorUserId" show-search option-filter-prop="label" placeholder="è¯·éæ©å·¡æ£äººå" :options="users.map(u=>({value:u.id,label:u.nickname}))" /></Form.Item> |
| | | <Form.Item label="åæ ¼æ°é" required><InputNumber v-model:value="form.qualifiedQuantity" :min="0" class="w-full" /></Form.Item> |
| | | <Form.Item label="ä¸åæ ¼æ°é" required><InputNumber v-model:value="form.unqualifiedQuantity" :min="0" class="w-full" /></Form.Item> |
| | | <Form.Item label="å·¡æ£æ¥æ" required><DatePicker v-model:value="form.inspectDate" show-time value-format="YYYY-MM-DDTHH:mm:ss" class="w-full" /></Form.Item> |
| | | <Form.Item label="夿³¨"><Input v-model:value="form.remark" :maxlength="1000" placeholder="请è¾å
¥å¤æ³¨" /></Form.Item> |
| | | </Form> |
| | | <div class="attachment-section"><div class="form-heading"><h3>å·¡æ£éä»¶</h3><p>ä¸ä¼ å·¡æ£ç¸å
³æä»¶</p></div><QcAttachments v-if="form.id" :key="attachmentVersion" qc-type="patrol" :qc-id="form.id" /><Upload v-else :show-upload-list="false" :before-upload="addPendingFile" :disabled="uploading"><Button :loading="uploading">ä¸ä¼ å·¡æ£éä»¶</Button></Upload><div v-for="file in pendingFiles" :key="file.id" class="pending-file"><span>{{ file.name }}ï¼çå¾
ä¿åå
³èï¼</span><Button type="link" danger @click="pendingFiles = pendingFiles.filter(item => item.id !== file.id)">ç§»é¤</Button></div></div> |
| | | </div> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | | |
| | | <style scoped> |
| | | .patrol-page { display: flex; flex-direction: column; gap: 16px; min-height: 100%; } |
| | | .patrol-search, .patrol-list { background: hsl(var(--card)); border-radius: 8px; padding: 20px 24px; } |
| | | .patrol-search { display: flex; align-items: end; justify-content: space-between; gap: 24px; } |
| | | .search-fields { display: grid; grid-template-columns: repeat(2, minmax(180px, 280px)); gap: 24px; flex: 1; } |
| | | .search-field { display: grid; gap: 8px; color: hsl(var(--foreground)); font-weight: 500; } |
| | | .search-actions { flex-shrink: 0; } |
| | | .patrol-list { flex: 1; min-width: 0; } |
| | | .list-heading { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 20px; } |
| | | .list-heading h2 { margin: 0; font-size: 16px; font-weight: 600; } |
| | | .list-heading p, .form-heading p { margin: 4px 0 0; color: hsl(var(--muted-foreground)); font-size: 13px; } |
| | | .empty-state { display: flex; min-height: 180px; flex-direction: column; align-items: center; justify-content: center; gap: 6px; color: hsl(var(--muted-foreground)); } |
| | | .empty-state p { margin: 0; color: hsl(var(--foreground)); font-weight: 500; } |
| | | .empty-state span { font-size: 13px; } |
| | | .patrol-modal { max-height: 65vh; overflow-y: auto; padding: 8px 4px 0; } |
| | | .form-heading { margin-bottom: 16px; } |
| | | .form-heading h3 { margin: 0; font-size: 15px; font-weight: 600; } |
| | | .patrol-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; } |
| | | .attachment-section { border-top: 1px solid hsl(var(--border)); padding-top: 20px; margin-top: 4px; } |
| | | .pending-file { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 8px; } |
| | | .pending-file span { overflow-wrap: anywhere; } |
| | | @media (max-width: 768px) { .patrol-search { align-items: stretch; flex-direction: column; gap: 16px; } .search-fields { grid-template-columns: 1fr; gap: 12px; } .search-actions { align-self: end; } .patrol-search, .patrol-list { padding: 16px; } .patrol-form { grid-template-columns: 1fr; } } |
| | | </style> |