| | |
| | | productId?: number; // 产åç¼å· |
| | | productName?: string; // 产ååç§° |
| | | productBarCode?: string; // äº§åæ¡ç |
| | | totalQuantity?: number; // å·²å®¡æ ¸è®¢åéè´æ°é |
| | | avgPrice?: number; // å¹³åä»·æ ¼ |
| | | minPrice?: number; // æä½ä»·æ ¼ |
| | | maxPrice?: number; // æé«ä»·æ ¼ |
| | |
| | | '/erp/purchase-statistics/uncompleted', |
| | | { params }, |
| | | ); |
| | | } |
| | | } |
| | |
| | | workOrderId?: number; // ç产工åç¼å· |
| | | workOrderCode?: string; // ç产工åç¼ç |
| | | activeAlarmCount?: number; // æªæ¶é¤æ¥è¦æ°é |
| | | dataJson?: string; // ç½å
³ä¸æ¥çåå§ç¬æ¶æ°æ® |
| | | } |
| | | |
| | | /** æ°é宿¶æ°æ® */ |
| | |
| | | dataJson?: string; |
| | | } |
| | | |
| | | export interface OfflineInterval { |
| | | startTime: string; |
| | | endTime: string; |
| | | durationSeconds: number; |
| | | } |
| | | export interface OfflineResult { |
| | | firstSampleTime?: string; |
| | | intervals: OfflineInterval[]; |
| | | } |
| | | |
| | | /** æ°éå·¥èºåæ°å¿«ç
§ */ |
| | | export interface CollectTech { |
| | | id?: number; |
| | |
| | | } |
| | | } |
| | | |
| | | /** ææ°é离线æ¶é´æ¥è¯¢åæºåºé´ï¼æ°æ®æ¥èªæ°éåå²ã */ |
| | | export function getCollectOfflineIntervals(params: { deviceCode: string; startTime: string; endTime: string }) { |
| | | return requestClient.get<MesCollectApi.OfflineResult>('/mes/collect/offline/intervals', { params }); |
| | | } |
| | | |
| | | /** æ¥è¯¢æ°é设å¤ç¶æå表 */ |
| | | export function getCollectDeviceList() { |
| | | return requestClient.get<MesCollectApi.CollectDevice[]>( |
| | |
| | | unitMeasureName?: string; // åä½åç§° |
| | | quantity?: number; // ç产æ°é |
| | | quantityProduced?: number; // å·²ç产æ°é |
| | | completionProgressQuantity?: number; // æäº§é¡µé¢ç¶æè¿åº¦å±ç¤ºå£å¾ï¼ä¸å䏿¥å·¥ç»è®¡ |
| | | quantityChanged?: number; // è°æ´æ°é |
| | | quantityScheduled?: number; // å·²æäº§æ°é |
| | | clientId?: number; // 客æ·ç¼å· |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import type { PageParam, PageResult } from '@vben/request'; |
| | | import { requestClient } from '#/api/request'; |
| | | |
| | | export interface SafetyDocument { |
| | | id?: number; |
| | | documentKey?: string; |
| | | revision?: number; |
| | | title: string; |
| | | category: string; |
| | | attachmentUrl: string; |
| | | description?: string; |
| | | effectiveDate?: string; |
| | | status?: string; |
| | | ownerUserId?: number; |
| | | submitterUserId?: number; |
| | | approverUserId?: number; |
| | | approvalTime?: string; |
| | | approvalComment?: string; |
| | | } |
| | | export interface SafetyDocumentApproval { |
| | | id: number; |
| | | approverUserId: number; |
| | | decision: string; |
| | | comment?: string; |
| | | decisionTime: string; |
| | | attachmentUrlSnapshot?: string; |
| | | } |
| | | |
| | | export function getSafetyDocuments(params: PageParam & { title?: string; status?: string }) { |
| | | return requestClient.get<PageResult<SafetyDocument>>('/safety/document/page', { params }); |
| | | } |
| | | export function getSafetyDocumentApprovals(id: number) { |
| | | return requestClient.get<SafetyDocumentApproval[]>('/safety/document/approvals', { params: { id } }); |
| | | } |
| | | export function createSafetyDocument(data: SafetyDocument) { |
| | | return requestClient.post<number>('/safety/document/create', data); |
| | | } |
| | | export function updateSafetyDocument(data: SafetyDocument) { |
| | | return requestClient.put<boolean>('/safety/document/update', data); |
| | | } |
| | | export function submitSafetyDocument(id: number) { |
| | | return requestClient.put<boolean>('/safety/document/submit', undefined, { params: { id } }); |
| | | } |
| | | export function approveSafetyDocument(data: { id: number; approved: boolean; comment?: string }) { |
| | | return requestClient.put<boolean>('/safety/document/approve', data); |
| | | } |
| | | export function reviseSafetyDocument(id: number) { |
| | | return requestClient.post<number>('/safety/document/revision', undefined, { params: { id } }); |
| | | } |
| | |
| | | |
| | | import { useVbenModal } from '@vben/common-ui'; |
| | | import { downloadFileFromBlobPart } from '@vben/utils'; |
| | | import { shallowRef } from 'vue'; |
| | | |
| | | import { Button, message, Upload } from 'ant-design-vue'; |
| | | |
| | |
| | | |
| | | defineOptions({ name: 'WmDocImportForm' }); |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | interface ImportOptions { |
| | | /** ä¸è½½æ¨¡æ¿æ¥å£ */ |
| | | templateApi: () => Promise<Blob | BlobPart | any>; |
| | | /** 导å
¥æ¥å£ */ |
| | | importApi: (file: File) => Promise<any>; |
| | | /** åæ®åç§°ï¼ç¨äºæç¤ºä¸æä»¶å */ |
| | | subject: string; |
| | | /** ä¸ä¼ æ¡æ ç¾ */ |
| | | fileLabel?: string; |
| | | }>(), |
| | | { |
| | | fileLabel: 'Excel æ°æ®', |
| | | }, |
| | | ); |
| | | } |
| | | |
| | | const options = shallowRef<ImportOptions>(); |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | |
| | | }); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | onOpenChange(isOpen) { |
| | | if (isOpen) { |
| | | options.value = modalApi.getData<ImportOptions>(); |
| | | } |
| | | }, |
| | | async onConfirm() { |
| | | const { valid } = await formApi.validate(); |
| | | if (!valid) { |
| | |
| | | modalApi.lock(); |
| | | const data = await formApi.getValues(); |
| | | try { |
| | | const result = (await props.importApi(data.file as File)) as any; |
| | | const result = (await options.value!.importApi(data.file as File)) as any; |
| | | const createCodes: string[] = result?.createCodes ?? []; |
| | | const failureCodes: Record<string, string> = result?.failureCodes ?? {}; |
| | | const failCount = Object.keys(failureCodes).length; |
| | |
| | | // ç»ææç¤ºï¼æå/失败å嫿±æ»ï¼å¤±è´¥æç»æå¤å±ç¤º 5 æ¡ |
| | | const lines: string[] = []; |
| | | if (createCodes.length > 0) { |
| | | lines.push(`æå导å
¥ ${createCodes.length} å¼ ${props.subject}`); |
| | | lines.push(`æå导å
¥ ${createCodes.length} å¼ ${options.value!.subject}`); |
| | | } |
| | | if (failCount > 0) { |
| | | const detail = Object.entries(failureCodes) |
| | |
| | | } else { |
| | | message.warning(lines.join('ï¼')); |
| | | } |
| | | } catch { |
| | | // 请æ±å±å·²æç¤ºé误ï¼ä¿çå¼¹çªä¾ç¨æ·è°æ´æä»¶åéè¯ã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | |
| | | /** ä¸è½½æ¨¡ç */ |
| | | async function handleDownload() { |
| | | const data = await props.templateApi(); |
| | | const data = await options.value!.templateApi(); |
| | | downloadFileFromBlobPart({ |
| | | fileName: `${props.subject}导å
¥æ¨¡æ¿.xls`, |
| | | fileName: `${options.value!.subject}导å
¥æ¨¡æ¿.xls`, |
| | | source: data, |
| | | }); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal :title="`导å
¥${subject}`" class="w-2/5"> |
| | | <Modal :title="`导å
¥${options?.subject ?? ''}`" class="w-2/5"> |
| | | <Form class="mx-4"> |
| | | <template #file> |
| | | <div class="w-full"> |
| | |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | |
| | | import { Tag } from 'ant-design-vue'; |
| | | import { Alert, Tag } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid } from '#/adapter/vxe-table'; |
| | | import { getPriceAnalysis } from '#/api/erp/purchase/statistics'; |
| | |
| | | }, |
| | | { |
| | | field: 'productBarCode', |
| | | title: 'äº§åæ¡ç ', |
| | | title: 'ç©æç¼ç ', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'totalQuantity', |
| | | title: 'éè´æ°é', |
| | | minWidth: 120, |
| | | }, |
| | | { |
| | | field: 'avgPrice', |
| | | title: 'å¹³åä»·æ ¼', |
| | | title: 'å æå¹³åä»·æ ¼', |
| | | minWidth: 120, |
| | | formatter: 'formatAmount2', |
| | | }, |
| | |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <Alert class="mb-4" type="info" show-icon message="ç»è®¡å·²å®¡æ ¸éè´è®¢åçæªç¨åä»·ï¼å¹³åä»·æ ¼ææ°éå æï¼ä»·æ ¼ååçæ¯è¾çéèå´å
æè¿ä¸¤ç¬éè´ä»·æ ¼ã" /> |
| | | <Grid table-title="éè´ä»·æ ¼åæ"> |
| | | <template #priceChangeRate="{ row }"> |
| | | <Tag v-if="row.priceChangeRate > 0" color="red"> |
| | | <Tag v-if="row.priceChangeRate == null">ææ å¯¹æ¯</Tag> |
| | | <Tag v-else-if="row.priceChangeRate > 0" color="red"> |
| | | +{{ row.priceChangeRate }}% |
| | | </Tag> |
| | | <Tag v-else-if="row.priceChangeRate < 0" color="green"> |
| | |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| | | </template> |
| | |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æäº¤æå'); |
| | | } catch { |
| | | // 请æ±å±å±ç¤ºå¤±è´¥åå ï¼ä¿çç»´ä¿®åä¾ä¿®æ¹ã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } catch { |
| | | // 请æ±å±å±ç¤ºå¤±è´¥åå ï¼ä¿çç»´ä¿®åä¾ä¿®æ¹ã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } catch { |
| | | // 请æ±å±å±ç¤ºå¤±è´¥åå ï¼ä¿çéªæ¶çªå£ã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | } |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } catch { |
| | | // 请æ±å±å±ç¤ºå¤±è´¥åå ï¼ä¿çå½å表åã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { onMounted, ref } from 'vue'; |
| | | |
| | | import { Page } from '@vben/common-ui'; |
| | | import { Alert, Button, Card, Col, DatePicker, Modal, Row, Statistic, Table } from 'ant-design-vue'; |
| | | import type { Dayjs } from 'dayjs'; |
| | | import dayjs from 'dayjs'; |
| | | |
| | | import { requestClient } from '#/api/request'; |
| | | import { getCollectDeviceList, getCollectOfflineIntervals } from '#/api/mes/dv/collect'; |
| | | import type { MesCollectApi } from '#/api/mes/dv/collect'; |
| | | |
| | | interface DeviceOee { |
| | | machineryId: number; |
| | | machineryCode: string; |
| | | machineryName: string; |
| | | oee: number; |
| | | availability: number; |
| | | performance: number; |
| | | quality: number; |
| | | taskCount: number; |
| | | plannedQuantity: number; |
| | | producedQuantity: number; |
| | | } |
| | | interface OeeResult { |
| | | overallOee: number; |
| | | overallAvailability: number; |
| | | overallPerformance: number; |
| | | overallQuality: number; |
| | | taskCount: number; |
| | | items: DeviceOee[]; |
| | | } |
| | | |
| | | const loading = ref(false); |
| | | const dates = ref<[Dayjs, Dayjs]>(); |
| | | const result = ref<OeeResult>(); |
| | | const collectDevices = ref<MesCollectApi.CollectDevice[]>([]); |
| | | const collectError = ref(false); |
| | | const collectLoading = ref(false); |
| | | const offlineOpen = ref(false); |
| | | const offlineLoading = ref(false); |
| | | const offlineDevice = ref<MesCollectApi.CollectDevice>(); |
| | | const offlineRange = ref<[Dayjs, Dayjs] | undefined>([dayjs().subtract(1, 'day'), dayjs()]); |
| | | const offlineIntervals = ref<MesCollectApi.OfflineInterval[]>([]); |
| | | const offlineObserved = ref<string>(); |
| | | const offlineQueried = ref(false); |
| | | const columns = [ |
| | | { title: '设å¤ç¼ç ', dataIndex: 'machineryCode' }, |
| | | { title: '设å¤åç§°', dataIndex: 'machineryName' }, |
| | | { title: '综åæçï¼%ï¼', dataIndex: 'oee' }, |
| | | { title: '稼å¨çï¼%ï¼', dataIndex: 'availability' }, |
| | | { title: 'æ§è½å¼å¨çï¼%ï¼', dataIndex: 'performance' }, |
| | | { title: 'è¯åçï¼%ï¼', dataIndex: 'quality' }, |
| | | { title: '任塿°', dataIndex: 'taskCount' }, |
| | | { title: '计å产é', dataIndex: 'plannedQuantity' }, |
| | | { title: 'å®é
产é', dataIndex: 'producedQuantity' }, |
| | | ]; |
| | | const collectColumns = [ |
| | | { title: '设å¤ç¼ç ', dataIndex: 'machineryCode' }, |
| | | { title: '设å¤åç§°', dataIndex: 'machineryName' }, |
| | | { title: 'å¨çº¿ç¶æ', key: 'online' }, |
| | | { title: 'è¿è¡ç¶æ', dataIndex: 'stsName' }, |
| | | { title: 'ééæ°æ®', key: 'readings', width: 300 }, |
| | | { title: '油温', dataIndex: 'ot' }, |
| | | { title: 'æªæ¶é¤æ¥è¦', dataIndex: 'activeAlarmCount' }, |
| | | { title: 'æè¿ééæ¶é´', dataIndex: 'lastCollectTime' }, |
| | | { title: 'æä½', key: 'action', fixed: 'right' as const, width: 110 }, |
| | | ]; |
| | | const offlineColumns = [ |
| | | { title: '离线å¼å§', dataIndex: 'startTime' }, |
| | | { title: 'æ¢å¤ææ¥è¯¢ç»æ', dataIndex: 'endTime' }, |
| | | { title: '离线æ¶é¿', key: 'duration' }, |
| | | ]; |
| | | |
| | | function formatReadings(data?: string) { |
| | | if (!data) return 'ææ æ°æ®'; |
| | | try { |
| | | const values: unknown = JSON.parse(data); |
| | | if (!values || typeof values !== 'object' || Array.isArray(values)) return 'ææ æ°æ®'; |
| | | return Object.entries(values).slice(0, 6).map(([key, value]) => `${key}ï¼${String(value)}`).join('ï¼'); |
| | | } catch { |
| | | return 'ééæ°æ®æ ¼å¼å¼å¸¸'; |
| | | } |
| | | } |
| | | |
| | | async function load() { |
| | | loading.value = true; |
| | | try { |
| | | const range = dates.value; |
| | | result.value = await requestClient.get<OeeResult>('/mes/dv/statistics/oee', { |
| | | params: range ? { |
| | | startTime: range[0].startOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | endTime: range[1].endOf('day').format('YYYY-MM-DD HH:mm:ss'), |
| | | } : {}, |
| | | }); |
| | | } catch { |
| | | result.value = undefined; |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | } |
| | | async function loadCollect() { |
| | | collectLoading.value = true; |
| | | try { |
| | | collectDevices.value = await getCollectDeviceList(); |
| | | collectError.value = false; |
| | | } catch { |
| | | collectDevices.value = []; |
| | | collectError.value = true; |
| | | } finally { |
| | | collectLoading.value = false; |
| | | } |
| | | } |
| | | async function loadOfflineIntervals() { |
| | | if (!offlineDevice.value?.collectDeviceCode || !offlineRange.value) return; |
| | | offlineLoading.value = true; |
| | | try { |
| | | const data = await getCollectOfflineIntervals({ |
| | | deviceCode: offlineDevice.value.collectDeviceCode, |
| | | startTime: offlineRange.value[0].format('YYYY-MM-DD HH:mm:ss'), |
| | | endTime: offlineRange.value[1].format('YYYY-MM-DD HH:mm:ss'), |
| | | }); |
| | | offlineIntervals.value = data.intervals; |
| | | offlineObserved.value = data.firstSampleTime; |
| | | offlineQueried.value = true; |
| | | } catch { |
| | | offlineIntervals.value = []; |
| | | offlineQueried.value = false; |
| | | } finally { |
| | | offlineLoading.value = false; |
| | | } |
| | | } |
| | | function openOffline(device: MesCollectApi.CollectDevice) { |
| | | offlineDevice.value = device; |
| | | offlineRange.value = [dayjs().subtract(1, 'day'), dayjs()]; |
| | | offlineIntervals.value = []; |
| | | offlineObserved.value = undefined; |
| | | offlineQueried.value = false; |
| | | offlineOpen.value = true; |
| | | void loadOfflineIntervals(); |
| | | } |
| | | function durationText(seconds: number) { |
| | | const minutes = Math.floor(seconds / 60); |
| | | return `${Math.floor(minutes / 60)}å°æ¶${minutes % 60}å${seconds % 60}ç§`; |
| | | } |
| | | onMounted(() => { void load(); void loadCollect(); }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <div class="space-y-4"> |
| | | <Alert type="info" show-icon message="å½åææ æç产任å¡è®¡ååæ¥å·¥è®°å½ä¼°ç®ï¼æªæ¥å
¥è®¾å¤å®æ¶è¿è¡ååæºæ°æ®ã" /> |
| | | <Card> |
| | | <div class="mb-4 flex items-center gap-3"> |
| | | <span>ä»»å¡å®ææ¥æ</span> |
| | | <DatePicker.RangePicker v-model:value="dates" /> |
| | | <Button type="primary" :loading="loading" @click="load">æ¥è¯¢</Button> |
| | | </div> |
| | | <Row :gutter="16"> |
| | | <Col :span="6"><Statistic title="综åæç" :value="result?.overallOee ?? 0" suffix="%" /></Col> |
| | | <Col :span="6"><Statistic title="稼å¨ç" :value="result?.overallAvailability ?? 0" suffix="%" /></Col> |
| | | <Col :span="6"><Statistic title="æ§è½å¼å¨ç" :value="result?.overallPerformance ?? 0" suffix="%" /></Col> |
| | | <Col :span="6"><Statistic title="è¯åç" :value="result?.overallQuality ?? 0" suffix="%" /></Col> |
| | | </Row> |
| | | </Card> |
| | | <Card title="è®¾å¤æç»"> |
| | | <Table :columns="columns" :data-source="result?.items ?? []" :loading="loading" row-key="machineryId" :scroll="{ x: 1000 }" /> |
| | | </Card> |
| | | <Card title="è®¾å¤æ°é宿¶ç¶æ"> |
| | | <template #extra><Button :loading="collectLoading" @click="loadCollect">å·æ°</Button></template> |
| | | <Alert v-if="collectError" type="warning" show-icon message="æ°éç¶æå 载失败ï¼è¯·æ£æ¥æ°éæå¡åéè¯ã" class="mb-4" /> |
| | | <Table :columns="collectColumns" :data-source="collectDevices" :loading="collectLoading" row-key="machineryId" :scroll="{ x: 900 }"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'online'">{{ record.online ? 'å¨çº¿' : '离线' }}</template> |
| | | <template v-else-if="column.key === 'readings'">{{ formatReadings(record.dataJson) }}</template> |
| | | <template v-else-if="column.key === 'action'"><Button type="link" @click="openOffline(record)">åæºè®°å½</Button></template> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | <Modal v-model:open="offlineOpen" :title="`${offlineDevice?.machineryName ?? ''} - ç¦»çº¿åæºè®°å½`" :footer="null" width="800px"> |
| | | <Alert class="mb-4" type="info" show-icon message="ææ°é䏿è¶
è¿ 5 åé计ç®ï¼éä¿¡ä¸æä¹ä¼è®¡å
¥ã馿¬¡ééå䏿¨æï¼è®°å½åæ°éåå²ä¿çæéå¶ï¼å次æå¤æ¥è¯¢ 7 天ã" /> |
| | | <Alert v-if="offlineQueried && !offlineObserved" class="mb-4" type="warning" show-icon message="è¯¥æ¶æ®µæ²¡æå¯ç¨éæ ·è®°å½ï¼æ æ³å¤æåæºæ
åµã" /> |
| | | <Alert v-else-if="offlineQueried && offlineObserved && offlineRange && dayjs(offlineObserved).isAfter(offlineRange[0])" class="mb-4" type="warning" show-icon message="æ¥è¯¢å¼å§æ¶æ²¡æåå²éæ ·ï¼é¦æ¬¡éæ ·åçç¶ææ æ³å¤æã" /> |
| | | <div class="mb-4 flex items-center gap-3"> |
| | | <DatePicker.RangePicker v-model:value="offlineRange" show-time /> |
| | | <Button type="primary" :loading="offlineLoading" @click="loadOfflineIntervals">æ¥è¯¢</Button> |
| | | <span v-if="offlineQueried && offlineObserved">åºé´å计ï¼{{ durationText(offlineIntervals.reduce((sum, item) => sum + item.durationSeconds, 0)) }}</span> |
| | | </div> |
| | | <Table :columns="offlineColumns" :data-source="offlineIntervals" :loading="offlineLoading" row-key="startTime"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'duration'">{{ durationText(record.durationSeconds) }}</template> |
| | | </template> |
| | | </Table> |
| | | </Modal> |
| | | </div> |
| | | </Page> |
| | | </template> |
| | |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success('æ¥å·¥åå·²æäº¤'); |
| | | } catch { |
| | | // 请æ±å±å·²æç¤ºå¤±è´¥åå ï¼ä¿çå¼¹çªä¾ç¨æ·ä¿®æ¹åéè¯ã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |
| | |
| | | // }, |
| | | { |
| | | field: 'quantityProduced', |
| | | title: 'å·²ç产æ°é', |
| | | title: 'å®é
æ¥å·¥æ°é', |
| | | width: 100, |
| | | }, |
| | | { |
| | | field: 'progressPercent', |
| | | title: '宿è¿åº¦', |
| | | title: 'ç¶æè¿åº¦', |
| | | width: 150, |
| | | slots: { default: 'progressPercent' }, |
| | | }, |
| | |
| | | } |
| | | |
| | | /** |
| | | * å·²å®æç±»åè¡çâ宿æâå
åºå±ç¤ºï¼ |
| | | * å·²ç产æ°éæè®¢åæ°éå±ç¤ºï¼è¥ä½äºè®¢åæ°éï¼ï¼ä½¿å®æè¿åº¦æ¾ç¤ºä¸º 100%ã |
| | | * ä»
ä½ç¨äºæ¬é¡µå表å±ç¤ºï¼ä¸ä¿®æ¹åç«¯æ°æ®ï¼ä¸å½±ååºå/æ¥å·¥/导åºççå®å£å¾ã |
| | | * 已宿工åçç¶æè¿åº¦æå®æç¶æå±ç¤ºï¼å®é
æ¥å·¥æ°éä¿çæ¥å£åå¼ã |
| | | */ |
| | | function normalizeCompletionDisplay(row: MesProWorkOrderApi.WorkOrder): void { |
| | | if (!isCompletedWorkOrder(row)) { |
| | | return; |
| | | } |
| | | const quantity = row.quantity ?? 0; |
| | | const produced = row.quantityProduced ?? 0; |
| | | if (produced < quantity) { |
| | | row.quantityProduced = quantity; |
| | | } |
| | | Object.assign(row, { completionProgressQuantity: isCompletedWorkOrder(row) ? Math.max(produced, quantity) : produced }); |
| | | } |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | |
| | | type: MesProWorkOrderTypeEnum.SELF, |
| | | ...formValues, |
| | | }); |
| | | // å·²å®æç±»åè¡ï¼å·²ç产æ°é/宿è¿åº¦åâ宿æâå
åºå±ç¤ºï¼ä»
æ¬é¡µå±ç¤ºï¼ä¸æ¹åºï¼ |
| | | // ç¶æè¿åº¦ä¸å®é
æ¥å·¥æ°éåå«å±ç¤ºã |
| | | if (res?.list) { |
| | | res.list.forEach(normalizeCompletionDisplay); |
| | | } |
| | |
| | | </template> |
| | | <template #progressPercent="{ row }"> |
| | | <Progress |
| | | :percent="Math.min(100, Math.round((row.quantity ? ((row.quantityProduced || 0) / row.quantity) * 100 : 0)))" |
| | | :percent="Math.min(100, Math.round((row.quantity ? ((row.completionProgressQuantity || 0) / row.quantity) * 100 : 0)))" |
| | | size="small" |
| | | /> |
| | | </template> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import type { SafetyDocument, SafetyDocumentApproval } from '#/api/safety/document'; |
| | | import { onMounted, reactive, ref } from 'vue'; |
| | | import { Page } from '@vben/common-ui'; |
| | | import { Alert, Button, Card, DatePicker, Form, FormItem, Input, message, Modal, Select, Space, Table, Tag } from 'ant-design-vue'; |
| | | import { uploadSafetyFiles } from '#/api/safety'; |
| | | import { approveSafetyDocument, createSafetyDocument, getSafetyDocumentApprovals, getSafetyDocuments, reviseSafetyDocument, submitSafetyDocument, updateSafetyDocument } from '#/api/safety/document'; |
| | | |
| | | defineOptions({ name: 'SafetyDocument' }); |
| | | const rows = ref<SafetyDocument[]>([]); |
| | | const loading = ref(false); |
| | | const saving = ref(false); |
| | | const uploading = ref(false); |
| | | const open = ref(false); |
| | | const approvalOpen = ref(false); |
| | | const historyOpen = ref(false); |
| | | const approvalHistory = ref<SafetyDocumentApproval[]>([]); |
| | | const approvalRow = ref<SafetyDocument>(); |
| | | const approvalComment = ref(''); |
| | | const filters = reactive<{ title?: string; status?: string }>({}); |
| | | const paging = reactive({ current: 1, pageSize: 10, total: 0 }); |
| | | const model = reactive<SafetyDocument>({ title: '', category: 'å®å
¨ç®¡çå¶åº¦', attachmentUrl: '' }); |
| | | const states: Record<string, string> = { DRAFT: 'è稿', PENDING: 'å¾
审æ¹', APPROVED: 'å·²æ¹å', REJECTED: '已驳å', SUPERSEDED: 'åå²çæ¬' }; |
| | | const columns = [ |
| | | { title: 'å¶åº¦åç§°', dataIndex: 'title' }, { title: 'ç±»å«', dataIndex: 'category' }, |
| | | { title: 'çæ¬', dataIndex: 'revision' }, { title: 'çææ¥æ', dataIndex: 'effectiveDate' }, |
| | | { title: 'ç¶æ', key: 'status' }, { title: 'éä»¶', key: 'file' }, |
| | | { title: 'æè¿å®¡æ¹æè§', dataIndex: 'approvalComment' }, { title: 'æä½', key: 'action', width: 280 }, |
| | | ]; |
| | | |
| | | async function load(reset = false) { |
| | | if (reset) paging.current = 1; |
| | | loading.value = true; |
| | | try { |
| | | const result = await getSafetyDocuments({ pageNo: paging.current, pageSize: paging.pageSize, ...filters }); |
| | | rows.value = result.list; paging.total = result.total; |
| | | } catch { rows.value = []; } finally { loading.value = false; } |
| | | } |
| | | function edit(row?: SafetyDocument) { |
| | | Object.assign(model, { id: undefined, title: '', category: 'å®å
¨ç®¡çå¶åº¦', attachmentUrl: '', description: '', effectiveDate: undefined }, row || {}); |
| | | open.value = true; |
| | | } |
| | | async function upload(event: Event) { |
| | | const input = event.target as HTMLInputElement; |
| | | const file = input.files?.[0]; |
| | | if (!file) return; |
| | | uploading.value = true; |
| | | try { const urls = await uploadSafetyFiles([file], 'safety/document'); model.attachmentUrl = urls[0] || ''; } |
| | | catch { /* 请æ±å±å±ç¤ºé误 */ } |
| | | finally { uploading.value = false; input.value = ''; } |
| | | } |
| | | async function save() { |
| | | if (!model.title.trim() || !model.category.trim() || !model.attachmentUrl.trim()) { message.warning('请填åå¶åº¦åç§°ãç±»å«å¹¶ä¸ä¼ éä»¶'); return; } |
| | | saving.value = true; |
| | | try { |
| | | const data = { id: model.id, title: model.title, category: model.category, attachmentUrl: model.attachmentUrl, description: model.description, effectiveDate: model.effectiveDate }; |
| | | if (model.id) await updateSafetyDocument(data); else await createSafetyDocument(data); |
| | | message.success('å¶åº¦è稿已ä¿å'); open.value = false; await load(); |
| | | } catch { /* 请æ±å±å±ç¤ºé误 */ } finally { saving.value = false; } |
| | | } |
| | | async function submit(row: SafetyDocument) { |
| | | if (!row.id) return; |
| | | try { await submitSafetyDocument(row.id); message.success('å·²æäº¤å®¡æ¹'); await load(); } catch { /* 请æ±å±å±ç¤ºé误 */ } |
| | | } |
| | | async function revise(row: SafetyDocument) { |
| | | if (!row.id) return; |
| | | try { await reviseSafetyDocument(row.id); message.success('å·²å建修订è稿'); await load(true); } catch { /* 请æ±å±å±ç¤ºé误 */ } |
| | | } |
| | | function openApproval(row: SafetyDocument) { approvalRow.value = row; approvalComment.value = ''; approvalOpen.value = true; } |
| | | async function showHistory(row: SafetyDocument) { |
| | | if (!row.id) return; |
| | | try { approvalHistory.value = await getSafetyDocumentApprovals(row.id); historyOpen.value = true; } catch { /* 请æ±å±å±ç¤ºé误 */ } |
| | | } |
| | | async function approve(approved: boolean) { |
| | | if (!approvalRow.value?.id) return; |
| | | if (!approved && !approvalComment.value.trim()) { message.warning('é©³åæ¶è¯·å¡«åå®¡æ¹æè§'); return; } |
| | | saving.value = true; |
| | | try { await approveSafetyDocument({ id: approvalRow.value.id, approved, comment: approvalComment.value }); message.success(approved ? '审æ¹éè¿' : '已驳å'); approvalOpen.value = false; await load(); } |
| | | catch { /* 请æ±å±å±ç¤ºé误 */ } finally { saving.value = false; } |
| | | } |
| | | onMounted(() => { void load(); }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Page title="å®å
¨å¶åº¦æ¡£æ¡" description="ç»´æ¤å®å
¨å¶åº¦éä»¶ï¼æäº¤å®¡æ¹å¹¶ä¿çåå²çæ¬ã" auto-content-height> |
| | | <Card> |
| | | <div class="mb-4 flex flex-wrap items-center gap-3"> |
| | | <Input v-model:value="filters.title" placeholder="å¶åº¦åç§°" style="width: 200px" allow-clear /> |
| | | <Select v-model:value="filters.status" placeholder="ç¶æ" style="width: 130px" allow-clear :options="Object.entries(states).map(([value, label]) => ({ value, label }))" /> |
| | | <Button type="primary" @click="load(true)">æ¥è¯¢</Button><Button v-access:code="['safety:document:create']" @click="edit()">æ°å¢å¶åº¦</Button> |
| | | </div> |
| | | <Table :columns="columns" :data-source="rows" :loading="loading" :pagination="paging" row-key="id" :scroll="{ x: 1200 }" @change="p => { paging.current = p.current || 1; paging.pageSize = p.pageSize || 10; load(); }"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <Tag v-if="column.key === 'status'">{{ states[record.status] || record.status }}</Tag> |
| | | <a v-else-if="column.key === 'file'" :href="record.attachmentUrl" target="_blank" rel="noopener noreferrer">æ¥çéä»¶</a> |
| | | <Space v-else-if="column.key === 'action'"> |
| | | <Button type="link" @click="showHistory(record as SafetyDocument)">审æ¹è®°å½</Button> |
| | | <Button v-if="['DRAFT', 'REJECTED'].includes(record.status)" v-access:code="['safety:document:update']" type="link" @click="edit(record as SafetyDocument)">ç¼è¾</Button> |
| | | <Button v-if="record.status === 'DRAFT'" v-access:code="['safety:document:update']" type="link" @click="submit(record as SafetyDocument)">æäº¤</Button> |
| | | <Button v-if="record.status === 'PENDING'" v-access:code="['safety:document:approve']" type="link" @click="openApproval(record as SafetyDocument)">审æ¹</Button> |
| | | <Button v-if="record.status === 'APPROVED'" v-access:code="['safety:document:create']" type="link" @click="revise(record as SafetyDocument)">å建修订</Button> |
| | | </Space> |
| | | </template> |
| | | </Table> |
| | | </Card> |
| | | <Modal v-model:open="open" title="å¶åº¦è稿" :confirm-loading="saving" ok-text="ä¿å" @ok="save"> |
| | | <Form layout="vertical"> |
| | | <FormItem label="å¶åº¦åç§°" required><Input v-model:value="model.title" :maxlength="200" /></FormItem> |
| | | <FormItem label="ç±»å«" required><Input v-model:value="model.category" :maxlength="50" /></FormItem> |
| | | <FormItem label="çææ¥æ" extra="æä¸æ¯ææªæ¥æ¥æèªå¨çæï¼è®¾ç½®æªæ¥æ¥ææ¶ï¼è¯·å¨çææ¥æå½å¤©å®¡æ¹ã"><DatePicker v-model:value="model.effectiveDate" value-format="YYYY-MM-DD" class="w-full" /></FormItem> |
| | | <FormItem label="å¶åº¦éä»¶" required><input type="file" accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png" :disabled="uploading" @change="upload" /><div v-if="model.attachmentUrl">éä»¶å·²ä¸ä¼ </div></FormItem> |
| | | <FormItem label="说æ"><Input.TextArea v-model:value="model.description" :maxlength="2000" :rows="3" /></FormItem> |
| | | </Form> |
| | | </Modal> |
| | | <Modal v-model:open="approvalOpen" title="审æ¹å¶åº¦" :footer="null"> |
| | | <Alert type="info" show-icon message="æäº¤äººä¸è½å®¡æ¹èªå·±çå¶åº¦ã" class="mb-4" /> |
| | | <Input.TextArea v-model:value="approvalComment" placeholder="å®¡æ¹æè§ï¼é©³åæ¶å¿
å¡«" :maxlength="500" :rows="3" /> |
| | | <div class="mt-4 flex justify-end gap-2"><Button :loading="saving" danger @click="approve(false)">驳å</Button><Button :loading="saving" type="primary" @click="approve(true)">éè¿</Button></div> |
| | | </Modal> |
| | | <Modal v-model:open="historyOpen" title="审æ¹è®°å½" :footer="null"> |
| | | <Table :data-source="approvalHistory" row-key="id" :pagination="false" :columns="[{ title: '审æ¹äººè´¦å·ç¼å·', dataIndex: 'approverUserId' }, { title: 'ç»æ', key: 'decision' }, { title: 'æè§', dataIndex: 'comment' }, { title: 'æ¶é´', dataIndex: 'decisionTime' }, { title: 'å®¡æ¹æ¶éä»¶', key: 'attachment' }]"> |
| | | <template #bodyCell="{ column, record }"> |
| | | <template v-if="column.key === 'decision'">{{ states[record.decision] || record.decision }}</template> |
| | | <a v-else-if="column.key === 'attachment' && record.attachmentUrlSnapshot" :href="record.attachmentUrlSnapshot" target="_blank" rel="noopener noreferrer">æ¥ç</a> |
| | | </template> |
| | | </Table> |
| | | </Modal> |
| | | </Page> |
| | | </template> |
| | |
| | | message.success('已忴¾'); |
| | | assignOpen.value = false; |
| | | await load(); |
| | | } catch { |
| | | // 请æ±å±æç¤ºåå ï¼ä¿çåæ´¾çªå£ä¾ç¨æ·ä¿®æ£ã |
| | | } finally { |
| | | saving.value = false; |
| | | } |
| | |
| | | message.warning('è¯¥ç©æå·²æ·»å ï¼è¯·å¿é夿·»å '); |
| | | return; |
| | | } |
| | | if (materialForm.value.id) { |
| | | await updateMaterial(materialForm.value); |
| | | } else { |
| | | await createMaterial(materialForm.value); |
| | | } |
| | | materialFormVisible.value = false; |
| | | loadMaterials(); |
| | | try { |
| | | if (materialForm.value.id) await updateMaterial(materialForm.value); |
| | | else await createMaterial(materialForm.value); |
| | | materialFormVisible.value = false; |
| | | await loadMaterials(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误ï¼ä¿ç表å */ } |
| | | } |
| | | function onMaterialSelect(item?: MdmItemApi.Item) { |
| | | if (item) { |
| | |
| | | try { bidList.value = await getBidListByProject(projectId); } finally { bidLoading.value = false; } |
| | | } |
| | | async function handleCreateBid() { |
| | | await createBid(bidForm.value); |
| | | bidFormVisible.value = false; |
| | | loadBids(); |
| | | try { |
| | | await createBid(bidForm.value); |
| | | bidFormVisible.value = false; |
| | | await loadBids(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误ï¼ä¿ç表å */ } |
| | | } |
| | | async function handleWithdrawBid(id: number) { |
| | | await withdrawBid(id); |
| | |
| | | bidTotalDraft.value = bid?.bidTotalAmount ?? undefined; |
| | | } |
| | | async function handleSaveQuote() { |
| | | if (quoteForm.value.id) { |
| | | await updateQuote(quoteForm.value); |
| | | } else { |
| | | await createQuote(quoteForm.value); |
| | | } |
| | | quoteFormVisible.value = false; |
| | | loadQuotes(selectedBidId.value); |
| | | try { |
| | | if (quoteForm.value.id) await updateQuote(quoteForm.value); |
| | | else await createQuote(quoteForm.value); |
| | | quoteFormVisible.value = false; |
| | | await loadQuotes(selectedBidId.value); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误ï¼ä¿ç表å */ } |
| | | } |
| | | function handleAddQuote() { |
| | | quoteForm.value = { bidId: selectedBidId.value!, tenderMaterialId: undefined! }; |
| | |
| | | </a-modal> |
| | | </template> |
| | | </Page> |
| | | </template> |
| | | </template> |
| | |
| | | await deleteTenderProject(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.tenderName])); |
| | | handleRefresh(); |
| | | } catch { |
| | | // 请æ±å±å·²æç¤ºå¤±è´¥åå ã |
| | | } finally { |
| | | hideLoading(); |
| | | } |
| | | } |
| | | |
| | | async function handlePublish(row: SrmTenderApi.TenderProjectVO) { |
| | | await publishTender(row.id!); |
| | | message.success('å叿å'); |
| | | handleRefresh(); |
| | | try { |
| | | await publishTender(row.id!); message.success('å叿å'); handleRefresh(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误 */ } |
| | | } |
| | | |
| | | async function handleStartBidding(row: SrmTenderApi.TenderProjectVO) { |
| | | await startBidding(row.id!); |
| | | message.success('å·²å¼å§ææ '); |
| | | handleRefresh(); |
| | | try { |
| | | await startBidding(row.id!); message.success('å·²å¼å§ææ '); handleRefresh(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误 */ } |
| | | } |
| | | |
| | | async function handleStartEvaluation(row: SrmTenderApi.TenderProjectVO) { |
| | | await startEvaluation(row.id!); |
| | | message.success('å·²å¼å§è¯æ '); |
| | | handleRefresh(); |
| | | try { |
| | | await startEvaluation(row.id!); message.success('å·²å¼å§è¯æ '); handleRefresh(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误 */ } |
| | | } |
| | | |
| | | async function handleClose(row: SrmTenderApi.TenderProjectVO) { |
| | | await closeTender(row.id!); |
| | | message.success('å·²å
³é'); |
| | | handleRefresh(); |
| | | try { |
| | | await closeTender(row.id!); message.success('å·²å
³é'); handleRefresh(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误 */ } |
| | | } |
| | | |
| | | async function handleGeneratePurchaseOrder(row: SrmTenderApi.TenderProjectVO) { |
| | | if (!row.awardId) return; |
| | | await generatePurchaseOrder(row.awardId); |
| | | message.success('éè´è®¢åå·²çæ'); |
| | | handleRefresh(); |
| | | try { |
| | | await generatePurchaseOrder(row.awardId); message.success('éè´è®¢åå·²çæ'); handleRefresh(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误 */ } |
| | | } |
| | | |
| | | async function handleStartPriceComparing(row: SrmTenderApi.TenderProjectVO) { |
| | | await startPriceComparing(row.id!); |
| | | message.success('å·²å¼å§æ¯ä»·'); |
| | | handleRefresh(); |
| | | try { |
| | | await startPriceComparing(row.id!); message.success('å·²å¼å§æ¯ä»·'); handleRefresh(); |
| | | } catch { /* 请æ±å±å·²æç¤ºé误 */ } |
| | | } |
| | | |
| | | // ç¡®è®¤ä¸æ ç¸å
³ |
| | |
| | | label: `${b.supplierName} - ${b.bidNo} (Â¥${b.bidTotalAmount ?? 0})`, |
| | | value: b.id!, |
| | | })); |
| | | } catch { |
| | | return; |
| | | } finally { |
| | | confirmLoading.value = false; |
| | | } |
| | |
| | | message.success('ç¡®è®¤ä¸æ æåï¼éè´è®¢åå·²èªå¨çæ'); |
| | | confirmModalApi.close(); |
| | | handleRefresh(); |
| | | } catch { |
| | | // 请æ±å±å·²æç¤ºå¤±è´¥åå ï¼ä¿çå¼¹çªã |
| | | } finally { |
| | | confirmLoading.value = false; |
| | | } |
| | |
| | | await modalApi.close(); |
| | | emit('success'); |
| | | message.success($t('ui.actionMessage.operationSuccess')); |
| | | } catch { |
| | | // 请æ±å±å·²æç¤ºå¤±è´¥åå ï¼ä¿çå½å表åã |
| | | } finally { |
| | | modalApi.unlock(); |
| | | } |