已删除1个文件
已修改10个文件
已添加13个文件
| | |
| | | "js-beautify": "1.14.11", |
| | | "js-cookie": "3.0.5", |
| | | "jsencrypt": "3.3.2", |
| | | "jszip": "^3.10.1", |
| | | "nprogress": "0.2.0", |
| | | "pinia": "2.1.7", |
| | | "print-js": "^1.6.0", |
| | |
| | | method: 'get', |
| | | params: query, |
| | | }) |
| | | } |
| | | } |
| | | |
| | | export function save(data){ |
| | | return request({ |
| | | url: '/staff/staffOnJob/save', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup> |
| | | import {computed, ref, reactive, defineEmits,watch} from "vue"; |
| | | import {getToken} from "@/utils/auth.js"; |
| | | import axios from "axios"; |
| | | import {ElMessage} from "element-plus"; |
| | | |
| | | const emit = defineEmits(['update:fileList', 'onUpload',"update:ids"]); |
| | | |
| | | const props = defineProps({ |
| | | action: {type: String, default: "/common/minioUploads"}, |
| | | fileList: {type: Array, default: () => []}, |
| | | statusType: {type: Number, default: 0} |
| | | }) |
| | | |
| | | const localFileList = ref([...props.fileList]) |
| | | |
| | | |
| | | const headers = computed(() => ({Authorization: "Bearer " + getToken()})); |
| | | const uploadFileUrl = computed(() => import.meta.env.VITE_APP_BASE_API + props.action); |
| | | |
| | | const handleChange = () => { |
| | | emit('update:ids', localFileList.value.map(item => item.id)) |
| | | emit('update:fileList', localFileList.value) |
| | | emit('onUpload', localFileList.value) |
| | | } |
| | | const handleUploadSuccess = (res, file) => { |
| | | // console.log(res) |
| | | localFileList.value.push(...res.data.map((it) => { |
| | | return { |
| | | id: it.id, |
| | | url: it.downloadUrl, |
| | | name: it.originalFilename, |
| | | status: "success", |
| | | uid: file.uid |
| | | } |
| | | })) |
| | | handleChange() |
| | | } |
| | | |
| | | const handleUploadPreview = (it) => { |
| | | const link = document.createElement("a"); |
| | | if (it.url) { |
| | | link.href = it.url |
| | | } else { |
| | | link.href = localFileList.value.find(fl => fl.uid === it.uid).url; |
| | | } |
| | | link.download = it.name; |
| | | link.click(); |
| | | } |
| | | |
| | | const handleUploadRemove = (it) => { |
| | | localFileList.value = localFileList.value.filter(f => f.uid !== it.uid); |
| | | handleChange() |
| | | } |
| | | |
| | | watch( |
| | | () => props.fileList, |
| | | (val) => { |
| | | localFileList.value = [...val] |
| | | }, |
| | | { immediate: true, deep: true } |
| | | ) |
| | | |
| | | // æä»¶ä¸ä¼ å¤ç |
| | | const UploadImage = (param) => { |
| | | const formData = new FormData(); |
| | | formData.append("files", param.file); |
| | | formData.append("type", props.statusType); |
| | | axios.post(uploadFileUrl.value, formData, { |
| | | headers: { |
| | | "Content-Type": "multipart/form-data", |
| | | ...headers.value, |
| | | }, |
| | | onUploadProgress: (progressEvent) => { |
| | | const percent = Math.round((progressEvent.loaded * 100) / progressEvent.total); |
| | | param.onProgress({percent}); |
| | | }, |
| | | }) |
| | | .then((response) => { |
| | | if (response.data.code === 200) { |
| | | handleUploadSuccess(response.data, param.file); |
| | | ElMessage.success("ä¸ä¼ æå"); |
| | | } else { |
| | | param.onError(new Error(response.data.msg)); |
| | | ElMessage.error(response.data.msg); |
| | | } |
| | | }) |
| | | .catch((error) => { |
| | | param.onError(error); |
| | | }); |
| | | }; |
| | | |
| | | </script> |
| | | |
| | | <template> |
| | | <div class="upload-file"> |
| | | <el-upload |
| | | class="upload-demo" |
| | | drag |
| | | :fileList="localFileList" |
| | | :action="props.action" |
| | | :headers="headers" |
| | | :http-request="UploadImage" |
| | | :on-success="handleUploadSuccess" |
| | | :on-remove="handleUploadRemove" |
| | | :on-preview="handleUploadPreview" |
| | | multiple> |
| | | <i class="el-icon-upload"></i> |
| | | <div class="el-upload__text">å°æä»¶æå°æ¤å¤ï¼æ<em>ç¹å»ä¸ä¼ </em></div> |
| | | </el-upload> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | |
| | | |
| | | |
| | | </style> |
| | |
| | | :fixed="item.fixed" |
| | | :label="item.label" |
| | | :prop="item.prop" |
| | | show-overflow-tooltip |
| | | :show-overflow-tooltip="item.dataType !== 'multiTagLink'" |
| | | :align="item.align" |
| | | :sortable="!!item.sortable" |
| | | :type="item.type" |
| | |
| | | style="width: 40px; height: 40px; margin-top: 10px" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- tag --> |
| | | <div v-else-if="item.dataType == 'tag'"> |
| | | <el-tag |
| | | v-if=" |
| | | typeof dataTypeFn(scope.row[item.prop], item.formatData) === |
| | | 'string' |
| | | " |
| | | :title="formatters(scope.row[item.prop], item.formatData)" |
| | | :type="formatType(scope.row[item.prop], item.formatType)" |
| | | <div v-else-if="item.dataType === 'multiTagLink'"> |
| | | <el-tooltip |
| | | v-for="(file, index) in dataTypeFnArray(scope.row[item.prop], item.formatData)" |
| | | :key="index" |
| | | :content="file.name" |
| | | effect="dark" |
| | | placement="top" |
| | | > |
| | | {{ formatters(scope.row[item.prop], item.formatData) }} |
| | | </el-tag> |
| | | |
| | | <el-tag |
| | | v-for="(tag, index) in dataTypeFn( |
| | | scope.row[item.prop], |
| | | item.formatData |
| | | )" |
| | | v-else-if=" |
| | | typeof dataTypeFn(scope.row[item.prop], item.formatData) === |
| | | 'object' |
| | | " |
| | | :key="index" |
| | | :title="formatters(scope.row[item.prop], item.formatData)" |
| | | :type="formatType(tag, item.formatType)" |
| | | > |
| | | {{ item.tagGroup ? tag[item.tagGroup.label] ?? tag : tag }} |
| | | </el-tag> |
| | | |
| | | <el-tag |
| | | v-else |
| | | :title="formatters(scope.row[item.prop], item.formatData)" |
| | | :type="formatType(scope.row[item.prop], item.formatType)" |
| | | > |
| | | {{ formatters(scope.row[item.prop], item.formatData) }} |
| | | </el-tag> |
| | | <el-tag |
| | | type="info" |
| | | style="margin-right: 4px;margin-top: 4px; cursor: pointer;" |
| | | @click="downloadFile(file)" |
| | | > |
| | | {{ truncateName(file.name, 5) }} |
| | | </el-tag> |
| | | </el-tooltip> |
| | | </div> |
| | | |
| | | <!-- æé® --> |
| | |
| | | return format(val); |
| | | } else return val; |
| | | }; |
| | | const dataTypeFnArray = (val, format) => { |
| | | if (!val) return []; |
| | | if (typeof format === "function") { |
| | | return format(val); |
| | | } |
| | | // ä¿è¯è¿åçæ¯æ°ç» |
| | | return Array.isArray(val) ? val : []; |
| | | }; |
| | | |
| | | const truncateName = (name, length = 5) => { |
| | | if (!name) return ''; |
| | | return name.length > length ? name.slice(0, length) + '...' : name; |
| | | }; |
| | | |
| | | |
| | | const downloadFile = (file) => { |
| | | const link = document.createElement("a"); |
| | | link.href = file.url; |
| | | // 设置ä¸è½½æä»¶å为 file.name |
| | | link.download = file.name; |
| | | link.click(); |
| | | }; |
| | | |
| | | |
| | | const formatType = (val, format) => { |
| | | if (typeof format === "function") { |
| | |
| | | import { saveAs } from 'file-saver' |
| | | import { getToken } from '@/utils/auth' |
| | | import errorCode from '@/utils/errorCode' |
| | | import request from '@/utils/request' |
| | | import { blobValidate } from '@/utils/ruoyi' |
| | | |
| | | const baseURL = import.meta.env.VITE_APP_BASE_API |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | export function findFileListByIds(data){ |
| | | return request({ |
| | | url: "/common/findFileListByIds", |
| | | method: "post", |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | |
| | |
| | | width:200 |
| | | }, |
| | | { |
| | | label: "éä»¶", |
| | | prop: "attachFileList", |
| | | width:220, |
| | | dataType: "multiTagLink" |
| | | } |
| | | , |
| | | { |
| | | label: "ç»è®°æ¥æ", |
| | | prop: "createTime", |
| | | width:300 |
| | |
| | | tableData.value = res.records; |
| | | page.total = res.total; |
| | | }); |
| | | tableLoading.value = false; |
| | | }; |
| | | // å表åè®¡æ¹æ³ |
| | | const summarizeMainTable1 = (param) => { |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogVisible" |
| | | @update:model-value="$emit('update:dialogVisible', $event)" |
| | | title="æ¹éä¸è½½å票" |
| | | width="800px" |
| | | :close-on-click-modal="false" |
| | | :close-on-press-escape="false" |
| | | > |
| | | <div class="batch-download-container"> |
| | | <!-- å票åè¡¨ä¿¡æ¯ --> |
| | | <el-card class="invoice-list" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>å¾
ä¸è½½å票å表</span> |
| | | <el-tag type="info" size="small" style="margin-left: 10px"> |
| | | å
± {{ invoices.length }} å¼ å票 |
| | | </el-tag> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="invoices" style="width: 100%" max-height="300"> |
| | | <el-table-column prop="invoiceNo" label="å票å·ç " width="120" /> |
| | | <el-table-column prop="buyerName" label="è´ä¹°æ¹" width="150" /> |
| | | <el-table-column prop="amount" label="éé¢" width="100"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.amount.toFixed(2) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)" size="small"> |
| | | {{ getStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- ä¸è½½é项 --> |
| | | <el-card class="download-options" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header">ä¸è½½é项</div> |
| | | </template> |
| | | |
| | | <el-form :model="downloadOptions" label-width="120px"> |
| | | <!-- æä»¶æ ¼å¼ --> |
| | | <el-form-item label="æä»¶æ ¼å¼"> |
| | | <el-radio-group v-model="downloadOptions.format"> |
| | | <el-radio label="html">HTMLæ ¼å¼</el-radio> |
| | | <el-radio label="excel">Excelæ ¼å¼</el-radio> |
| | | <el-radio label="zip">ZIPå缩å
</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <!-- ä¸è½½å
容 --> |
| | | <el-form-item label="ä¸è½½å
容"> |
| | | <el-checkbox-group v-model="downloadOptions.content"> |
| | | <el-checkbox label="invoice">åç¥¨æ£æ¬</el-checkbox> |
| | | <el-checkbox label="details">æç»æ¸
å</el-checkbox> |
| | | <el-checkbox label="summary">æ±æ»æ¥è¡¨</el-checkbox> |
| | | </el-checkbox-group> |
| | | </el-form-item> |
| | | |
| | | <!-- æä»¶å½å --> |
| | | <el-form-item label="æä»¶å½å"> |
| | | <el-select v-model="downloadOptions.naming" style="width: 100%"> |
| | | <el-option label="å票å·ç _è´ä¹°æ¹åç§°" value="invoice_buyer" /> |
| | | <el-option label="å票å·ç _æ¥æ" value="invoice_date" /> |
| | | <el-option label="è´ä¹°æ¹åç§°_æ¥æ" value="buyer_date" /> |
| | | <el-option label="èªå®ä¹åç¼" value="custom" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <!-- èªå®ä¹åç¼ --> |
| | | <el-form-item v-if="downloadOptions.naming === 'custom'" label="èªå®ä¹åç¼"> |
| | | <el-input v-model="downloadOptions.customPrefix" placeholder="请è¾å
¥æä»¶åç¼" /> |
| | | </el-form-item> |
| | | |
| | | <!-- å缩é项 --> |
| | | <el-form-item label="å缩é项"> |
| | | <el-checkbox v-model="downloadOptions.compress">å¯ç¨å缩</el-checkbox> |
| | | <el-checkbox v-model="downloadOptions.password">设置解åå¯ç </el-checkbox> |
| | | </el-form-item> |
| | | |
| | | <!-- è§£åå¯ç --> |
| | | <el-form-item v-if="downloadOptions.password" label="è§£åå¯ç "> |
| | | <el-input v-model="downloadOptions.extractPassword" placeholder="请è¾å
¥è§£åå¯ç " /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- ä¸è½½è¿åº¦ --> |
| | | <el-card v-if="downloading" class="download-progress" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header">ä¸è½½è¿åº¦</div> |
| | | </template> |
| | | |
| | | <div class="progress-content"> |
| | | <el-progress |
| | | :percentage="downloadProgress" |
| | | :status="downloadProgress === 100 ? 'success' : ''" |
| | | :stroke-width="20" |
| | | /> |
| | | <div class="progress-text"> |
| | | {{ downloadProgress === 100 ? 'ä¸è½½å®æ' : `æ£å¨ä¸è½½... ${downloadProgress}%` }} |
| | | </div> |
| | | <div class="progress-detail"> |
| | | {{ downloadProgress === 100 ? 'ææå票ä¸è½½å®æ' : `å·²ä¸è½½ ${downloadedCount} å¼ ï¼å©ä½ ${invoices.length - downloadedCount} å¼ ` }} |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose" :disabled="downloading">åæ¶</el-button> |
| | | <el-button |
| | | type="primary" |
| | | @click="handleBatchDownload" |
| | | :loading="downloading" |
| | | :disabled="!canDownload" |
| | | > |
| | | {{ downloading ? 'ä¸è½½ä¸...' : 'å¼å§ä¸è½½' }} |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch } from 'vue'; |
| | | import { ElMessage } from 'element-plus'; |
| | | |
| | | // å®ä¹props |
| | | const props = defineProps({ |
| | | dialogVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | invoices: { |
| | | type: Array, |
| | | default: () => [] |
| | | } |
| | | }); |
| | | |
| | | // å®ä¹emits |
| | | const emit = defineEmits(['update:dialogVisible', 'success']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const downloading = ref(false); |
| | | const downloadProgress = ref(0); |
| | | const downloadedCount = ref(0); |
| | | const downloadOptions = ref({ |
| | | format: 'pdf', |
| | | content: ['invoice'], |
| | | naming: 'invoice_buyer', |
| | | customPrefix: '', |
| | | compress: true, |
| | | password: false, |
| | | extractPassword: '' |
| | | }); |
| | | |
| | | // 计ç®å±æ§ |
| | | const canDownload = computed(() => { |
| | | return props.invoices.length > 0 && !downloading.value; |
| | | }); |
| | | |
| | | // çå¬å¨ |
| | | watch(() => props.invoices, () => { |
| | | // éç½®ä¸è½½ç¶æ |
| | | downloading.value = false; |
| | | downloadProgress.value = 0; |
| | | downloadedCount.value = 0; |
| | | }, { immediate: true }); |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const statusMap = { |
| | | 'issued': 'success', |
| | | 'pending': 'warning', |
| | | 'cancelled': 'danger' |
| | | }; |
| | | return statusMap[status] || 'info'; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | 'issued': 'å·²å¼ç¥¨', |
| | | 'pending': 'å¾
å¼ç¥¨', |
| | | 'cancelled': 'å·²ä½åº' |
| | | }; |
| | | return statusMap[status] || 'æªç¥'; |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | if (!downloading.value) { |
| | | emit('update:dialogVisible', false); |
| | | } |
| | | }; |
| | | |
| | | // æ¹éä¸è½½ |
| | | const handleBatchDownload = async () => { |
| | | if (downloading.value) return; |
| | | |
| | | try { |
| | | downloading.value = true; |
| | | downloadProgress.value = 0; |
| | | downloadedCount.value = 0; |
| | | |
| | | // æ¹éä¸è½½è¿ç¨ |
| | | if (downloadOptions.format === 'zip') { |
| | | // ZIPæ ¼å¼ï¼çæå个å缩å
|
| | | await generateZIPFile(); |
| | | downloadProgress.value = 100; |
| | | downloadedCount.value = props.invoices.length; |
| | | } else { |
| | | // å
¶ä»æ ¼å¼ï¼é个ä¸è½½æä»¶ |
| | | const totalInvoices = props.invoices.length; |
| | | const progressStep = 100 / totalInvoices; |
| | | |
| | | for (let i = 0; i < totalInvoices; i++) { |
| | | // çæå个å票æä»¶ |
| | | await generateInvoiceFile(props.invoices[i], i); |
| | | |
| | | downloadedCount.value++; |
| | | downloadProgress.value = Math.round((i + 1) * progressStep); |
| | | |
| | | // çæå»¶è¿ï¼é¿å
æµè§å¨é»å¡ |
| | | await new Promise(resolve => setTimeout(resolve, 200)); |
| | | } |
| | | |
| | | // çææ±æ»æä»¶ |
| | | if (downloadOptions.content.includes('summary')) { |
| | | await generateSummaryFile(); |
| | | } |
| | | } |
| | | |
| | | // ä¸è½½å®æ |
| | | ElMessage.success(`æåä¸è½½ ${totalInvoices} å¼ å票`); |
| | | emit('success'); |
| | | |
| | | } catch (error) { |
| | | console.error('æ¹éä¸è½½å¤±è´¥:', error); |
| | | ElMessage.error("æ¹éä¸è½½å¤±è´¥ï¼è¯·éè¯"); |
| | | downloading.value = false; |
| | | downloadProgress.value = 0; |
| | | downloadedCount.value = 0; |
| | | } |
| | | }; |
| | | |
| | | // çæå个å票æä»¶ |
| | | const generateInvoiceFile = async (invoice, index) => { |
| | | try { |
| | | let fileContent, fileName, mimeType; |
| | | |
| | | if (downloadOptions.format === 'html') { |
| | | fileContent = generateHTMLContent(invoice); |
| | | fileName = `${getFileName(invoice, index)}.html`; |
| | | mimeType = 'text/html'; |
| | | } else if (downloadOptions.content.includes('details')) { |
| | | fileContent = generateExcelContent(invoice); |
| | | fileName = `${getFileName(invoice, index)}.csv`; |
| | | mimeType = 'text/csv'; |
| | | } else if (downloadOptions.format === 'excel') { |
| | | fileContent = generateExcelContent(invoice); |
| | | fileName = `${getFileName(invoice, index)}.csv`; |
| | | mimeType = 'text/csv'; |
| | | } else if (downloadOptions.format === 'zip') { |
| | | // ZIPæ ¼å¼éè¦ç¹æ®å¤çï¼è¿éå
è·³è¿ |
| | | return; |
| | | } |
| | | |
| | | // å建Blob对象 |
| | | const blob = new Blob([fileContent], { type: mimeType }); |
| | | |
| | | // å建ä¸è½½é¾æ¥ |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement('a'); |
| | | link.href = url; |
| | | link.download = fileName; |
| | | |
| | | // 触åä¸è½½ |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | |
| | | // æ¸
ç |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | |
| | | } catch (error) { |
| | | console.error(`çæå票æä»¶å¤±è´¥: ${invoice.invoiceNo}`, error); |
| | | } |
| | | }; |
| | | |
| | | // çæZIPå缩å
|
| | | const generateZIPFile = async () => { |
| | | try { |
| | | // 卿坼å
¥JSZipåº |
| | | const JSZip = await import('jszip'); |
| | | const zip = new JSZip.default(); |
| | | |
| | | // æ·»å å票æä»¶å°ZIP |
| | | props.invoices.forEach((invoice, index) => { |
| | | let fileContent, fileName; |
| | | |
| | | if (downloadOptions.content.includes('invoice')) { |
| | | if (downloadOptions.content.includes('details')) { |
| | | fileContent = generateExcelContent(invoice); |
| | | fileName = `${getFileName(invoice, index)}.csv`; |
| | | } else { |
| | | fileContent = generateHTMLContent(invoice); |
| | | fileName = `${getFileName(invoice, index)}.html`; |
| | | } |
| | | zip.file(fileName, fileContent); |
| | | } |
| | | }); |
| | | |
| | | // æ·»å æ±æ»æä»¶ |
| | | if (downloadOptions.content.includes('summary')) { |
| | | const summaryContent = generateSummaryContent(); |
| | | zip.file('åç¥¨æ±æ».csv', summaryContent); |
| | | } |
| | | |
| | | // çæZIPæä»¶ |
| | | const zipBlob = await zip.generateAsync({ |
| | | type: 'blob', |
| | | compression: downloadOptions.compress ? 'DEFLATE' : 'STORE' |
| | | }); |
| | | |
| | | // ä¸è½½ZIPæä»¶ |
| | | const fileName = `å票æ¹éä¸è½½_${new Date().toISOString().split('T')[0]}.zip`; |
| | | const url = window.URL.createObjectURL(zipBlob); |
| | | const link = document.createElement('a'); |
| | | link.href = url; |
| | | link.download = fileName; |
| | | |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | |
| | | } catch (error) { |
| | | console.error('çæZIPæä»¶å¤±è´¥:', error); |
| | | ElMessage.error('ZIPæä»¶çæå¤±è´¥ï¼è¯·æ£æ¥æ¯å¦å®è£
äºjszipåº'); |
| | | } |
| | | }; |
| | | |
| | | // çææ±æ»æä»¶ |
| | | const generateSummaryFile = async () => { |
| | | try { |
| | | const summaryContent = generateSummaryContent(); |
| | | const fileName = `åç¥¨æ±æ»_${new Date().toISOString().split('T')[0]}.csv`; |
| | | |
| | | const blob = new Blob([summaryContent], { type: 'text/csv;charset=utf-8;' }); |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement('a'); |
| | | link.href = url; |
| | | link.download = fileName; |
| | | |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | |
| | | } catch (error) { |
| | | console.error('çææ±æ»æä»¶å¤±è´¥:', error); |
| | | } |
| | | }; |
| | | |
| | | // è·åæä»¶å |
| | | const getFileName = (invoice, index) => { |
| | | let fileName = ''; |
| | | |
| | | if (downloadOptions.naming === 'invoice_buyer') { |
| | | fileName = `${invoice.invoiceNo}_${invoice.buyerName}`; |
| | | } else if (downloadOptions.naming === 'invoice_date') { |
| | | fileName = `${invoice.invoiceNo}_${invoice.invoiceDate}`; |
| | | } else if (downloadOptions.naming === 'buyer_date') { |
| | | fileName = `${invoice.buyerName}_${invoice.invoiceDate}`; |
| | | } else if (downloadOptions.naming === 'custom') { |
| | | fileName = `${downloadOptions.customPrefix || 'å票'}_${index + 1}`; |
| | | } |
| | | |
| | | // æ¸
çæä»¶åä¸çç¹æ®å符 |
| | | return fileName.replace(/[<>:"/\\|?*]/g, '_'); |
| | | }; |
| | | |
| | | // çæHTMLå
容 |
| | | const generateHTMLContent = (invoice) => { |
| | | const content = `<!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | | <title>åç¥¨ä¿¡æ¯ - ${invoice.invoiceNo || 'N/A'}</title> |
| | | <style> |
| | | body { |
| | | font-family: 'Microsoft YaHei', Arial, sans-serif; |
| | | margin: 0; |
| | | padding: 20px; |
| | | background-color: #f5f5f5; |
| | | } |
| | | .invoice-container { |
| | | max-width: 800px; |
| | | margin: 0 auto; |
| | | background: white; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| | | overflow: hidden; |
| | | } |
| | | .invoice-header { |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: white; |
| | | padding: 30px; |
| | | text-align: center; |
| | | } |
| | | .invoice-header h1 { |
| | | margin: 0; |
| | | font-size: 28px; |
| | | font-weight: 300; |
| | | } |
| | | .invoice-header .subtitle { |
| | | margin-top: 10px; |
| | | opacity: 0.9; |
| | | font-size: 16px; |
| | | } |
| | | .invoice-content { |
| | | padding: 30px; |
| | | } |
| | | .info-section { |
| | | margin-bottom: 25px; |
| | | } |
| | | .info-section h3 { |
| | | color: #333; |
| | | border-bottom: 2px solid #667eea; |
| | | padding-bottom: 8px; |
| | | margin-bottom: 15px; |
| | | } |
| | | .info-grid { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 15px; |
| | | } |
| | | .info-item { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 12px; |
| | | background: #f8f9fa; |
| | | border-radius: 6px; |
| | | border-left: 4px solid #667eea; |
| | | } |
| | | .info-label { |
| | | font-weight: bold; |
| | | color: #555; |
| | | min-width: 100px; |
| | | } |
| | | .info-value { |
| | | color: #333; |
| | | margin-left: 10px; |
| | | } |
| | | .amount-section { |
| | | background: #f8f9fa; |
| | | padding: 20px; |
| | | border-radius: 8px; |
| | | margin-top: 20px; |
| | | } |
| | | .amount-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, 1fr); |
| | | gap: 20px; |
| | | } |
| | | .amount-item { |
| | | text-align: center; |
| | | padding: 15px; |
| | | background: white; |
| | | border-radius: 6px; |
| | | box-shadow: 0 1px 3px rgba(0,0,0,0.1); |
| | | } |
| | | .amount-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | margin-bottom: 8px; |
| | | } |
| | | .amount-value { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #667eea; |
| | | } |
| | | .footer { |
| | | text-align: center; |
| | | padding: 20px; |
| | | color: #666; |
| | | border-top: 1px solid #eee; |
| | | margin-top: 20px; |
| | | } |
| | | @media print { |
| | | body { background: white; } |
| | | .invoice-container { box-shadow: none; } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div class="invoice-container"> |
| | | <div class="invoice-header"> |
| | | <h1>å票信æ¯</h1> |
| | | <div class="subtitle">Invoice Information</div> |
| | | </div> |
| | | |
| | | <div class="invoice-content"> |
| | | <div class="info-section"> |
| | | <h3>åºæ¬ä¿¡æ¯</h3> |
| | | <div class="info-grid"> |
| | | <div class="info-item"> |
| | | <span class="info-label">å票å·ç :</span> |
| | | <span class="info-value">${invoice.invoiceNo || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">å票代ç :</span> |
| | | <span class="info-value">${invoice.invoiceCode || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">å¼ç¥¨æ¥æ:</span> |
| | | <span class="info-value">${invoice.invoiceDate || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">ç¶æ:</span> |
| | | <span class="info-value">${getStatusText(invoice.status)}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="info-section"> |
| | | <h3>è´ä¹°æ¹ä¿¡æ¯</h3> |
| | | <div class="info-grid"> |
| | | <div class="info-item"> |
| | | <span class="info-label">è´ä¹°æ¹åç§°:</span> |
| | | <span class="info-value">${invoice.buyerName || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">è´ä¹°æ¹ç¨å·:</span> |
| | | <span class="info-value">${invoice.buyerTaxNo || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">è´ä¹°æ¹å°å:</span> |
| | | <span class="info-value">${invoice.buyerAddress || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">é¶è¡è´¦æ·:</span> |
| | | <span class="info-value">${invoice.buyerBankAccount || 'N/A'}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="info-section"> |
| | | <h3>é宿¹ä¿¡æ¯</h3> |
| | | <div class="info-grid"> |
| | | <div class="info-item"> |
| | | <span class="info-label">é宿¹åç§°:</span> |
| | | <span class="info-value">${invoice.sellerName || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">é宿¹ç¨å·:</span> |
| | | <span class="info-value">${invoice.sellerTaxNo || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">é宿¹å°å:</span> |
| | | <span class="info-value">${invoice.sellerAddress || 'N/A'}</span> |
| | | </div> |
| | | <div class="info-item"> |
| | | <span class="info-label">é¶è¡è´¦æ·:</span> |
| | | <span class="info-value">${invoice.sellerBankAccount || 'N/A'}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="amount-section"> |
| | | <h3>éé¢ä¿¡æ¯</h3> |
| | | <div class="amount-grid"> |
| | | <div class="amount-item"> |
| | | <div class="amount-label">éé¢</div> |
| | | <div class="amount-value">Â¥${(invoice.amount || 0).toFixed(2)}</div> |
| | | </div> |
| | | <div class="info-item"> |
| | | <div class="amount-label">ç¨é¢</div> |
| | | <div class="amount-value">Â¥${(invoice.taxAmount || 0).toFixed(2)}</div> |
| | | </div> |
| | | <div class="amount-item"> |
| | | <div class="amount-label">ä»·ç¨å计</div> |
| | | <div class="amount-value">Â¥${(invoice.totalAmount || 0).toFixed(2)}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="footer"> |
| | | <p>çææ¶é´: ${new Date().toLocaleString()}</p> |
| | | <p>æ¤æä»¶ç±ç³»ç»èªå¨çæ</p> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </body> |
| | | </html>`; |
| | | |
| | | return content; |
| | | }; |
| | | |
| | | // çæExcelå
容 |
| | | const generateExcelContent = (invoice) => { |
| | | const content = `åç¥¨ä¿¡æ¯ |
| | | å票å·ç ,${invoice.invoiceNo || 'N/A'} |
| | | å票代ç ,${invoice.invoiceCode || 'N/A'} |
| | | å¼ç¥¨æ¥æ,${invoice.invoiceDate || 'N/A'} |
| | | è´ä¹°æ¹åç§°,${invoice.buyerName || 'N/A'} |
| | | é宿¹åç§°,${invoice.sellerName || 'N/A'} |
| | | éé¢,${(invoice.amount || 0).toFixed(2)} |
| | | ç¨é¢,${(invoice.taxAmount || 0).toFixed(2)} |
| | | ä»·ç¨å计,${(invoice.totalAmount || 0).toFixed(2)} |
| | | ç¶æ,${getStatusText(invoice.status)} |
| | | å建æ¶é´,${invoice.createTime || 'N/A'}`; |
| | | return content; |
| | | }; |
| | | |
| | | // çææ±æ»å
容 |
| | | const generateSummaryContent = () => { |
| | | let content = 'åç¥¨æ±æ»æ¥è¡¨\n'; |
| | | content += 'å票å·ç ,å票代ç ,å¼ç¥¨æ¥æ,è´ä¹°æ¹åç§°,é宿¹åç§°,éé¢,ç¨é¢,ä»·ç¨å计,ç¶æ,å建æ¶é´\n'; |
| | | |
| | | props.invoices.forEach(invoice => { |
| | | content += `${invoice.invoiceNo || 'N/A'},${invoice.invoiceCode || 'N/A'},${invoice.invoiceDate || 'N/A'},${invoice.buyerName || 'N/A'},${invoice.sellerName || 'N/A'},${(invoice.amount || 0).toFixed(2)},${(invoice.taxAmount || 0).toFixed(2)},${(invoice.totalAmount || 0).toFixed(2)},${getStatusText(invoice.status)},${invoice.createTime || 'N/A'}\n`; |
| | | }); |
| | | |
| | | // æ·»å åè®¡è¡ |
| | | const totalAmount = props.invoices.reduce((sum, item) => sum + (item.amount || 0), 0); |
| | | const totalTaxAmount = props.invoices.reduce((sum, item) => sum + (item.taxAmount || 0), 0); |
| | | const totalSum = props.invoices.reduce((sum, item) => sum + (item.totalAmount || 0), 0); |
| | | |
| | | content += `å计,,,,,${totalAmount.toFixed(2)},${totalTaxAmount.toFixed(2)},${totalSum.toFixed(2)},,`; |
| | | |
| | | return content; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .batch-download-container { |
| | | padding: 0; |
| | | } |
| | | |
| | | .invoice-list, |
| | | .download-options, |
| | | .download-progress { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .invoice-list:last-child, |
| | | .download-options:last-child, |
| | | .download-progress:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .progress-content { |
| | | padding: 20px 0; |
| | | text-align: center; |
| | | } |
| | | |
| | | .progress-text { |
| | | margin-top: 15px; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .progress-detail { |
| | | margin-top: 10px; |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-radio-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .el-checkbox-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogVisible" |
| | | @update:model-value="$emit('update:dialogVisible', $event)" |
| | | title="ä¸è½½å票" |
| | | width="600px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div class="download-container"> |
| | | <!-- åç¥¨ä¿¡æ¯ --> |
| | | <el-card class="invoice-info" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>å票信æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="å票å·ç ">{{ invoice.invoiceNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å票代ç ">{{ invoice.invoiceCode || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¼ç¥¨æ¥æ">{{ invoice.invoiceDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="è´ä¹°æ¹">{{ invoice.buyerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="éé¢">{{ (invoice.amount || 0).toFixed(2) }} å
</el-descriptions-item> |
| | | <el-descriptions-item label="ä»·ç¨å计">{{ (invoice.totalAmount || 0).toFixed(2) }} å
</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- ä¸è½½é项 --> |
| | | <el-card class="download-options" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>ä¸è½½é项</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-form :model="downloadOptions" label-width="120px"> |
| | | <el-form-item label="æä»¶æ ¼å¼"> |
| | | <el-radio-group v-model="downloadOptions.format"> |
| | | <el-radio label="pdf">PDFæ ¼å¼</el-radio> |
| | | <el-radio label="excel">Excelæ ¼å¼</el-radio> |
| | | <el-radio label="image">å¾çæ ¼å¼</el-radio> |
| | | <el-radio label="zip">ZIPå缩å
</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å
å«å
容"> |
| | | <el-checkbox-group v-model="downloadOptions.content"> |
| | | <el-checkbox label="basic">åºæ¬ä¿¡æ¯</el-checkbox> |
| | | <el-checkbox label="buyer">è´ä¹°æ¹ä¿¡æ¯</el-checkbox> |
| | | <el-checkbox label="seller">é宿¹ä¿¡æ¯</el-checkbox> |
| | | <el-checkbox label="items">ååæç»</el-checkbox> |
| | | <el-checkbox label="summary">å计信æ¯</el-checkbox> |
| | | </el-checkbox-group> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æä»¶å½å"> |
| | | <el-input |
| | | v-model="downloadOptions.fileName" |
| | | placeholder="请è¾å
¥æä»¶åï¼ä¸å
嫿©å±åï¼" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æ°´å°è®¾ç½®"> |
| | | <el-switch |
| | | v-model="downloadOptions.watermark" |
| | | active-text="æ·»å æ°´å°" |
| | | inactive-text="æ æ°´å°" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å缩设置" v-if="downloadOptions.format === 'image'"> |
| | | <el-select v-model="downloadOptions.compression" placeholder="éæ©å缩质é" style="width: 100%"> |
| | | <el-option label="é«è´¨éï¼æä»¶è¾å¤§ï¼" value="high" /> |
| | | <el-option label="ä¸çè´¨é" value="medium" /> |
| | | <el-option label="ä½è´¨éï¼æä»¶è¾å°ï¼" value="low" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- ä¸è½½è¿åº¦ --> |
| | | <el-card v-if="downloading" class="download-progress" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>ä¸è½½è¿åº¦</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="progress-content"> |
| | | <el-progress |
| | | :percentage="downloadProgress" |
| | | :status="downloadProgress === 100 ? 'success' : ''" |
| | | :stroke-width="20" |
| | | /> |
| | | <div class="progress-text"> |
| | | {{ downloadProgress === 100 ? 'ä¸è½½å®æ' : `æ£å¨ä¸è½½... ${downloadProgress}%` }} |
| | | </div> |
| | | <div class="progress-detail" v-if="downloadProgress < 100"> |
| | | <span>æ£å¨çæ{{ getFormatText(downloadOptions.format) }}æä»¶...</span> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose" :disabled="downloading">åæ¶</el-button> |
| | | <el-button |
| | | type="primary" |
| | | @click="handleDownload" |
| | | :loading="downloading" |
| | | :disabled="!canDownload" |
| | | > |
| | | {{ downloading ? 'ä¸è½½ä¸...' : 'å¼å§ä¸è½½' }} |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | invoice: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogVisible', 'success']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const downloading = ref(false); |
| | | const downloadProgress = ref(0); |
| | | |
| | | // ä¸è½½é项 |
| | | const downloadOptions = reactive({ |
| | | format: 'pdf', |
| | | content: ['basic', 'buyer', 'seller', 'items', 'summary'], |
| | | fileName: '', |
| | | watermark: true, |
| | | compression: 'medium' |
| | | }); |
| | | |
| | | // 计ç®å±æ§ |
| | | const canDownload = computed(() => { |
| | | return downloadOptions.content.length > 0 && downloadOptions.fileName.trim() !== ''; |
| | | }); |
| | | |
| | | // çå¬å票ååï¼èªå¨è®¾ç½®æä»¶å |
| | | watch(() => props.invoice, (newInvoice) => { |
| | | if (newInvoice && newInvoice.invoiceNo) { |
| | | downloadOptions.fileName = `${newInvoice.invoiceNo}_${newInvoice.invoiceDate}`; |
| | | } |
| | | }, { immediate: true }); |
| | | |
| | | // è·åæ ¼å¼ææ¬ |
| | | const getFormatText = (format) => { |
| | | const formatMap = { |
| | | pdf: 'PDF', |
| | | excel: 'Excel', |
| | | image: 'å¾ç' |
| | | }; |
| | | return formatMap[format] || format; |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | if (downloading.value) { |
| | | ElMessage.warning("ä¸è½½è¿è¡ä¸ï¼è¯·çå¾
宿"); |
| | | return; |
| | | } |
| | | emit('update:dialogVisible', false); |
| | | // éç½®ç¶æ |
| | | downloading.value = false; |
| | | downloadProgress.value = 0; |
| | | }; |
| | | |
| | | // å¼å§ä¸è½½ |
| | | const handleDownload = async () => { |
| | | if (!canDownload.value) { |
| | | ElMessage.warning("请å®åä¸è½½é项"); |
| | | return; |
| | | } |
| | | |
| | | downloading.value = true; |
| | | downloadProgress.value = 0; |
| | | |
| | | try { |
| | | // 模æä¸è½½è¿ç¨ |
| | | const steps = [ |
| | | { progress: 20, message: "æ£å¨éªè¯å票信æ¯..." }, |
| | | { progress: 40, message: "æ£å¨çææä»¶å
容..." }, |
| | | { progress: 60, message: "æ£å¨åºç¨æ ¼å¼è®¾ç½®..." }, |
| | | { progress: 80, message: "æ£å¨çææä»¶..." }, |
| | | { progress: 100, message: "ä¸è½½å®æ" } |
| | | ]; |
| | | |
| | | for (let i = 0; i < steps.length; i++) { |
| | | const step = steps[i]; |
| | | await new Promise(resolve => { |
| | | setTimeout(() => { |
| | | downloadProgress.value = step.progress; |
| | | resolve(); |
| | | }, 800); |
| | | }); |
| | | } |
| | | |
| | | // çæçå®çæä»¶å¹¶ä¸è½½ |
| | | await generateAndDownloadFile(); |
| | | |
| | | } catch (error) { |
| | | ElMessage.error("ä¸è½½å¤±è´¥ï¼è¯·éè¯"); |
| | | downloading.value = false; |
| | | downloadProgress.value = 0; |
| | | } |
| | | }; |
| | | |
| | | // çæå¹¶ä¸è½½æä»¶ |
| | | const generateAndDownloadFile = async () => { |
| | | try { |
| | | let fileContent, fileName, mimeType; |
| | | |
| | | if (downloadOptions.format === 'pdf') { |
| | | // çæPDFå
å®¹ï¼æ¨¡æï¼ |
| | | fileContent = generatePDFContent(); |
| | | fileName = `${downloadOptions.fileName}.pdf`; |
| | | mimeType = 'application/pdf'; |
| | | } else if (downloadOptions.format === 'excel') { |
| | | // çæExcelå
容ï¼CSVæ ¼å¼ï¼å
¼å®¹æ§æ´å¥½ï¼ |
| | | fileContent = generateExcelContent(); |
| | | fileName = `${downloadOptions.fileName}.csv`; |
| | | mimeType = 'text/csv'; |
| | | } else if (downloadOptions.format === 'image') { |
| | | // çæå¾çå
容ï¼SVGæ ¼å¼ï¼ |
| | | fileContent = generateImageContent(); |
| | | fileName = `${downloadOptions.fileName}.svg`; |
| | | mimeType = 'image/svg+xml'; |
| | | } else if (downloadOptions.format === 'zip') { |
| | | // çæZIPå缩å
|
| | | await generateZIPFile(); |
| | | return; // ZIPä¸è½½å®æåç´æ¥è¿å |
| | | } |
| | | |
| | | // å建Blob对象 |
| | | const blob = new Blob([fileContent], { type: mimeType }); |
| | | |
| | | // å建ä¸è½½é¾æ¥ |
| | | const url = window.URL.createObjectURL(blob); |
| | | const link = document.createElement('a'); |
| | | link.href = url; |
| | | link.download = fileName; |
| | | |
| | | // 触åä¸è½½ |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | |
| | | // æ¸
ç |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | |
| | | ElMessage.success(`å票ä¸è½½æåï¼æä»¶åï¼${fileName}`); |
| | | emit('success'); |
| | | |
| | | // å»¶è¿å
³éå¯¹è¯æ¡ |
| | | setTimeout(() => { |
| | | handleClose(); |
| | | }, 1500); |
| | | |
| | | } catch (error) { |
| | | console.error('æä»¶çæå¤±è´¥:', error); |
| | | ElMessage.error("æä»¶çæå¤±è´¥ï¼è¯·éè¯"); |
| | | } |
| | | }; |
| | | |
| | | // çæZIPå缩å
|
| | | const generateZIPFile = async () => { |
| | | try { |
| | | // 卿坼å
¥JSZipåº |
| | | const JSZip = await import('jszip'); |
| | | const zip = new JSZip.default(); |
| | | |
| | | // æ ¹æ®éæ©çå
å®¹æ·»å æä»¶å°ZIP |
| | | if (downloadOptions.content.includes('basic')) { |
| | | const basicContent = generateBasicContent(); |
| | | zip.file('åºæ¬ä¿¡æ¯.csv', basicContent); |
| | | } |
| | | |
| | | if (downloadOptions.content.includes('buyer')) { |
| | | const buyerContent = generateBuyerContent(); |
| | | zip.file('è´ä¹°æ¹ä¿¡æ¯.csv', buyerContent); |
| | | } |
| | | |
| | | if (downloadOptions.content.includes('seller')) { |
| | | const sellerContent = generateSellerContent(); |
| | | zip.file('é宿¹ä¿¡æ¯.csv', sellerContent); |
| | | } |
| | | |
| | | if (downloadOptions.content.includes('items')) { |
| | | const itemsContent = generateItemsContent(); |
| | | zip.file('ååæç».csv', itemsContent); |
| | | } |
| | | |
| | | if (downloadOptions.content.includes('summary')) { |
| | | const summaryContent = generateSummaryContent(); |
| | | zip.file('å计信æ¯.csv', summaryContent); |
| | | } |
| | | |
| | | // çæZIPæä»¶ |
| | | const zipBlob = await zip.generateAsync({ |
| | | type: 'blob', |
| | | compression: 'DEFLATE' |
| | | }); |
| | | |
| | | // ä¸è½½ZIPæä»¶ |
| | | const fileName = `${downloadOptions.fileName}.zip`; |
| | | const url = window.URL.createObjectURL(zipBlob); |
| | | const link = document.createElement('a'); |
| | | link.href = url; |
| | | link.download = fileName; |
| | | |
| | | document.body.appendChild(link); |
| | | link.click(); |
| | | |
| | | document.body.removeChild(link); |
| | | window.URL.revokeObjectURL(url); |
| | | |
| | | ElMessage.success(`å票ä¸è½½æåï¼æä»¶åï¼${fileName}`); |
| | | emit('success'); |
| | | |
| | | // å»¶è¿å
³éå¯¹è¯æ¡ |
| | | setTimeout(() => { |
| | | handleClose(); |
| | | }, 1500); |
| | | |
| | | } catch (error) { |
| | | console.error('ZIPæä»¶çæå¤±è´¥:', error); |
| | | ElMessage.error('ZIPæä»¶çæå¤±è´¥ï¼è¯·æ£æ¥æ¯å¦å®è£
äºjszipåº'); |
| | | } |
| | | }; |
| | | |
| | | // çæPDFå
å®¹ï¼æ¨¡æï¼ |
| | | const generatePDFContent = () => { |
| | | const invoice = props.invoice; |
| | | const content = ` |
| | | %PDF-1.4 |
| | | 1 0 obj |
| | | << |
| | | /Type /Catalog |
| | | /Pages 2 0 R |
| | | >> |
| | | endobj |
| | | |
| | | 2 0 obj |
| | | << |
| | | /Type /Pages |
| | | /Kids [3 0 R] |
| | | /Count 1 |
| | | >> |
| | | endobj |
| | | |
| | | 3 0 obj |
| | | << |
| | | /Type /Page |
| | | /Parent 2 0 R |
| | | /MediaBox [0 0 612 792] |
| | | /Contents 4 0 R |
| | | >> |
| | | endobj |
| | | |
| | | 4 0 obj |
| | | << |
| | | /Length 200 |
| | | >> |
| | | stream |
| | | BT |
| | | /F1 12 Tf |
| | | 72 720 Td |
| | | (å票å·ç : ${invoice.invoiceNo || 'N/A'}) Tj |
| | | 0 -20 Td |
| | | (å¼ç¥¨æ¥æ: ${invoice.invoiceDate || 'N/A'}) Tj |
| | | 0 -20 Td |
| | | (è´ä¹°æ¹: ${invoice.buyerName || 'N/A'}) Tj |
| | | 0 -20 Td |
| | | (éé¢: ${(invoice.amount || 0).toFixed(2)} å
) Tj |
| | | 0 -20 Td |
| | | (ä»·ç¨å计: ${(invoice.totalAmount || 0).toFixed(2)} å
) Tj |
| | | ET |
| | | endstream |
| | | endobj |
| | | |
| | | xref |
| | | 0 5 |
| | | 0000000000 65535 f |
| | | 0000000009 00000 n |
| | | 0000000058 00000 n |
| | | 0000000115 00000 n |
| | | 0000000204 00000 n |
| | | trailer |
| | | << |
| | | /Size 5 |
| | | /Root 1 0 R |
| | | >> |
| | | startxref |
| | | 295 |
| | | %%EOF |
| | | `; |
| | | return content; |
| | | }; |
| | | |
| | | // çæExcelå
容ï¼CSVæ ¼å¼ï¼ |
| | | const generateExcelContent = () => { |
| | | const invoice = props.invoice; |
| | | const content = `åç¥¨ä¿¡æ¯ |
| | | å票å·ç ,${invoice.invoiceNo || 'N/A'} |
| | | å票代ç ,${invoice.invoiceCode || 'N/A'} |
| | | å¼ç¥¨æ¥æ,${invoice.invoiceDate || 'N/A'} |
| | | è´ä¹°æ¹åç§°,${invoice.buyerName || 'N/A'} |
| | | é宿¹åç§°,${invoice.sellerName || 'N/A'} |
| | | éé¢,${(invoice.amount || 0).toFixed(2)} |
| | | ç¨é¢,${(invoice.taxAmount || 0).toFixed(2)} |
| | | ä»·ç¨å计,${(invoice.totalAmount || 0).toFixed(2)} |
| | | ç¶æ,${getStatusText(invoice.status)} |
| | | å建æ¶é´,${invoice.createTime || 'N/A'}`; |
| | | return content; |
| | | }; |
| | | |
| | | // çæå¾çå
容ï¼SVGæ ¼å¼ï¼ |
| | | const generateImageContent = () => { |
| | | const invoice = props.invoice; |
| | | const content = `<?xml version="1.0" encoding="UTF-8"?> |
| | | <svg width="600" height="400" xmlns="http://www.w3.org/2000/svg"> |
| | | <rect width="600" height="400" fill="white" stroke="black" stroke-width="2"/> |
| | | <text x="20" y="40" font-family="Arial" font-size="24" fill="black">å票</text> |
| | | <text x="20" y="80" font-family="Arial" font-size="16" fill="black">å票å·ç : ${invoice.invoiceNo || 'N/A'}</text> |
| | | <text x="20" y="110" font-family="Arial" font-size="16" fill="black">å¼ç¥¨æ¥æ: ${invoice.invoiceDate || 'N/A'}</text> |
| | | <text x="20" y="140" font-family="Arial" font-size="16" fill="black">è´ä¹°æ¹: ${invoice.buyerName || 'N/A'}</text> |
| | | <text x="20" y="170" font-family="Arial" font-size="16" fill="black">éé¢: ${(invoice.amount || 0).toFixed(2)} å
</text> |
| | | <text x="20" y="200" font-family="Arial" font-size="16" fill="black">ä»·ç¨å计: ${(invoice.totalAmount || 0).toFixed(2)} å
</text> |
| | | <text x="20" y="230" font-family="Arial" font-size="16" fill="black">ç¶æ: ${getStatusText(invoice.status)}</text> |
| | | </svg>`; |
| | | return content; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | 'draft': 'è稿', |
| | | 'pending': 'å¾
å¼ç¥¨', |
| | | 'issuing': 'å¼ç¥¨ä¸', |
| | | 'issued': 'å·²å¼ç¥¨', |
| | | 'failed': 'å¼ç¥¨å¤±è´¥', |
| | | 'cancelled': 'å·²ä½åº' |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // çæåºæ¬ä¿¡æ¯å
容 |
| | | const generateBasicContent = () => { |
| | | const invoice = props.invoice; |
| | | return `åºæ¬ä¿¡æ¯ |
| | | å票å·ç ,${invoice.invoiceNo || 'N/A'} |
| | | å票代ç ,${invoice.invoiceCode || 'N/A'} |
| | | å¼ç¥¨æ¥æ,${invoice.invoiceDate || 'N/A'} |
| | | ç¶æ,${getStatusText(invoice.status)} |
| | | å建æ¶é´,${invoice.createTime || 'N/A'}`; |
| | | }; |
| | | |
| | | // çæè´ä¹°æ¹ä¿¡æ¯å
容 |
| | | const generateBuyerContent = () => { |
| | | const invoice = props.invoice; |
| | | return `è´ä¹°æ¹ä¿¡æ¯ |
| | | è´ä¹°æ¹åç§°,${invoice.buyerName || 'N/A'} |
| | | è´ä¹°æ¹ç¨å·,${invoice.buyerTaxNo || 'N/A'} |
| | | è´ä¹°æ¹å°å,${invoice.buyerAddress || 'N/A'} |
| | | è´ä¹°æ¹é¶è¡è´¦æ·,${invoice.buyerBankAccount || 'N/A'}`; |
| | | }; |
| | | |
| | | // çæé宿¹ä¿¡æ¯å
容 |
| | | const generateSellerContent = () => { |
| | | const invoice = props.invoice; |
| | | return `é宿¹ä¿¡æ¯ |
| | | é宿¹åç§°,${invoice.sellerName || 'N/A'} |
| | | é宿¹ç¨å·,${invoice.sellerTaxNo || 'N/A'} |
| | | é宿¹å°å,${invoice.sellerAddress || 'N/A'} |
| | | é宿¹é¶è¡è´¦æ·,${invoice.sellerBankAccount || 'N/A'}`; |
| | | }; |
| | | |
| | | // çæååæç»å
容 |
| | | const generateItemsContent = () => { |
| | | const invoice = props.invoice; |
| | | if (!invoice.items || invoice.items.length === 0) { |
| | | return `ååæç» |
| | | ææ ååæç»ä¿¡æ¯`; |
| | | } |
| | | |
| | | let content = 'ååæç»\nåååç§°,è§æ ¼åå·,æ°é,åä»·,éé¢,ç¨ç,ç¨é¢,ä»·ç¨å计\n'; |
| | | invoice.items.forEach(item => { |
| | | content += `${item.name || 'N/A'},${item.spec || 'N/A'},${item.quantity || 0},${(item.price || 0).toFixed(2)},${(item.amount || 0).toFixed(2)},${(item.taxRate || 0).toFixed(2)}%,${(item.taxAmount || 0).toFixed(2)},${(item.totalAmount || 0).toFixed(2)}\n`; |
| | | }); |
| | | |
| | | return content; |
| | | }; |
| | | |
| | | // çæå计信æ¯å
容 |
| | | const generateSummaryContent = () => { |
| | | const invoice = props.invoice; |
| | | return `åè®¡ä¿¡æ¯ |
| | | éé¢å计,${(invoice.amount || 0).toFixed(2)} å
|
| | | ç¨é¢å计,${(invoice.taxAmount || 0).toFixed(2)} å
|
| | | ä»·ç¨å计,${(invoice.totalAmount || 0).toFixed(2)} å
|
| | | 夿³¨,${invoice.remark || 'N/A'}`; |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .download-container { |
| | | padding: 0; |
| | | } |
| | | |
| | | .invoice-info, |
| | | .download-options, |
| | | .download-progress { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .invoice-info:last-child, |
| | | .download-options:last-child, |
| | | .download-progress:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .progress-content { |
| | | padding: 20px 0; |
| | | text-align: center; |
| | | } |
| | | |
| | | .progress-text { |
| | | margin-top: 15px; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .progress-detail { |
| | | margin-top: 10px; |
| | | color: #606266; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-checkbox-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .el-radio-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogFormVisible" |
| | | @update:model-value="$emit('update:dialogFormVisible', $event)" |
| | | :title="title" |
| | | width="1200px" |
| | | :close-on-click-modal="false" |
| | | @close="handleClose" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="formData" |
| | | :rules="rules" |
| | | label-width="120px" |
| | | class="invoice-form" |
| | | > |
| | | <!-- è´ä¹°æ¹ä¿¡æ¯ --> |
| | | <el-card class="buyer-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>è´ä¹°æ¹ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="è´ä¹°æ¹åç§°" prop="buyerName"> |
| | | <el-input |
| | | v-model="formData.buyerName" |
| | | placeholder="请è¾å
¥è´ä¹°æ¹åç§°" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="纳ç¨äººè¯å«å·" prop="buyerTaxNo"> |
| | | <el-input |
| | | v-model="formData.buyerTaxNo" |
| | | placeholder="请è¾å
¥çº³ç¨äººè¯å«å·" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å°åçµè¯" prop="buyerAddress"> |
| | | <el-input |
| | | v-model="formData.buyerAddress" |
| | | placeholder="请è¾å
¥å°åçµè¯" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="弿·è¡åè´¦å·" prop="buyerBankAccount"> |
| | | <el-input |
| | | v-model="formData.buyerBankAccount" |
| | | placeholder="请è¾å
¥å¼æ·è¡åè´¦å·" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <!-- é宿¹ä¿¡æ¯ --> |
| | | <el-card class="seller-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>é宿¹ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é宿¹åç§°" prop="sellerName"> |
| | | <el-input |
| | | v-model="formData.sellerName" |
| | | placeholder="请è¾å
¥é宿¹åç§°" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="纳ç¨äººè¯å«å·" prop="sellerTaxNo"> |
| | | <el-input |
| | | v-model="formData.sellerTaxNo" |
| | | placeholder="请è¾å
¥çº³ç¨äººè¯å«å·" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å°åçµè¯" prop="sellerAddress"> |
| | | <el-input |
| | | v-model="formData.sellerAddress" |
| | | placeholder="请è¾å
¥å°åçµè¯" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="弿·è¡åè´¦å·" prop="sellerBankAccount"> |
| | | <el-input |
| | | v-model="formData.sellerBankAccount" |
| | | placeholder="请è¾å
¥å¼æ·è¡åè´¦å·" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <!-- ååæç» --> |
| | | <el-card class="items-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>ååæç»</span> |
| | | <el-button type="primary" size="small" @click="addItem"> |
| | | æ·»å åå |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="formData.items" border style="width: 100%"> |
| | | <el-table-column label="åååç§°" width="200"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model="scope.row.name" |
| | | placeholder="åååç§°" |
| | | style="width: 100%" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è§æ ¼åå·" width="150"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model="scope.row.specification" |
| | | placeholder="è§æ ¼åå·" |
| | | style="width: 100%" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä½" width="100"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model="scope.row.unit" |
| | | placeholder="åä½" |
| | | style="width: 100%" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ°é" width="120"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model.number="scope.row.quantity" |
| | | placeholder="æ°é" |
| | | type="number" |
| | | @input="calculateItemAmount(scope.$index)" |
| | | style="width: 100%" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä»·" width="120"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model.number="scope.row.unitPrice" |
| | | placeholder="åä»·" |
| | | type="number" |
| | | @input="calculateItemAmount(scope.$index)" |
| | | style="width: 100%" |
| | | > |
| | | <template v-slot:suffix> |
| | | <span>å
</span> |
| | | </template> |
| | | </el-input> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="éé¢" width="120"> |
| | | <template #default="scope"> |
| | | <span>{{ (scope.row.amount || 0).toFixed(2) }} å
</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¨ç" width="120"> |
| | | <template #default="scope"> |
| | | <el-select |
| | | v-model="scope.row.taxRate" |
| | | placeholder="éæ©ç¨ç" |
| | | @change="calculateItemAmount(scope.$index)" |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="0%" value="0" /> |
| | | <el-option label="1%" value="0.01" /> |
| | | <el-option label="3%" value="0.03" /> |
| | | <el-option label="6%" value="0.06" /> |
| | | <el-option label="9%" value="0.09" /> |
| | | <el-option label="13%" value="0.13" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¨é¢" width="120"> |
| | | <template #default="scope"> |
| | | <span>{{ (scope.row.taxAmount || 0).toFixed(2) }} å
</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ä»·ç¨å计" width="120"> |
| | | <template #default="scope"> |
| | | <span>{{ (scope.row.totalAmount || 0).toFixed(2) }} å
</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="removeItem(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- åè®¡ä¿¡æ¯ --> |
| | | <div class="summary-info"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <span class="summary-label">éé¢å计ï¼</span> |
| | | <span class="summary-value">{{ totalAmount.toFixed(2) }} å
</span> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <span class="summary-label">ç¨é¢å计ï¼</span> |
| | | <span class="summary-value">{{ totalTaxAmount.toFixed(2) }} å
</span> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <span class="summary-label">ä»·ç¨å计ï¼</span> |
| | | <span class="summary-value">{{ totalTotalAmount.toFixed(2) }} å
</span> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 夿³¨ä¿¡æ¯ --> |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input |
| | | v-model="formData.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å¤æ³¨ä¿¡æ¯" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSubmit" :loading="submitLoading"> |
| | | æäº¤ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, watch, computed } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogFormVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | form: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "" |
| | | }, |
| | | isEdit: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogFormVisible', 'update:form', 'submit', 'success']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const formRef = ref(null); |
| | | const submitLoading = ref(false); |
| | | |
| | | // è¡¨åæ°æ® |
| | | const formData = reactive({ |
| | | buyerName: "", |
| | | buyerTaxNo: "", |
| | | buyerAddress: "", |
| | | buyerBankAccount: "", |
| | | sellerName: "æ¬å
¬å¸", |
| | | sellerTaxNo: "123456789012345678", |
| | | sellerAddress: "å
¬å¸å°å", |
| | | sellerBankAccount: "é¶è¡è´¦æ·", |
| | | items: [], |
| | | remark: "" |
| | | }); |
| | | |
| | | // 表åéªè¯è§å |
| | | const rules = { |
| | | buyerName: [ |
| | | { required: true, message: "请è¾å
¥è´ä¹°æ¹åç§°", trigger: "blur" } |
| | | ], |
| | | buyerTaxNo: [ |
| | | { required: true, message: "请è¾å
¥çº³ç¨äººè¯å«å·", trigger: "blur" } |
| | | ], |
| | | items: [ |
| | | { |
| | | type: "array", |
| | | required: true, |
| | | message: "请è³å°æ·»å ä¸ä¸ªåå", |
| | | trigger: "change", |
| | | validator: (rule, value, callback) => { |
| | | if (!value || value.length === 0) { |
| | | callback(new Error("请è³å°æ·»å ä¸ä¸ªåå")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // 计ç®å±æ§ |
| | | const totalAmount = computed(() => { |
| | | return formData.items.reduce((sum, item) => sum + (item.amount || 0), 0); |
| | | }); |
| | | |
| | | const totalTaxAmount = computed(() => { |
| | | return formData.items.reduce((sum, item) => sum + (item.taxAmount || 0), 0); |
| | | }); |
| | | |
| | | const totalTotalAmount = computed(() => { |
| | | return formData.items.reduce((sum, item) => sum + (item.totalAmount || 0), 0); |
| | | }); |
| | | |
| | | // çå¬è¡¨åæ°æ®åå |
| | | watch(() => props.form, (newVal) => { |
| | | Object.assign(formData, newVal); |
| | | if (!formData.items || formData.items.length === 0) { |
| | | formData.items = []; |
| | | } |
| | | }, { deep: true, immediate: true }); |
| | | |
| | | // æ·»å åå |
| | | const addItem = () => { |
| | | formData.items.push({ |
| | | name: "", |
| | | specification: "", |
| | | unit: "", |
| | | quantity: 0, |
| | | unitPrice: 0, |
| | | amount: 0, |
| | | taxRate: "0.13", |
| | | taxAmount: 0, |
| | | totalAmount: 0 |
| | | }); |
| | | }; |
| | | |
| | | // å é¤åå |
| | | const removeItem = (index) => { |
| | | formData.items.splice(index, 1); |
| | | }; |
| | | |
| | | // 计ç®ååéé¢ |
| | | const calculateItemAmount = (index) => { |
| | | const item = formData.items[index]; |
| | | if (item.quantity && item.unitPrice) { |
| | | item.amount = item.quantity * item.unitPrice; |
| | | item.taxAmount = item.amount * parseFloat(item.taxRate); |
| | | item.totalAmount = item.amount + item.taxAmount; |
| | | } |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | emit('update:dialogFormVisible', false); |
| | | formRef.value?.resetFields(); |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = async () => { |
| | | if (!formRef.value) return; |
| | | |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | // éªè¯ååä¿¡æ¯ |
| | | if (formData.items.length === 0) { |
| | | ElMessage.warning("请è³å°æ·»å ä¸ä¸ªåå"); |
| | | return; |
| | | } |
| | | |
| | | for (let item of formData.items) { |
| | | if (!item.name) { |
| | | ElMessage.warning("请è¾å
¥åååç§°"); |
| | | return; |
| | | } |
| | | if (!item.quantity || item.quantity <= 0) { |
| | | ElMessage.warning("请è¾å
¥ææçååæ°é"); |
| | | return; |
| | | } |
| | | if (!item.unitPrice || item.unitPrice <= 0) { |
| | | ElMessage.warning("请è¾å
¥ææçåååä»·"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | submitLoading.value = true; |
| | | |
| | | // 模ææäº¤ |
| | | setTimeout(() => { |
| | | submitLoading.value = false; |
| | | ElMessage.success("æäº¤æå"); |
| | | emit('submit', { ...formData }); |
| | | handleClose(); |
| | | }, 1000); |
| | | |
| | | } catch (error) { |
| | | console.error('表åéªè¯å¤±è´¥:', error); |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .invoice-form { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .buyer-card, |
| | | .seller-card, |
| | | .items-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .summary-info { |
| | | margin-top: 15px; |
| | | padding: 15px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .summary-label { |
| | | font-weight: bold; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .summary-value { |
| | | color: #409eff; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-table { |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogViewVisible" |
| | | @update:model-value="$emit('update:dialogViewVisible', $event)" |
| | | :title="title" |
| | | width="1000px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div class="invoice-view"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <el-card class="basic-info" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="å票å·ç ">{{ form.invoiceNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å票代ç ">{{ form.invoiceCode || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¼ç¥¨æ¥æ">{{ form.invoiceDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¼ç¥¨ç¶æ"> |
| | | <el-tag :type="getStatusType(form.status)"> |
| | | {{ getStatusText(form.status) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ç¨æ§å¹³å°ç¶æ"> |
| | | <el-tag :type="getTaxControlStatusType(form.taxControlStatus)"> |
| | | {{ getTaxControlStatusText(form.taxControlStatus) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å建æ¶é´">{{ form.createTime || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- è´ä¹°æ¹ä¿¡æ¯ --> |
| | | <el-card class="buyer-info" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>è´ä¹°æ¹ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="è´ä¹°æ¹åç§°">{{ form.buyerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="纳ç¨äººè¯å«å·">{{ form.buyerTaxNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å°åçµè¯">{{ form.buyerAddress || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="弿·è¡åè´¦å·">{{ form.buyerBankAccount || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- é宿¹ä¿¡æ¯ --> |
| | | <el-card class="seller-info" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>é宿¹ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="é宿¹åç§°">{{ form.sellerName || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="纳ç¨äººè¯å«å·">{{ form.sellerTaxNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="å°åçµè¯">{{ form.sellerAddress || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="弿·è¡åè´¦å·">{{ form.sellerBankAccount || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- ååæç» --> |
| | | <el-card class="items-info" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>ååæç»</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="form.items || []" border style="width: 100%"> |
| | | <el-table-column label="åååç§°" prop="name" width="200" /> |
| | | <el-table-column label="è§æ ¼åå·" prop="specification" width="150" /> |
| | | <el-table-column label="åä½" prop="unit" width="100" /> |
| | | <el-table-column label="æ°é" prop="quantity" width="100" /> |
| | | <el-table-column label="åä»·" prop="unitPrice" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.unitPrice }} å
|
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="éé¢" prop="amount" width="120"> |
| | | <template #default="scope"> |
| | | {{ (scope.row.amount || 0).toFixed(2) }} å
|
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¨ç" prop="taxRate" width="100"> |
| | | <template #default="scope"> |
| | | {{ (parseFloat(scope.row.taxRate || 0) * 100).toFixed(0) }}% |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¨é¢" prop="taxAmount" width="120"> |
| | | <template #default="scope"> |
| | | {{ (scope.row.taxAmount || 0).toFixed(2) }} å
|
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ä»·ç¨å计" prop="totalAmount" width="120"> |
| | | <template #default="scope"> |
| | | {{ (scope.row.totalAmount || 0).toFixed(2) }} å
|
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- åè®¡ä¿¡æ¯ --> |
| | | <div class="summary-info"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <span class="summary-label">éé¢å计ï¼</span> |
| | | <span class="summary-value">{{ getTotalAmount().toFixed(2) }} å
</span> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <span class="summary-label">ç¨é¢å计ï¼</span> |
| | | <span class="summary-value">{{ getTotalTaxAmount().toFixed(2) }} å
</span> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <span class="summary-label">ä»·ç¨å计ï¼</span> |
| | | <span class="summary-value">{{ getTotalTotalAmount().toFixed(2) }} å
</span> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 夿³¨ä¿¡æ¯ --> |
| | | <el-card class="remark-info" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>夿³¨ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="remark-content"> |
| | | {{ form.remark || 'æ ' }} |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogViewVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | form: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "" |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogViewVisible']); |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const statusMap = { |
| | | draft: "", |
| | | pending: "warning", |
| | | issuing: "warning", |
| | | issued: "success", |
| | | failed: "danger", |
| | | cancelled: "info" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | draft: "è稿", |
| | | pending: "å¾
å¼ç¥¨", |
| | | issuing: "å¼ç¥¨ä¸", |
| | | issued: "å·²å¼ç¥¨", |
| | | failed: "å¼ç¥¨å¤±è´¥", |
| | | cancelled: "å·²ä½åº" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // è·åç¨æ§å¹³å°ç¶æç±»å |
| | | const getTaxControlStatusType = (status) => { |
| | | const statusMap = { |
| | | pending: "warning", |
| | | syncing: "warning", |
| | | synced: "success", |
| | | failed: "danger" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åç¨æ§å¹³å°ç¶æææ¬ |
| | | const getTaxControlStatusText = (status) => { |
| | | const statusMap = { |
| | | pending: "å¾
忥", |
| | | syncing: "忥ä¸", |
| | | synced: "已忥", |
| | | failed: "åæ¥å¤±è´¥" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // è®¡ç®æ»éé¢ |
| | | const getTotalAmount = () => { |
| | | return (props.form.items || []).reduce((sum, item) => sum + (item.amount || 0), 0); |
| | | }; |
| | | |
| | | // è®¡ç®æ»ç¨é¢ |
| | | const getTotalTaxAmount = () => { |
| | | return (props.form.items || []).reduce((sum, item) => sum + (item.taxAmount || 0), 0); |
| | | }; |
| | | |
| | | // è®¡ç®æ»ä»·ç¨å计 |
| | | const getTotalTotalAmount = () => { |
| | | return (props.form.items || []).reduce((sum, item) => sum + (item.totalAmount || 0), 0); |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | emit('update:dialogViewVisible', false); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .invoice-view { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .basic-info, |
| | | .buyer-info, |
| | | .seller-info, |
| | | .items-info, |
| | | .remark-info { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .summary-info { |
| | | margin-top: 15px; |
| | | padding: 15px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .summary-label { |
| | | font-weight: bold; |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .summary-value { |
| | | color: #409eff; |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .remark-content { |
| | | padding: 15px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | min-height: 60px; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-table { |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogSyncVisible" |
| | | @update:model-value="$emit('update:dialogSyncVisible', $event)" |
| | | title="ç¨æ§å¹³å°åæ¥" |
| | | width="800px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div class="sync-container"> |
| | | <!-- åæ¥ç¶æ --> |
| | | <el-card class="sync-status" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>åæ¥ç¶æ</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="status-content"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <div class="status-item"> |
| | | <div class="status-icon success"> |
| | | <el-icon><Check /></el-icon> |
| | | </div> |
| | | <div class="status-text"> |
| | | <div class="status-title">已忥</div> |
| | | <div class="status-count">{{ syncedCount }}</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="status-item"> |
| | | <div class="status-icon warning"> |
| | | <el-icon><Clock /></el-icon> |
| | | </div> |
| | | <div class="status-text"> |
| | | <div class="status-title">å¾
忥</div> |
| | | <div class="status-count">{{ pendingCount }}</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="status-item"> |
| | | <div class="status-icon danger"> |
| | | <el-icon><Close /></el-icon> |
| | | </div> |
| | | <div class="status-text"> |
| | | <div class="status-title">åæ¥å¤±è´¥</div> |
| | | <div class="status-count">{{ failedCount }}</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 忥é
ç½® --> |
| | | <el-card class="sync-config" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>忥é
ç½®</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-form :model="syncConfig" label-width="120px"> |
| | | <el-form-item label="ç¨æ§å¹³å°å°å"> |
| | | <el-input |
| | | v-model="syncConfig.taxControlUrl" |
| | | placeholder="请è¾å
¥ç¨æ§å¹³å°å°å" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="忥é¢ç"> |
| | | <el-select |
| | | v-model="syncConfig.syncFrequency" |
| | | placeholder="è¯·éæ©åæ¥é¢ç" |
| | | style="width: 100%" |
| | | > |
| | | <el-option label="宿¶åæ¥" value="realtime" /> |
| | | <el-option label="æ¯å°æ¶åæ¥" value="hourly" /> |
| | | <el-option label="æ¯å¤©åæ¥" value="daily" /> |
| | | <el-option label="æå¨åæ¥" value="manual" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="èªå¨éè¯"> |
| | | <el-switch |
| | | v-model="syncConfig.autoRetry" |
| | | active-text="å¼å¯" |
| | | inactive-text="å
³é" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="éè¯æ¬¡æ°" v-if="syncConfig.autoRetry"> |
| | | <el-input-number |
| | | v-model="syncConfig.retryCount" |
| | | :min="1" |
| | | :max="10" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- 忥æ¥å¿ --> |
| | | <el-card class="sync-log" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>忥æ¥å¿</span> |
| | | <el-button type="primary" size="small" @click="refreshLog"> |
| | | å·æ°æ¥å¿ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="syncLogs" border style="width: 100%" max-height="300"> |
| | | <el-table-column label="æ¶é´" prop="time" width="160" /> |
| | | <el-table-column label="æä½" prop="action" width="120" /> |
| | | <el-table-column label="ç¶æ" prop="status" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getLogStatusType(scope.row.status)"> |
| | | {{ getLogStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="详æ
" prop="detail" show-overflow-tooltip /> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSync" :loading="syncLoading"> |
| | | å¼å§åæ¥ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { Check, Clock, Close } from "@element-plus/icons-vue"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogSyncVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogSyncVisible', 'success']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const syncLoading = ref(false); |
| | | const syncedCount = ref(15); |
| | | const pendingCount = ref(8); |
| | | const failedCount = ref(2); |
| | | |
| | | // 忥é
ç½® |
| | | const syncConfig = reactive({ |
| | | taxControlUrl: "https://tax-control.example.com/api", |
| | | syncFrequency: "manual", |
| | | autoRetry: true, |
| | | retryCount: 3 |
| | | }); |
| | | |
| | | // 忥æ¥å¿ |
| | | const syncLogs = ref([ |
| | | { |
| | | time: "2024-12-01 15:30:00", |
| | | action: "åç¥¨åæ¥", |
| | | status: "success", |
| | | detail: "æå忥15å¼ å票å°ç¨æ§å¹³å°" |
| | | }, |
| | | { |
| | | time: "2024-12-01 15:25:00", |
| | | action: "åç¥¨åæ¥", |
| | | status: "success", |
| | | detail: "æå忥8å¼ å票å°ç¨æ§å¹³å°" |
| | | }, |
| | | { |
| | | time: "2024-12-01 15:20:00", |
| | | action: "åç¥¨åæ¥", |
| | | status: "failed", |
| | | detail: "åæ¥å¤±è´¥ï¼ç½ç»è¿æ¥è¶
æ¶" |
| | | }, |
| | | { |
| | | time: "2024-12-01 15:15:00", |
| | | action: "åç¥¨åæ¥", |
| | | status: "success", |
| | | detail: "æå忥12å¼ å票å°ç¨æ§å¹³å°" |
| | | } |
| | | ]); |
| | | |
| | | // è·åæ¥å¿ç¶æç±»å |
| | | const getLogStatusType = (status) => { |
| | | const statusMap = { |
| | | success: "success", |
| | | failed: "danger", |
| | | pending: "warning" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åæ¥å¿ç¶æææ¬ |
| | | const getLogStatusText = (status) => { |
| | | const statusMap = { |
| | | success: "æå", |
| | | failed: "失败", |
| | | pending: "è¿è¡ä¸" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // å·æ°æ¥å¿ |
| | | const refreshLog = () => { |
| | | ElMessage.success("æ¥å¿å·²å·æ°"); |
| | | }; |
| | | |
| | | // å¼å§åæ¥ |
| | | const handleSync = async () => { |
| | | if (!syncConfig.taxControlUrl) { |
| | | ElMessage.warning("请å
é
ç½®ç¨æ§å¹³å°å°å"); |
| | | return; |
| | | } |
| | | |
| | | syncLoading.value = true; |
| | | |
| | | try { |
| | | // 模æåæ¥è¿ç¨ |
| | | await new Promise(resolve => setTimeout(resolve, 2000)); |
| | | |
| | | // æ´æ°åæ¥ç¶æ |
| | | const newSynced = Math.min(pendingCount.value, 5); |
| | | syncedCount.value += newSynced; |
| | | pendingCount.value -= newSynced; |
| | | |
| | | // æ·»å 忥æ¥å¿ |
| | | syncLogs.value.unshift({ |
| | | time: new Date().toLocaleString(), |
| | | action: "åç¥¨åæ¥", |
| | | status: "success", |
| | | detail: `æå忥${newSynced}å¼ å票å°ç¨æ§å¹³å°` |
| | | }); |
| | | |
| | | ElMessage.success("忥宿"); |
| | | emit('success'); |
| | | } catch (error) { |
| | | ElMessage.error("åæ¥å¤±è´¥"); |
| | | failedCount.value++; |
| | | |
| | | // æ·»å 失败æ¥å¿ |
| | | syncLogs.value.unshift({ |
| | | time: new Date().toLocaleString(), |
| | | action: "åç¥¨åæ¥", |
| | | status: "failed", |
| | | detail: "åæ¥å¤±è´¥ï¼ç³»ç»é误" |
| | | }); |
| | | } finally { |
| | | syncLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | emit('update:dialogSyncVisible', false); |
| | | }; |
| | | |
| | | // ç»ä»¶æè½½æ¶åå§åæ°æ® |
| | | onMounted(() => { |
| | | // å¯ä»¥å¨è¿éå è½½åå§æ°æ® |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .sync-container { |
| | | padding: 0; |
| | | } |
| | | |
| | | .sync-status, |
| | | .sync-config, |
| | | .sync-log { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .sync-status:last-child, |
| | | .sync-config:last-child, |
| | | .sync-log:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .status-content { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .status-item { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | padding: 20px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .status-icon { |
| | | width: 60px; |
| | | height: 60px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | font-size: 24px; |
| | | color: white; |
| | | } |
| | | |
| | | .status-icon.success { |
| | | background-color: #67c23a; |
| | | } |
| | | |
| | | .status-icon.warning { |
| | | background-color: #e6a23c; |
| | | } |
| | | |
| | | .status-icon.danger { |
| | | background-color: #f56c6c; |
| | | } |
| | | |
| | | .status-text { |
| | | text-align: left; |
| | | } |
| | | |
| | | .status-title { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .status-count { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-table { |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- æç´¢è¡¨å --> |
| | | <el-form :inline="true" :model="queryParams" class="search-form"> |
| | | <el-form-item label="å票å·ç "> |
| | | <el-input |
| | | v-model="queryParams.invoiceNo" |
| | | placeholder="请è¾å
¥å票å·ç " |
| | | clearable |
| | | :style="{ width: '200px' }" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="å¼ç¥¨ç¶æ"> |
| | | <el-select |
| | | v-model="queryParams.status" |
| | | placeholder="è¯·éæ©å¼ç¥¨ç¶æ" |
| | | clearable |
| | | :style="{ width: '150px' }" |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in statusList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="å¼ç¥¨æ¥æ"> |
| | | <el-date-picker |
| | | v-model="queryParams.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | :style="{ width: '240px' }" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-card> |
| | | <!-- æä½æé®åº --> |
| | | <el-row :gutter="24" class="table-toolbar" justify="space-between"> |
| | | <el-button type="primary" :icon="Plus" @click="handleAdd"> |
| | | æ°å¢å票 |
| | | </el-button> |
| | | <el-button type="success" :icon="Refresh" @click="handleSyncTaxControl"> |
| | | åæ¥ç¨æ§å¹³å° |
| | | </el-button> |
| | | <el-button type="warning" :icon="Download" @click="handleBatchDownload"> |
| | | æ¹éä¸è½½ |
| | | </el-button> |
| | | <el-button type="danger" :icon="Delete" @click="handleBatchDelete" :disabled="selectedIds.length === 0"> |
| | | æ¹éå é¤ |
| | | </el-button> |
| | | </el-row> |
| | | |
| | | <!-- è¡¨æ ¼ç»ä»¶ --> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | @selection-change="handleSelectionChange" |
| | | border |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column label="å票å·ç " prop="invoiceNo" width="180" /> |
| | | <el-table-column label="å票代ç " prop="invoiceCode" width="150" /> |
| | | <el-table-column label="å¼ç¥¨æ¥æ" prop="invoiceDate" width="120" /> |
| | | <el-table-column label="è´ä¹°æ¹åç§°" prop="buyerName" width="200" show-overflow-tooltip /> |
| | | <el-table-column label="é宿¹åç§°" prop="sellerName" width="200" show-overflow-tooltip /> |
| | | <el-table-column label="éé¢" prop="amount" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.amount }} å
|
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¨é¢" prop="taxAmount" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.taxAmount }} å
|
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ä»·ç¨å计" prop="totalAmount" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.totalAmount }} å
|
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¼ç¥¨ç¶æ" prop="status" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)"> |
| | | {{ getStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¨æ§å¹³å°ç¶æ" prop="taxControlStatus" width="120"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getTaxControlStatusType(scope.row.taxControlStatus)"> |
| | | {{ getTaxControlStatusText(scope.row.taxControlStatus) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å建æ¶é´" prop="createTime" width="160" /> |
| | | <el-table-column label="æä½" width="250" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | size="small" |
| | | type="primary" |
| | | @click="handleView(scope.row)" |
| | | > |
| | | æ¥ç |
| | | </el-button> |
| | | <el-button |
| | | size="small" |
| | | type="success" |
| | | @click="handleDownload(scope.row)" |
| | | v-if="scope.row.status === 'issued'" |
| | | > |
| | | ä¸è½½ |
| | | </el-button> |
| | | <el-button |
| | | size="small" |
| | | type="warning" |
| | | @click="handleEdit(scope.row)" |
| | | v-if="scope.row.status === 'draft'" |
| | | > |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button |
| | | size="small" |
| | | type="danger" |
| | | @click="handleDelete(scope.row)" |
| | | v-if="scope.row.status === 'draft'" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- å页ç»ä»¶ --> |
| | | <pagination |
| | | v-if="total > 0" |
| | | :page="current" |
| | | :limit="pageSize" |
| | | :total="total" |
| | | @pagination="handlePagination" |
| | | :layout="'total, prev, pager, next, jumper'" |
| | | /> |
| | | </el-card> |
| | | |
| | | <!-- æ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <InvoiceDialog |
| | | v-model:dialogFormVisible="dialogFormVisible" |
| | | v-model:form="form" |
| | | :title="title" |
| | | :is-edit="isEdit" |
| | | @submit="handleSubmit" |
| | | @success="handleSuccess" |
| | | ref="invoiceDialog" |
| | | /> |
| | | |
| | | <!-- æ¥ç详æ
å¯¹è¯æ¡ --> |
| | | <InvoiceViewDialog |
| | | v-model:dialogViewVisible="dialogViewVisible" |
| | | :form="viewForm" |
| | | title="å票详æ
" |
| | | /> |
| | | |
| | | <!-- ç¨æ§å¹³å°åæ¥å¯¹è¯æ¡ --> |
| | | <TaxControlSyncDialog |
| | | v-model:dialogSyncVisible="dialogSyncVisible" |
| | | @success="handleSyncSuccess" |
| | | /> |
| | | |
| | | <!-- å个å票ä¸è½½å¯¹è¯æ¡ --> |
| | | <DownloadDialog |
| | | v-model:dialogVisible="downloadDialogVisible" |
| | | :invoice="currentDownloadInvoice" |
| | | @success="handleDownloadSuccess" |
| | | /> |
| | | |
| | | <!-- æ¹éä¸è½½å¯¹è¯æ¡ --> |
| | | <BatchDownloadDialog |
| | | v-model:dialogVisible="batchDownloadDialogVisible" |
| | | :invoices="batchDownloadInvoices" |
| | | @success="handleBatchDownloadSuccess" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { Plus, Edit, Delete, Refresh, Download, View } from "@element-plus/icons-vue"; |
| | | import Pagination from "@/components/Pagination"; |
| | | import InvoiceDialog from "./components/InvoiceDialog.vue"; |
| | | import InvoiceViewDialog from "./components/InvoiceViewDialog.vue"; |
| | | import TaxControlSyncDialog from "./components/TaxControlSyncDialog.vue"; |
| | | import DownloadDialog from "./components/DownloadDialog.vue"; |
| | | import BatchDownloadDialog from "./components/BatchDownloadDialog.vue"; |
| | | |
| | | // ååºå¼æ°æ® |
| | | const loading = ref(false); |
| | | const tableData = ref([]); |
| | | const selectedIds = ref([]); |
| | | const current = ref(1); |
| | | const pageSize = ref(10); |
| | | const total = ref(0); |
| | | const dialogFormVisible = ref(false); |
| | | const dialogViewVisible = ref(false); |
| | | const dialogSyncVisible = ref(false); |
| | | const downloadDialogVisible = ref(false); |
| | | const batchDownloadDialogVisible = ref(false); |
| | | const isEdit = ref(false); |
| | | const title = ref(""); |
| | | const form = ref({}); |
| | | const viewForm = ref({}); |
| | | const currentDownloadInvoice = ref({}); |
| | | const batchDownloadInvoices = ref([]); |
| | | |
| | | // æ¥è¯¢åæ° |
| | | const queryParams = reactive({ |
| | | invoiceNo: "", |
| | | status: "", |
| | | dateRange: [] |
| | | }); |
| | | |
| | | // ç¶æå表 |
| | | const statusList = ref([ |
| | | { value: "draft", label: "è稿" }, |
| | | { value: "pending", label: "å¾
å¼ç¥¨" }, |
| | | { value: "issuing", label: "å¼ç¥¨ä¸" }, |
| | | { value: "issued", label: "å·²å¼ç¥¨" }, |
| | | { value: "failed", label: "å¼ç¥¨å¤±è´¥" }, |
| | | { value: "cancelled", label: "å·²ä½åº" } |
| | | ]); |
| | | |
| | | // æ¨¡ææ°æ® |
| | | const mockData = [ |
| | | { |
| | | id: "1", |
| | | invoiceNo: "FP20241201001", |
| | | invoiceCode: "123456789", |
| | | invoiceDate: "2024-12-01", |
| | | buyerName: "客æ·Aå
¬å¸", |
| | | sellerName: "æ¬å
¬å¸", |
| | | amount: 10000.00, |
| | | taxAmount: 1300.00, |
| | | totalAmount: 11300.00, |
| | | status: "issued", |
| | | taxControlStatus: "synced", |
| | | createTime: "2024-12-01 10:00:00" |
| | | }, |
| | | { |
| | | id: "2", |
| | | invoiceNo: "FP20241201002", |
| | | invoiceCode: "123456790", |
| | | invoiceDate: "2024-12-01", |
| | | buyerName: "客æ·Bå
¬å¸", |
| | | sellerName: "æ¬å
¬å¸", |
| | | amount: 5000.00, |
| | | taxAmount: 650.00, |
| | | totalAmount: 5650.00, |
| | | status: "pending", |
| | | taxControlStatus: "pending", |
| | | createTime: "2024-12-01 14:30:00" |
| | | } |
| | | ]; |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const statusMap = { |
| | | draft: "", |
| | | pending: "warning", |
| | | issuing: "warning", |
| | | issued: "success", |
| | | failed: "danger", |
| | | cancelled: "info" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | draft: "è稿", |
| | | pending: "å¾
å¼ç¥¨", |
| | | issuing: "å¼ç¥¨ä¸", |
| | | issued: "å·²å¼ç¥¨", |
| | | failed: "å¼ç¥¨å¤±è´¥", |
| | | cancelled: "å·²ä½åº" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // è·åç¨æ§å¹³å°ç¶æç±»å |
| | | const getTaxControlStatusType = (status) => { |
| | | const statusMap = { |
| | | pending: "warning", |
| | | syncing: "warning", |
| | | synced: "success", |
| | | failed: "danger" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åç¨æ§å¹³å°ç¶æææ¬ |
| | | const getTaxControlStatusText = (status) => { |
| | | const statusMap = { |
| | | pending: "å¾
忥", |
| | | syncing: "忥ä¸", |
| | | synced: "已忥", |
| | | failed: "åæ¥å¤±è´¥" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // æ¥è¯¢ |
| | | const handleQuery = () => { |
| | | current.value = 1; |
| | | loadData(); |
| | | }; |
| | | |
| | | // éç½®æ¥è¯¢ |
| | | const resetQuery = () => { |
| | | Object.assign(queryParams, { |
| | | invoiceNo: "", |
| | | status: "", |
| | | dateRange: [] |
| | | }); |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // å è½½æ°æ® |
| | | const loadData = () => { |
| | | loading.value = true; |
| | | // 模æAPIè°ç¨ |
| | | setTimeout(() => { |
| | | tableData.value = mockData; |
| | | total.value = mockData.length; |
| | | loading.value = false; |
| | | }, 500); |
| | | }; |
| | | |
| | | // å页å¤ç |
| | | const handlePagination = (pagination) => { |
| | | current.value = pagination.page; |
| | | pageSize.value = pagination.limit; |
| | | loadData(); |
| | | }; |
| | | |
| | | // éæ©åå |
| | | const handleSelectionChange = (selection) => { |
| | | selectedIds.value = selection.map(item => item.id); |
| | | }; |
| | | |
| | | // æ°å¢ |
| | | const handleAdd = () => { |
| | | isEdit.value = false; |
| | | title.value = "æ°å¢å票"; |
| | | form.value = { |
| | | buyerName: "", |
| | | buyerTaxNo: "", |
| | | buyerAddress: "", |
| | | buyerBankAccount: "", |
| | | sellerName: "æ¬å
¬å¸", |
| | | sellerTaxNo: "123456789012345678", |
| | | sellerAddress: "å
¬å¸å°å", |
| | | sellerBankAccount: "é¶è¡è´¦æ·", |
| | | items: [], |
| | | remark: "" |
| | | }; |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | |
| | | // ç¼è¾ |
| | | const handleEdit = (row) => { |
| | | isEdit.value = true; |
| | | title.value = "ç¼è¾å票"; |
| | | form.value = { ...row }; |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | |
| | | // æ¥ç |
| | | const handleView = (row) => { |
| | | viewForm.value = { ...row }; |
| | | dialogViewVisible.value = true; |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = (row) => { |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤å票 ${row.invoiceNo} åï¼`, |
| | | "æç¤º", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | // 模æå é¤ |
| | | const index = tableData.value.findIndex(item => item.id === row.id); |
| | | if (index > -1) { |
| | | tableData.value.splice(index, 1); |
| | | total.value--; |
| | | ElMessage.success("å 餿å"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æ¹éå é¤ |
| | | const handleBatchDelete = () => { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©è¦å é¤çè®°å½"); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤éä¸ç ${selectedIds.value.length} æ¡è®°å½åï¼`, |
| | | "æç¤º", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | // æ¨¡ææ¹éå é¤ |
| | | tableData.value = tableData.value.filter(item => !selectedIds.value.includes(item.id)); |
| | | total.value = tableData.value.length; |
| | | selectedIds.value = []; |
| | | ElMessage.success("æ¹éå 餿å"); |
| | | }); |
| | | }; |
| | | |
| | | // ä¸è½½å票 |
| | | const handleDownload = (row) => { |
| | | if (row.status !== 'issued') { |
| | | ElMessage.warning("åªæå·²å¼ç¥¨çå票æè½ä¸è½½"); |
| | | return; |
| | | } |
| | | |
| | | // æ¾ç¤ºä¸è½½éé¡¹å¯¹è¯æ¡ |
| | | downloadDialogVisible.value = true; |
| | | currentDownloadInvoice.value = row; |
| | | }; |
| | | |
| | | // æ¹éä¸è½½ |
| | | const handleBatchDownload = () => { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©è¦ä¸è½½çè®°å½"); |
| | | return; |
| | | } |
| | | |
| | | // æ£æ¥éä¸çåç¥¨ç¶æ |
| | | const selectedInvoices = tableData.value.filter(item => selectedIds.value.includes(item.id)); |
| | | const issuedInvoices = selectedInvoices.filter(item => item.status === 'issued'); |
| | | |
| | | if (issuedInvoices.length === 0) { |
| | | ElMessage.warning("éä¸çåç¥¨ä¸æ²¡æå·²å¼ç¥¨çå票"); |
| | | return; |
| | | } |
| | | |
| | | if (issuedInvoices.length < selectedInvoices.length) { |
| | | ElMessage.warning(`éä¸ç${selectedInvoices.length}å¼ å票ä¸ï¼åªæ${issuedInvoices.length}å¼ å·²å¼ç¥¨ï¼å°åªä¸è½½å·²å¼ç¥¨çå票`); |
| | | } |
| | | |
| | | // æ¾ç¤ºæ¹éä¸è½½éé¡¹å¯¹è¯æ¡ |
| | | batchDownloadDialogVisible.value = true; |
| | | batchDownloadInvoices.value = issuedInvoices; |
| | | }; |
| | | |
| | | // åæ¥ç¨æ§å¹³å° |
| | | const handleSyncTaxControl = () => { |
| | | dialogSyncVisible.value = true; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = (formData) => { |
| | | if (isEdit.value) { |
| | | // ç¼è¾ |
| | | const index = tableData.value.findIndex(item => item.id === formData.id); |
| | | if (index > -1) { |
| | | tableData.value[index] = { ...formData }; |
| | | ElMessage.success("ç¼è¾æå"); |
| | | } |
| | | } else { |
| | | // æ°å¢ |
| | | const newItem = { |
| | | id: Date.now().toString(), |
| | | invoiceNo: `FP${Date.now()}`, |
| | | invoiceCode: "123456789", |
| | | invoiceDate: new Date().toISOString().split('T')[0], |
| | | buyerName: formData.buyerName, |
| | | sellerName: formData.sellerName, |
| | | amount: formData.items.reduce((sum, item) => sum + (item.amount || 0), 0), |
| | | taxAmount: formData.items.reduce((sum, item) => sum + (item.taxAmount || 0), 0), |
| | | totalAmount: formData.items.reduce((sum, item) => sum + (item.totalAmount || 0), 0), |
| | | status: "draft", |
| | | taxControlStatus: "pending", |
| | | createTime: new Date().toLocaleString() |
| | | }; |
| | | tableData.value.unshift(newItem); |
| | | total.value++; |
| | | ElMessage.success("æ°å¢æå"); |
| | | } |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | |
| | | // 表åæååè° |
| | | const handleSuccess = () => { |
| | | loadData(); |
| | | }; |
| | | |
| | | // 忥æååè° |
| | | const handleSyncSuccess = () => { |
| | | loadData(); |
| | | ElMessage.success("ç¨æ§å¹³å°åæ¥æå"); |
| | | }; |
| | | |
| | | // å个ä¸è½½æååè° |
| | | const handleDownloadSuccess = () => { |
| | | downloadDialogVisible.value = false; |
| | | ElMessage.success("å票ä¸è½½æå"); |
| | | }; |
| | | |
| | | // æ¹éä¸è½½æååè° |
| | | const handleBatchDownloadSuccess = () => { |
| | | batchDownloadDialogVisible.value = false; |
| | | ElMessage.success("æ¹éä¸è½½æå"); |
| | | }; |
| | | |
| | | // 页é¢å è½½ |
| | | onMounted(() => { |
| | | loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .search-form { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .table-toolbar { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .el-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="market-analysis-container"> |
| | | |
| | | <!-- æ°æ®æ¦è§å¡ç --> |
| | | <el-row :gutter="20" class="data-overview"> |
| | | <el-col :span="6"> |
| | | <el-card class="overview-card" shadow="hover"> |
| | | <div class="card-content"> |
| | | <div class="card-icon price-icon"> |
| | | <el-icon><TrendCharts /></el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <div class="card-title">å¹³åç
¤ä»·</div> |
| | | <div class="card-value">Â¥{{ marketData.avgPrice.toFixed(2) }}</div> |
| | | <div class="card-change" :class="marketData.priceChange >= 0 ? 'positive' : 'negative'"> |
| | | {{ marketData.priceChange >= 0 ? '+' : '' }}{{ marketData.priceChange.toFixed(2) }}% |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="6"> |
| | | <el-card class="overview-card" shadow="hover"> |
| | | <div class="card-content"> |
| | | <div class="card-icon volume-icon"> |
| | | <el-icon><DataLine /></el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <div class="card-title">交æé</div> |
| | | <div class="card-value">{{ marketData.totalVolume }}ä¸å¨</div> |
| | | <div class="card-change" :class="marketData.volumeChange >= 0 ? 'positive' : 'negative'"> |
| | | {{ marketData.volumeChange >= 0 ? '+' : '' }}{{ marketData.volumeChange.toFixed(2) }}% |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="6"> |
| | | <el-card class="overview-card" shadow="hover"> |
| | | <div class="card-content"> |
| | | <div class="card-icon customer-icon"> |
| | | <el-icon><User /></el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <div class="card-title">æ´»è·å®¢æ·</div> |
| | | <div class="card-value">{{ marketData.activeCustomers }}å®¶</div> |
| | | <div class="card-change" :class="marketData.customerChange >= 0 ? 'positive' : 'negative'"> |
| | | {{ marketData.customerChange >= 0 ? '+' : '' }}{{ marketData.customerChange.toFixed(2) }}% |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <el-col :span="6"> |
| | | <el-card class="overview-card" shadow="hover"> |
| | | <div class="card-content"> |
| | | <div class="card-icon trend-icon"> |
| | | <el-icon><TrendCharts /></el-icon> |
| | | </div> |
| | | <div class="card-info"> |
| | | <div class="card-title">å¸åºè¶å¿</div> |
| | | <div class="card-value">{{ marketData.marketTrend }}</div> |
| | | <div class="card-change" :class="marketData.trendScore >= 0 ? 'positive' : 'negative'"> |
| | | ä¿¡å¿ææ°: {{ marketData.trendScore }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 主è¦åæåºå --> |
| | | <el-row :gutter="20" class="main-analysis"> |
| | | <!-- ä»·æ ¼è¶å¿åæ --> |
| | | <el-col :span="16"> |
| | | <el-card class="analysis-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>ç
¤ç§ä»·æ ¼è¶å¿åæ</span> |
| | | <div class="header-controls"> |
| | | <el-select v-model="selectedCoalType" placeholder="éæ©ç
¤ç§" size="small" style="width: 120px"> |
| | | <el-option label="æ··ç
¤" value="mixed" /> |
| | | <el-option label="ç²¾ç
¤" value="refined" /> |
| | | <el-option label="å¨åç
¤" value="power" /> |
| | | <el-option label="ç¦ç
¤" value="coking" /> |
| | | </el-select> |
| | | <el-select v-model="selectedRegion" placeholder="éæ©äº§å°" size="small" style="width: 120px"> |
| | | <el-option label="山西" value="shanxi" /> |
| | | <el-option label="å
èå¤" value="neimenggu" /> |
| | | <el-option label="é西" value="shaanxi" /> |
| | | <el-option label="æ°ç" value="xinjiang" /> |
| | | </el-select> |
| | | <el-select v-model="selectedPeriod" placeholder="æ¶é´å¨æ" size="small" style="width: 100px"> |
| | | <el-option label="æ¥" value="day" /> |
| | | <el-option label="å¨" value="week" /> |
| | | <el-option label="æ" value="month" /> |
| | | <el-option label="å£" value="quarter" /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="chart-container"> |
| | | <div ref="priceChartRef" class="chart" style="height: 400px;"></div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <!-- 客æ·è¡ä¸ºåæ --> |
| | | <el-col :span="8"> |
| | | <el-card class="analysis-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>客æ·è¡ä¸ºç»å</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="customer-analysis"> |
| | | <div class="customer-type-distribution"> |
| | | <h4>客æ·ç±»ååå¸</h4> |
| | | <div ref="customerChartRef" class="chart" style="height: 200px;"></div> |
| | | </div> |
| | | |
| | | <div class="purchase-preference"> |
| | | <h4>éè´å好åæ</h4> |
| | | <div class="preference-item" v-for="item in customerPreferences" :key="item.type"> |
| | | <div class="preference-label">{{ item.type }}</div> |
| | | <div class="preference-bar"> |
| | | <div class="bar-fill" :style="{ width: item.percentage + '%' }"></div> |
| | | </div> |
| | | <div class="preference-value">{{ item.percentage }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 详ç»åæåºå --> |
| | | <el-row :gutter="20" class="detail-analysis"> |
| | | <!-- åºåä»·æ ¼å¯¹æ¯ --> |
| | | <el-col :span="12"> |
| | | <el-card class="analysis-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>åºåä»·æ ¼å¯¹æ¯</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="region-comparison"> |
| | | <div ref="regionChartRef" class="chart" style="height: 300px;"></div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | |
| | | <!-- 客æ·éè´å¨æåæ --> |
| | | <el-col :span="12"> |
| | | <el-card class="analysis-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>客æ·éè´å¨æåæ</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="purchase-cycle"> |
| | | <div ref="cycleChartRef" class="chart" style="height: 300px;"></div> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- æºè½æ¨èåºå --> |
| | | <el-row :gutter="20" class="smart-recommendations"> |
| | | <el-col :span="24"> |
| | | <el-card class="analysis-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>æºè½è¥éæ¨è</span> |
| | | <el-tag type="warning" size="small">AIç®æ³é©±å¨</el-tag> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="recommendations-content"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <div class="recommendation-section"> |
| | | <h4>个æ§åå®ä»·å»ºè®®</h4> |
| | | <div class="pricing-suggestions"> |
| | | <div class="suggestion-item" v-for="suggestion in pricingSuggestions" :key="suggestion.id"> |
| | | <div class="suggestion-header"> |
| | | <span class="customer-name">{{ suggestion.customerName }}</span> |
| | | <el-tag :type="suggestion.priority" size="small">{{ suggestion.priorityText }}</el-tag> |
| | | </div> |
| | | <div class="suggestion-content"> |
| | | <p>å»ºè®®ä»·æ ¼ï¼Â¥{{ suggestion.suggestedPrice }}/å¨</p> |
| | | <p>议价空é´ï¼{{ suggestion.negotiationSpace }}%</p> |
| | | <p>æ¨åæ¶æºï¼{{ suggestion.timing }}</p> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | |
| | | <el-col :span="8"> |
| | | <div class="recommendation-section"> |
| | | <h4>çéç
¤åæ¨è</h4> |
| | | <div class="hot-coal-types"> |
| | | <div class="coal-type-item" v-for="coal in hotCoalTypes" :key="coal.id"> |
| | | <div class="coal-info"> |
| | | <div class="coal-name">{{ coal.name }}</div> |
| | | <div class="coal-spec">{{ coal.specification }}</div> |
| | | </div> |
| | | <div class="coal-metrics"> |
| | | <div class="metric"> |
| | | <span class="label">çåº¦ææ°ï¼</span> |
| | | <span class="value">{{ coal.heatIndex }}</span> |
| | | </div> |
| | | <div class="metric"> |
| | | <span class="label">åºåç¶æï¼</span> |
| | | <el-tag :type="coal.stockStatus === 'å
è¶³' ? 'success' : 'warning'" size="small"> |
| | | {{ coal.stockStatus }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | |
| | | <el-col :span="8"> |
| | | <div class="recommendation-section"> |
| | | <h4>客æ·ç²æ§æå</h4> |
| | | <div class="loyalty-improvements"> |
| | | <div class="improvement-item" v-for="improvement in loyaltyImprovements" :key="improvement.id"> |
| | | <div class="improvement-header"> |
| | | <span class="strategy-name">{{ improvement.strategyName }}</span> |
| | | <span class="success-rate">æåç: {{ improvement.successRate }}%</span> |
| | | </div> |
| | | <div class="improvement-content"> |
| | | <p>{{ improvement.description }}</p> |
| | | <div class="action-buttons"> |
| | | <el-button type="primary" size="small">æ§è¡çç¥</el-button> |
| | | <el-button size="small">æ¥ç详æ
</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import { Refresh, TrendCharts, DataLine, User } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | |
| | | // ååºå¼æ°æ® |
| | | const refreshing = ref(false) |
| | | const lastUpdateTime = ref('') |
| | | const selectedCoalType = ref('mixed') |
| | | const selectedRegion = ref('shanxi') |
| | | const selectedPeriod = ref('month') |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const priceChartRef = ref(null) |
| | | const customerChartRef = ref(null) |
| | | const regionChartRef = ref(null) |
| | | const cycleChartRef = ref(null) |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let priceChart = null |
| | | let customerChart = null |
| | | let regionChart = null |
| | | let cycleChart = null |
| | | |
| | | // å¸åºæ°æ® |
| | | const marketData = reactive({ |
| | | avgPrice: 1250.50, |
| | | priceChange: 2.35, |
| | | totalVolume: 1250.8, |
| | | volumeChange: -1.25, |
| | | activeCustomers: 156, |
| | | customerChange: 3.45, |
| | | marketTrend: 'ç¨³ä¸æå', |
| | | trendScore: 8.5 |
| | | }) |
| | | |
| | | // 客æ·åå¥½æ°æ® |
| | | const customerPreferences = ref([ |
| | | { type: 'ç¦åå', percentage: 35 }, |
| | | { type: 'çµå', percentage: 28 }, |
| | | { type: 'é¢å', percentage: 22 }, |
| | | { type: 'åå·¥å', percentage: 15 } |
| | | ]) |
| | | |
| | | // å®ä»·å»ºè®® |
| | | const pricingSuggestions = ref([ |
| | | { |
| | | id: 1, |
| | | customerName: '山西ç¦åéå¢', |
| | | priority: 'high', |
| | | priorityText: 'é«ä¼å
级', |
| | | suggestedPrice: 1280, |
| | | negotiationSpace: 5.2, |
| | | timing: 'æ¬å¨å
' |
| | | }, |
| | | { |
| | | id: 2, |
| | | customerName: 'åè½çµå', |
| | | priority: 'medium', |
| | | priorityText: 'ä¸ä¼å
级', |
| | | suggestedPrice: 1250, |
| | | negotiationSpace: 3.8, |
| | | timing: 'ä¸å¨å' |
| | | }, |
| | | { |
| | | id: 3, |
| | | customerName: 'å®é¢éå¢', |
| | | priority: 'low', |
| | | priorityText: 'ä½ä¼å
级', |
| | | suggestedPrice: 1220, |
| | | negotiationSpace: 2.5, |
| | | timing: 'æ¬æå
' |
| | | } |
| | | ]) |
| | | |
| | | // çéç
¤å |
| | | const hotCoalTypes = ref([ |
| | | { |
| | | id: 1, |
| | | name: 'ä¼è´¨æ··ç
¤', |
| | | specification: 'åçé5500大å¡', |
| | | heatIndex: 9.2, |
| | | stockStatus: 'å
è¶³' |
| | | }, |
| | | { |
| | | id: 2, |
| | | name: 'ç²¾æ´ç¦ç
¤', |
| | | specification: 'ç°åâ¤8%', |
| | | heatIndex: 8.8, |
| | | stockStatus: 'å
è¶³' |
| | | }, |
| | | { |
| | | id: 3, |
| | | name: 'å¨åç
¤', |
| | | specification: 'åçé6000大å¡', |
| | | heatIndex: 8.5, |
| | | stockStatus: 'ç´§å¼ ' |
| | | } |
| | | ]) |
| | | |
| | | // 客æ·ç²æ§æåçç¥ |
| | | const loyaltyImprovements = ref([ |
| | | { |
| | | id: 1, |
| | | strategyName: 'å·®å¼åå®ä»·çç¥', |
| | | successRate: 85, |
| | | description: 'æ ¹æ®å®¢æ·éè´é¢æ¬¡å议价è½åï¼å¶å®ä¸ªæ§åä»·æ ¼æ¹æ¡' |
| | | }, |
| | | { |
| | | id: 2, |
| | | strategyName: 'ç²¾åæ¨åèå¥', |
| | | successRate: 78, |
| | | description: 'åºäºå®¢æ·éè´å¨æåæï¼å¨æä½³æ¶æºæ¨éç¸å
³äº§å' |
| | | }, |
| | | { |
| | | id: 3, |
| | | strategyName: 'å¢å¼æå¡å
', |
| | | successRate: 92, |
| | | description: 'æä¾ç©æµé
éãè´¨éæ£æµçå¢å¼æå¡ï¼æåå®¢æ·æ»¡æåº¦' |
| | | } |
| | | ]) |
| | | |
| | | // æ¨¡ææ°æ®çæ |
| | | const generateMockData = () => { |
| | | // çæä»·æ ¼è¶å¿æ°æ® |
| | | const dates = [] |
| | | const prices = [] |
| | | const volumes = [] |
| | | |
| | | for (let i = 30; i >= 0; i--) { |
| | | const date = new Date() |
| | | date.setDate(date.getDate() - i) |
| | | dates.push(date.toLocaleDateString()) |
| | | |
| | | const basePrice = 1200 + Math.random() * 200 |
| | | prices.push(basePrice) |
| | | |
| | | const baseVolume = 30 + Math.random() * 20 |
| | | volumes.push(baseVolume) |
| | | } |
| | | |
| | | return { dates, prices, volumes } |
| | | } |
| | | |
| | | // åå§åä»·æ ¼è¶å¿å¾è¡¨ |
| | | const initPriceChart = () => { |
| | | if (!priceChartRef.value) return |
| | | |
| | | priceChart = echarts.init(priceChartRef.value) |
| | | const { dates, prices, volumes } = generateMockData() |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'æ··ç
¤æåº¦ä»·æ ¼ååè¶å¿', |
| | | left: 'center', |
| | | textStyle: { |
| | | fontSize: 16, |
| | | fontWeight: 'bold' |
| | | } |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'cross' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['ä»·æ ¼(å
/å¨)', '交æé(ä¸å¨)'], |
| | | top: 30 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: dates, |
| | | axisLabel: { |
| | | rotate: 45 |
| | | } |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: 'value', |
| | | name: 'ä»·æ ¼(å
/å¨)', |
| | | position: 'left' |
| | | }, |
| | | { |
| | | type: 'value', |
| | | name: '交æé(ä¸å¨)', |
| | | position: 'right' |
| | | } |
| | | ], |
| | | series: [ |
| | | { |
| | | name: 'ä»·æ ¼(å
/å¨)', |
| | | type: 'line', |
| | | data: prices, |
| | | smooth: true, |
| | | lineStyle: { |
| | | color: '#409EFF', |
| | | width: 3 |
| | | }, |
| | | itemStyle: { |
| | | color: '#409EFF' |
| | | } |
| | | }, |
| | | { |
| | | name: '交æé(ä¸å¨)', |
| | | type: 'bar', |
| | | yAxisIndex: 1, |
| | | data: volumes, |
| | | itemStyle: { |
| | | color: '#67C23A' |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | |
| | | priceChart.setOption(option) |
| | | } |
| | | |
| | | // åå§å客æ·åå¸å¾è¡¨ |
| | | const initCustomerChart = () => { |
| | | if (!customerChartRef.value) return |
| | | |
| | | customerChart = echarts.init(customerChartRef.value) |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: 'item', |
| | | formatter: '{a} <br/>{b}: {c} ({d}%)' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: '客æ·ç±»å', |
| | | type: 'pie', |
| | | radius: ['40%', '70%'], |
| | | avoidLabelOverlap: false, |
| | | label: { |
| | | show: false, |
| | | position: 'center' |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: '18', |
| | | fontWeight: 'bold' |
| | | } |
| | | }, |
| | | labelLine: { |
| | | show: false |
| | | }, |
| | | data: [ |
| | | { value: 35, name: 'ç¦åå' }, |
| | | { value: 28, name: 'çµå' }, |
| | | { value: 22, name: 'é¢å' }, |
| | | { value: 15, name: 'åå·¥å' } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | |
| | | customerChart.setOption(option) |
| | | } |
| | | |
| | | // åå§ååºå对æ¯å¾è¡¨ |
| | | const initRegionChart = () => { |
| | | if (!regionChartRef.value) return |
| | | |
| | | regionChart = echarts.init(regionChartRef.value) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: 'å产å°ç
¤ä»·å¯¹æ¯', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | }, |
| | | legend: { |
| | | data: ['æ··ç
¤', 'ç²¾ç
¤', 'å¨åç
¤', 'ç¦ç
¤'], |
| | | top: 30 |
| | | }, |
| | | grid: { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | }, |
| | | xAxis: { |
| | | type: 'category', |
| | | data: ['山西', 'å
èå¤', 'é西', 'æ°ç'] |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | name: 'ä»·æ ¼(å
/å¨)' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'æ··ç
¤', |
| | | type: 'bar', |
| | | data: [1250, 1180, 1220, 1150] |
| | | }, |
| | | { |
| | | name: 'ç²¾ç
¤', |
| | | type: 'bar', |
| | | data: [1350, 1280, 1320, 1250] |
| | | }, |
| | | { |
| | | name: 'å¨åç
¤', |
| | | type: 'bar', |
| | | data: [1150, 1080, 1120, 1050] |
| | | }, |
| | | { |
| | | name: 'ç¦ç
¤', |
| | | type: 'bar', |
| | | data: [1450, 1380, 1420, 1350] |
| | | } |
| | | ] |
| | | } |
| | | |
| | | regionChart.setOption(option) |
| | | } |
| | | |
| | | // åå§åéè´å¨æå¾è¡¨ |
| | | const initCycleChart = () => { |
| | | if (!cycleChartRef.value) return |
| | | |
| | | cycleChart = echarts.init(cycleChartRef.value) |
| | | |
| | | const option = { |
| | | title: { |
| | | text: '客æ·éè´å¨æåå¸', |
| | | left: 'center' |
| | | }, |
| | | tooltip: { |
| | | trigger: 'item' |
| | | }, |
| | | series: [ |
| | | { |
| | | name: 'éè´å¨æ', |
| | | type: 'funnel', |
| | | left: '10%', |
| | | top: 60, |
| | | bottom: 60, |
| | | width: '80%', |
| | | height: '80%', |
| | | min: 0, |
| | | max: 100, |
| | | minSize: '0%', |
| | | maxSize: '100%', |
| | | sort: 'descending', |
| | | gap: 2, |
| | | label: { |
| | | show: true, |
| | | position: 'inside' |
| | | }, |
| | | labelLine: { |
| | | length: 10, |
| | | lineStyle: { |
| | | width: 1, |
| | | type: 'solid' |
| | | } |
| | | }, |
| | | itemStyle: { |
| | | borderColor: '#fff', |
| | | borderWidth: 1 |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | fontSize: 20 |
| | | } |
| | | }, |
| | | data: [ |
| | | { value: 100, name: 'é«é¢å®¢æ·(å¨éè´)' }, |
| | | { value: 80, name: 'ä¸é¢å®¢æ·(æéè´)' }, |
| | | { value: 60, name: 'ä½é¢å®¢æ·(å£éè´)' }, |
| | | { value: 40, name: 'å¶å客æ·(å¹´éè´)' } |
| | | ] |
| | | } |
| | | ] |
| | | } |
| | | |
| | | cycleChart.setOption(option) |
| | | } |
| | | |
| | | // å·æ°æ°æ® |
| | | const refreshData = async () => { |
| | | refreshing.value = true |
| | | |
| | | try { |
| | | // æ¨¡ææ°æ®å·æ° |
| | | await new Promise(resolve => setTimeout(resolve, 2000)) |
| | | |
| | | // æ´æ°å¸åºæ°æ® |
| | | marketData.avgPrice = 1200 + Math.random() * 200 |
| | | marketData.priceChange = (Math.random() - 0.5) * 10 |
| | | marketData.totalVolume = 1000 + Math.random() * 500 |
| | | marketData.volumeChange = (Math.random() - 0.5) * 8 |
| | | marketData.activeCustomers = 140 + Math.floor(Math.random() * 40) |
| | | marketData.customerChange = (Math.random() - 0.5) * 6 |
| | | marketData.trendScore = 7 + Math.random() * 3 |
| | | |
| | | // æ´æ°æ¶é´ |
| | | lastUpdateTime.value = new Date().toLocaleString() |
| | | |
| | | // éæ°åå§åå¾è¡¨ |
| | | await nextTick() |
| | | initPriceChart() |
| | | initCustomerChart() |
| | | initRegionChart() |
| | | initCycleChart() |
| | | |
| | | ElMessage.success('æ°æ®å·æ°æå') |
| | | } catch (error) { |
| | | ElMessage.error('æ°æ®å·æ°å¤±è´¥') |
| | | } finally { |
| | | refreshing.value = false |
| | | } |
| | | } |
| | | |
| | | // èªå¨å·æ°å®æ¶å¨ |
| | | let refreshTimer = null |
| | | |
| | | // å¯å¨èªå¨å·æ° |
| | | const startAutoRefresh = () => { |
| | | refreshTimer = setInterval(() => { |
| | | refreshData() |
| | | }, 10 * 60 * 1000) // 10åé |
| | | } |
| | | |
| | | // 忢èªå¨å·æ° |
| | | const stopAutoRefresh = () => { |
| | | if (refreshTimer) { |
| | | clearInterval(refreshTimer) |
| | | refreshTimer = null |
| | | } |
| | | } |
| | | |
| | | // çå¬çªå£å¤§å°åå |
| | | const handleResize = () => { |
| | | if (priceChart) priceChart.resize() |
| | | if (customerChart) customerChart.resize() |
| | | if (regionChart) regionChart.resize() |
| | | if (cycleChart) cycleChart.resize() |
| | | } |
| | | |
| | | // çå½å¨æ |
| | | onMounted(async () => { |
| | | // åå§åæ¶é´ |
| | | lastUpdateTime.value = new Date().toLocaleString() |
| | | |
| | | // çå¾
DOM渲æå®æ |
| | | await nextTick() |
| | | |
| | | // åå§åå¾è¡¨ |
| | | initPriceChart() |
| | | initCustomerChart() |
| | | initRegionChart() |
| | | initCycleChart() |
| | | |
| | | // å¯å¨èªå¨å·æ° |
| | | startAutoRefresh() |
| | | |
| | | // çå¬çªå£å¤§å°åå |
| | | window.addEventListener('resize', handleResize) |
| | | }) |
| | | |
| | | onUnmounted(() => { |
| | | // 忢èªå¨å·æ° |
| | | stopAutoRefresh() |
| | | |
| | | // 鿝å¾è¡¨ |
| | | if (priceChart) priceChart.dispose() |
| | | if (customerChart) customerChart.dispose() |
| | | if (regionChart) regionChart.dispose() |
| | | if (cycleChart) cycleChart.dispose() |
| | | |
| | | // ç§»é¤äºä»¶çå¬ |
| | | window.removeEventListener('resize', handleResize) |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .market-analysis-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .page-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: white; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .page-header h1 { |
| | | margin: 0; |
| | | color: #303133; |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .header-info { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .update-time { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .data-overview { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .overview-card { |
| | | height: 120px; |
| | | } |
| | | |
| | | .card-content { |
| | | display: flex; |
| | | align-items: center; |
| | | height: 100%; |
| | | } |
| | | |
| | | .card-icon { |
| | | width: 60px; |
| | | height: 60px; |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | margin-right: 15px; |
| | | font-size: 24px; |
| | | color: white; |
| | | } |
| | | |
| | | .price-icon { |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | } |
| | | |
| | | .volume-icon { |
| | | background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); |
| | | } |
| | | |
| | | .customer-icon { |
| | | background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); |
| | | } |
| | | |
| | | .trend-icon { |
| | | background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%); |
| | | } |
| | | |
| | | .card-info { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-title { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .card-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .card-change { |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .card-change.positive { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .card-change.negative { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .main-analysis { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .analysis-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .header-controls { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .chart-container { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .chart { |
| | | width: 100%; |
| | | } |
| | | |
| | | .customer-analysis h4 { |
| | | margin: 0 0 15px 0; |
| | | color: #303133; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .customer-type-distribution { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .preference-item { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .preference-label { |
| | | width: 80px; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .preference-bar { |
| | | flex: 1; |
| | | height: 8px; |
| | | background-color: #f0f0f0; |
| | | border-radius: 4px; |
| | | margin: 0 10px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .bar-fill { |
| | | height: 100%; |
| | | background: linear-gradient(90deg, #409eff 0%, #67c23a 100%); |
| | | border-radius: 4px; |
| | | transition: width 0.3s ease; |
| | | } |
| | | |
| | | .preference-value { |
| | | width: 40px; |
| | | font-size: 12px; |
| | | color: #409eff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .detail-analysis { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .smart-recommendations { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .recommendations-content { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .recommendation-section h4 { |
| | | margin: 0 0 15px 0; |
| | | color: #303133; |
| | | font-size: 14px; |
| | | border-bottom: 2px solid #409eff; |
| | | padding-bottom: 5px; |
| | | } |
| | | |
| | | .suggestion-item { |
| | | background: #f8f9fa; |
| | | border-radius: 6px; |
| | | padding: 12px; |
| | | margin-bottom: 12px; |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .suggestion-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .customer-name { |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .suggestion-content p { |
| | | margin: 5px 0; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .coal-type-item { |
| | | background: #f8f9fa; |
| | | border-radius: 6px; |
| | | padding: 12px; |
| | | margin-bottom: 12px; |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .coal-info { |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .coal-name { |
| | | font-weight: 500; |
| | | color: #303133; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .coal-spec { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .coal-metrics { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .metric { |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .metric .label { |
| | | color: #909399; |
| | | } |
| | | |
| | | .metric .value { |
| | | color: #409eff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .improvement-item { |
| | | background: #f8f9fa; |
| | | border-radius: 6px; |
| | | padding: 12px; |
| | | margin-bottom: 12px; |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .improvement-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .strategy-name { |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | .success-rate { |
| | | font-size: 12px; |
| | | color: #67c23a; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .improvement-content p { |
| | | margin: 5px 0 10px 0; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .action-buttons { |
| | | display: flex; |
| | | gap: 8px; |
| | | } |
| | | |
| | | /* ååºå¼è®¾è®¡ */ |
| | | @media (max-width: 1200px) { |
| | | .header-controls { |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | } |
| | | |
| | | .header-controls .el-select { |
| | | width: 100px !important; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .page-header { |
| | | flex-direction: column; |
| | | gap: 15px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .header-info { |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | form.value.fileList.push(...res.data.map((it,index)=>{ |
| | | return { |
| | | id:it.id, |
| | | url:it.downloadUrl, |
| | | name:it.originalFilename, |
| | | url:it.url, |
| | | name:it.name, |
| | | status:"success", |
| | | uid:file.uid |
| | | } |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <script setup> |
| | | import SimpleMultiFileUpload from "@/components/FileUpload/SimpleMultiFileUpload.vue" |
| | | import {ref, defineExpose} from "vue" |
| | | import {ElMessage, ElMessageBox} from "element-plus"; |
| | | import {findFileListByIds} from "@/plugins/download.js" |
| | | import {save} from "@/api/personnelManagement/employeeRecord.js" |
| | | |
| | | const ids = ref([]) |
| | | const fileList = ref([]) |
| | | const contract = ref({}) |
| | | const openDialog = (row, type) => { |
| | | dialogFormVisible.value = true; |
| | | contract.value = row |
| | | //æ¥è¯¢åºéä»¶ä¿¡æ¯è¿è¡æ¾ç¤º row.attachUpload |
| | | fileList.value = [] |
| | | if(row.attachUpload){ |
| | | findFileListByIds(row.attachUpload.split(",")).then(res => { |
| | | fileList.value = res.data |
| | | ids.value = fileList.value.map(it => it.id) |
| | | }) |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | const closeDia = () => { |
| | | emit('close') |
| | | dialogFormVisible.value = false |
| | | }; |
| | | |
| | | const saveDia = async () => { |
| | | // æäº¤ä¿å |
| | | await save({ |
| | | id: contract.value.id, |
| | | attachUpload: ids.value.join(',') |
| | | }).then(res => { |
| | | if (res.code === 200){ |
| | | ElMessage.success("æä½æå"); |
| | | } |
| | | }) |
| | | closeDia() |
| | | } |
| | | |
| | | const emit = defineEmits(['close']) |
| | | const dialogFormVisible = ref(false) |
| | | defineExpose({ |
| | | openDialog |
| | | }) |
| | | </script> |
| | | |
| | | <template> |
| | | <div> |
| | | <el-dialog |
| | | v-model="dialogFormVisible" |
| | | title="ä¸ä¼ éä»¶" |
| | | width="50%" |
| | | @close="closeDia" |
| | | > |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="closeDia">åæ¶</el-button> |
| | | <el-button @click="saveDia">ä¿å</el-button> |
| | | </div> |
| | | </template> |
| | | <SimpleMultiFileUpload |
| | | :key="contract.id" |
| | | v-model:ids="ids" |
| | | v-model:file-list="fileList" |
| | | /> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | |
| | | </style> |
| | |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | <files-dia ref="filesDia"></files-dia> |
| | | <files-dia ref="filesDia" @close="filesClose"></files-dia> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import dayjs from "dayjs"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import { getToken } from "@/utils/auth.js"; |
| | | import FilesDia from "./filesDia.vue"; |
| | | import FilesDia from "./components/filesDia.vue"; |
| | | const data = reactive({ |
| | | searchForm: { |
| | | staffName: "", |
| | |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | const getList = () => { |
| | | |
| | | const getList = async () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, ...page }; |
| | | params.entryDate = undefined |
| | | staffOnJobListPage(params).then(res => { |
| | | tableLoading.value = false; |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }) |
| | | let res = await staffOnJobListPage(params) |
| | | tableData.value = res.data.records |
| | | page.total = res.data.total; |
| | | tableLoading.value = false; |
| | | |
| | | }; |
| | | |
| | | |
| | | const filesClose = ()=>{ |
| | | getList() |
| | | } |
| | | |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¾
ç产æ°éï¼"> |
| | | <el-input v-model="pendingNum" placeholder="请è¾å
¥" clearable disabled/> |
| | | <el-input v-model="pendingNumTemp" placeholder="请è¾å
¥" clearable disabled/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | }, |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | const pendingNumTemp = ref(0) |
| | | const changeNum = (value) => { |
| | | console.log(value) |
| | | if (value > pendingNum.value) { |
| | | form.value.finishedNum = pendingNum.value |
| | | ElMessage.warning("æ¬æ¬¡ç产æ°éä¸å¯å¤§äºæäº§æ°é"); |
| | | } |
| | | pendingNum.value = pendingNum.value - form.value.finishedNum; |
| | | pendingNumTemp.value = pendingNum.value - value; |
| | | } |
| | | // Props å Emits |
| | | const props = defineProps({ |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogGenerateVisible" |
| | | @update:model-value="$emit('update:dialogGenerateVisible', $event)" |
| | | title="ä¸é®çæéè´§å" |
| | | width="1000px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div class="generate-container"> |
| | | <!-- éæ©éè´è®¢å --> |
| | | <el-card class="select-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>éæ©éè´è®¢å</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-form :inline="true" :model="searchForm" class="search-form"> |
| | | <el-form-item label="ä¾åºå"> |
| | | <el-select |
| | | v-model="searchForm.supplierId" |
| | | placeholder="è¯·éæ©ä¾åºå" |
| | | clearable |
| | | style="width: 200px" |
| | | @change="handleSupplierChange" |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in supplierList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="订åç¶æ"> |
| | | <el-select |
| | | v-model="searchForm.orderStatus" |
| | | placeholder="è¯·éæ©è®¢åç¶æ" |
| | | clearable |
| | | style="width: 150px" |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in orderStatusList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="searchOrders">æ¥è¯¢</el-button> |
| | | <el-button @click="resetSearch">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-table |
| | | :data="orderList" |
| | | @selection-change="handleOrderSelectionChange" |
| | | border |
| | | style="width: 100%" |
| | | max-height="300" |
| | | > |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column label="订åå·" prop="orderNo" width="180" /> |
| | | <el-table-column label="ä¾åºå" prop="supplierName" width="150" /> |
| | | <el-table-column label="è®¢åæ¥æ" prop="orderDate" width="120" /> |
| | | <el-table-column label="åååç§°" prop="coalName" width="150" /> |
| | | <el-table-column label="è®¢åæ°é" prop="orderQuantity" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.orderQuantity }} å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å·²æ¶è´§æ°é" prop="receivedQuantity" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.receivedQuantity }} å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¶æ" prop="status" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getOrderStatusType(scope.row.status)"> |
| | | {{ getOrderStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <!-- éè´§ä¿¡æ¯é
ç½® --> |
| | | <el-card class="config-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>éè´§ä¿¡æ¯é
ç½®</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-form :model="returnConfig" label-width="120px" class="config-form"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éè´§åå " prop="returnReason"> |
| | | <el-select |
| | | v-model="returnConfig.returnReason" |
| | | placeholder="è¯·éæ©éè´§åå " |
| | | style="width: 100%" |
| | | filterable |
| | | allow-create |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in returnReasonList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æä½å" prop="operatorId"> |
| | | <el-select |
| | | v-model="returnConfig.operatorId" |
| | | placeholder="è¯·éæ©æä½å" |
| | | style="width: 100%" |
| | | filterable |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in operatorList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input |
| | | v-model="returnConfig.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å¤æ³¨ä¿¡æ¯" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <!-- é¢è§éè´§å --> |
| | | <el-card class="preview-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>é¢è§éè´§å</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <div v-if="selectedOrders.length === 0" class="no-selection"> |
| | | <el-empty description="请å
éæ©è¦éè´§çéè´è®¢å" /> |
| | | </div> |
| | | |
| | | <div v-else class="preview-content"> |
| | | <el-table :data="previewReturnItems" border style="width: 100%"> |
| | | <el-table-column label="订åå·" prop="orderNo" width="150" /> |
| | | <el-table-column label="åååç§°" prop="coalName" width="150" /> |
| | | <el-table-column label="éè´§æ°é" width="120"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model.number="scope.row.returnQuantity" |
| | | placeholder="éè´§æ°é" |
| | | type="number" |
| | | @input="updateReturnQuantity(scope.$index)" |
| | | > |
| | | <template v-slot:suffix> |
| | | <span>å¨</span> |
| | | </template> |
| | | </el-input> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä»·" prop="unitPrice" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.unitPrice }} å
/å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å°è®¡" width="120"> |
| | | <template #default="scope"> |
| | | {{ (scope.row.returnQuantity * scope.row.unitPrice).toFixed(2) }} å
|
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <div class="preview-summary"> |
| | | <span class="summary-item"> |
| | | æ»æ°éï¼<strong>{{ getTotalReturnQuantity() }} å¨</strong> |
| | | </span> |
| | | <span class="summary-item"> |
| | | æ»éé¢ï¼<strong>{{ getTotalReturnAmount() }} å
</strong> |
| | | </span> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">åæ¶</el-button> |
| | | <el-button |
| | | type="primary" |
| | | @click="generateReturnOrder" |
| | | :loading="generateLoading" |
| | | :disabled="selectedOrders.length === 0" |
| | | > |
| | | çæéè´§å |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, watch } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogGenerateVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogGenerateVisible', 'success']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const searchForm = reactive({ |
| | | supplierId: "", |
| | | orderStatus: "" |
| | | }); |
| | | |
| | | const returnConfig = reactive({ |
| | | returnReason: "", |
| | | operatorId: "", |
| | | remark: "" |
| | | }); |
| | | |
| | | const orderList = ref([]); |
| | | const selectedOrders = ref([]); |
| | | const generateLoading = ref(false); |
| | | |
| | | // ä¾åºåå表 |
| | | const supplierList = ref([ |
| | | { value: "1", label: "ä¾åºåA" }, |
| | | { value: "2", label: "ä¾åºåB" }, |
| | | { value: "3", label: "ä¾åºåC" } |
| | | ]); |
| | | |
| | | // 订åç¶æå表 |
| | | const orderStatusList = ref([ |
| | | { value: "received", label: "å·²æ¶è´§" }, |
| | | { value: "partial_received", label: "é¨åæ¶è´§" }, |
| | | { value: "quality_issue", label: "è´¨éé®é¢" } |
| | | ]); |
| | | |
| | | // éè´§åå å表 |
| | | const returnReasonList = ref([ |
| | | { value: "è´¨éä¸åæ ¼", label: "è´¨éä¸åæ ¼" }, |
| | | { value: "交货æ»å", label: "交货æ»å" }, |
| | | { value: "è§æ ¼ä¸ç¬¦", label: "è§æ ¼ä¸ç¬¦" }, |
| | | { value: "æ°éä¸ç¬¦", label: "æ°éä¸ç¬¦" }, |
| | | { value: "å
¶ä»åå ", label: "å
¶ä»åå " } |
| | | ]); |
| | | |
| | | // æä½åå表 |
| | | const operatorList = ref([ |
| | | { value: "1", label: "éå¿å¼º" }, |
| | | { value: "2", label: "åç¾ç²" }, |
| | | { value: "3", label: "ç建å½" } |
| | | ]); |
| | | |
| | | // 模æéè´è®¢åæ°æ® |
| | | const mockOrderData = [ |
| | | { |
| | | id: "1", |
| | | orderNo: "CG20241201001", |
| | | supplierName: "ä¾åºåA", |
| | | orderDate: "2024-12-01", |
| | | coalName: "æ çç
¤", |
| | | orderQuantity: 100, |
| | | receivedQuantity: 80, |
| | | status: "partial_received", |
| | | unitPrice: 800 |
| | | }, |
| | | { |
| | | id: "2", |
| | | orderNo: "CG20241201002", |
| | | supplierName: "ä¾åºåA", |
| | | orderDate: "2024-12-01", |
| | | coalName: "çç
¤", |
| | | orderQuantity: 50, |
| | | receivedQuantity: 50, |
| | | status: "quality_issue", |
| | | unitPrice: 750 |
| | | }, |
| | | { |
| | | id: "3", |
| | | orderNo: "CG20241201003", |
| | | supplierName: "ä¾åºåB", |
| | | orderDate: "2024-12-01", |
| | | coalName: "è¤ç
¤", |
| | | orderQuantity: 80, |
| | | receivedQuantity: 60, |
| | | status: "partial_received", |
| | | unitPrice: 600 |
| | | } |
| | | ]; |
| | | |
| | | // è·å订åç¶æç±»å |
| | | const getOrderStatusType = (status) => { |
| | | const statusMap = { |
| | | received: "success", |
| | | partial_received: "warning", |
| | | quality_issue: "danger" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·å订åç¶æææ¬ |
| | | const getOrderStatusText = (status) => { |
| | | const statusMap = { |
| | | received: "å·²æ¶è´§", |
| | | partial_received: "é¨åæ¶è´§", |
| | | quality_issue: "è´¨éé®é¢" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // ä¾åºåååå¤ç |
| | | const handleSupplierChange = () => { |
| | | searchOrders(); |
| | | }; |
| | | |
| | | // æ¥è¯¢è®¢å |
| | | const searchOrders = () => { |
| | | // 模æAPIè°ç¨ |
| | | orderList.value = mockOrderData.filter(order => { |
| | | if (searchForm.supplierId && order.supplierName !== supplierList.value.find(s => s.value === searchForm.supplierId)?.label) { |
| | | return false; |
| | | } |
| | | if (searchForm.orderStatus && order.status !== searchForm.orderStatus) { |
| | | return false; |
| | | } |
| | | return true; |
| | | }); |
| | | }; |
| | | |
| | | // éç½®æç´¢ |
| | | const resetSearch = () => { |
| | | Object.assign(searchForm, { |
| | | supplierId: "", |
| | | orderStatus: "" |
| | | }); |
| | | searchOrders(); |
| | | }; |
| | | |
| | | // 订åéæ©åå |
| | | const handleOrderSelectionChange = (selection) => { |
| | | selectedOrders.value = selection; |
| | | }; |
| | | |
| | | // é¢è§éè´§åå |
| | | const previewReturnItems = computed(() => { |
| | | return selectedOrders.value.map(order => ({ |
| | | ...order, |
| | | returnQuantity: order.status === 'quality_issue' ? order.receivedQuantity : (order.orderQuantity - order.receivedQuantity) |
| | | })); |
| | | }); |
| | | |
| | | // æ´æ°éè´§æ°é |
| | | const updateReturnQuantity = (index) => { |
| | | const item = previewReturnItems.value[index]; |
| | | if (item.returnQuantity > item.receivedQuantity) { |
| | | item.returnQuantity = item.receivedQuantity; |
| | | ElMessage.warning("éè´§æ°éä¸è½è¶
è¿å·²æ¶è´§æ°é"); |
| | | } |
| | | }; |
| | | |
| | | // è®¡ç®æ»éè´§æ°é |
| | | const getTotalReturnQuantity = () => { |
| | | return previewReturnItems.value.reduce((total, item) => total + (item.returnQuantity || 0), 0); |
| | | }; |
| | | |
| | | // è®¡ç®æ»éè´§éé¢ |
| | | const getTotalReturnAmount = () => { |
| | | return previewReturnItems.value.reduce((total, item) => { |
| | | return total + ((item.returnQuantity || 0) * item.unitPrice); |
| | | }, 0).toFixed(2); |
| | | }; |
| | | |
| | | // çæéè´§å |
| | | const generateReturnOrder = async () => { |
| | | if (!returnConfig.returnReason) { |
| | | ElMessage.warning("è¯·éæ©éè´§åå "); |
| | | return; |
| | | } |
| | | if (!returnConfig.operatorId) { |
| | | ElMessage.warning("è¯·éæ©æä½å"); |
| | | return; |
| | | } |
| | | |
| | | generateLoading.value = true; |
| | | |
| | | try { |
| | | // 模æçæéè´§å |
| | | await new Promise(resolve => setTimeout(resolve, 1000)); |
| | | |
| | | const returnOrder = { |
| | | returnNo: `TH${Date.now()}`, |
| | | supplierName: selectedOrders.value[0]?.supplierName, |
| | | returnDate: new Date().toISOString().split('T')[0], |
| | | operatorName: operatorList.value.find(op => op.value === returnConfig.operatorId)?.label, |
| | | returnReason: returnConfig.returnReason, |
| | | returnQuantity: getTotalReturnQuantity(), |
| | | returnAmount: getTotalReturnAmount(), |
| | | status: "draft", |
| | | createTime: new Date().toLocaleString(), |
| | | remark: returnConfig.remark, |
| | | returnItems: previewReturnItems.value.map(item => ({ |
| | | coalId: item.id, |
| | | coalName: item.coalName, |
| | | specification: "æ åè§æ ¼", |
| | | quantity: item.returnQuantity, |
| | | unitPrice: item.unitPrice |
| | | })) |
| | | }; |
| | | |
| | | ElMessage.success("éè´§åçææå"); |
| | | emit('success', returnOrder); |
| | | handleClose(); |
| | | } catch (error) { |
| | | ElMessage.error("çæéè´§å失败"); |
| | | } finally { |
| | | generateLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | emit('update:dialogGenerateVisible', false); |
| | | // éç½®æ°æ® |
| | | Object.assign(searchForm, { |
| | | supplierId: "", |
| | | orderStatus: "" |
| | | }); |
| | | Object.assign(returnConfig, { |
| | | returnReason: "", |
| | | operatorId: "", |
| | | remark: "" |
| | | }); |
| | | selectedOrders.value = []; |
| | | orderList.value = mockOrderData; |
| | | }; |
| | | |
| | | // åå§åæ°æ® |
| | | watch(() => props.dialogGenerateVisible, (visible) => { |
| | | if (visible) { |
| | | orderList.value = mockOrderData; |
| | | } |
| | | }, { immediate: true }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .generate-container { |
| | | padding: 0; |
| | | } |
| | | |
| | | .select-card, |
| | | .config-card, |
| | | .preview-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .select-card:last-child, |
| | | .config-card:last-child, |
| | | .preview-card:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .search-form { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .config-form { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .no-selection { |
| | | text-align: center; |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .preview-content { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .preview-summary { |
| | | margin-top: 15px; |
| | | text-align: right; |
| | | padding: 10px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .summary-item { |
| | | margin-left: 20px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .summary-item strong { |
| | | color: #409eff; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogFormVisible" |
| | | @update:model-value="$emit('update:dialogFormVisible', $event)" |
| | | :title="title" |
| | | width="1000px" |
| | | :close-on-click-modal="false" |
| | | @close="handleClose" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="formData" |
| | | :rules="rules" |
| | | label-width="120px" |
| | | class="purchase-return-form" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ä¾åºå" prop="supplierId"> |
| | | <el-select |
| | | v-model="formData.supplierId" |
| | | placeholder="è¯·éæ©ä¾åºå" |
| | | style="width: 100%" |
| | | filterable |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in supplierList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åæ®æ¥æ" prop="returnDate"> |
| | | <el-date-picker |
| | | v-model="formData.returnDate" |
| | | type="date" |
| | | placeholder="è¯·éæ©åæ®æ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="æä½å" prop="operatorId"> |
| | | <el-select |
| | | v-model="formData.operatorId" |
| | | placeholder="è¯·éæ©æä½å" |
| | | style="width: 100%" |
| | | filterable |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in operatorList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éè´§åå " prop="returnReason"> |
| | | <el-select |
| | | v-model="formData.returnReason" |
| | | placeholder="è¯·éæ©éè´§åå " |
| | | style="width: 100%" |
| | | filterable |
| | | allow-create |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in returnReasonList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éè´§æ°é" prop="returnQuantity"> |
| | | <el-input |
| | | v-model.number="formData.returnQuantity" |
| | | placeholder="请è¾å
¥éè´§æ°é" |
| | | style="width: 100%" |
| | | > |
| | | <template v-slot:suffix> |
| | | <span>å¨</span> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éè´§éé¢" prop="returnAmount"> |
| | | <el-input |
| | | v-model.number="formData.returnAmount" |
| | | placeholder="请è¾å
¥éè´§éé¢" |
| | | style="width: 100%" |
| | | > |
| | | <template v-slot:suffix> |
| | | <span>å
</span> |
| | | </template> |
| | | </el-input> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-form-item label="éè´§ååä¿¡æ¯" prop="returnItems"> |
| | | <div class="return-items-container"> |
| | | <div class="return-items-header"> |
| | | <span>ååæç»</span> |
| | | <el-button type="primary" size="small" @click="addReturnItem"> |
| | | æ·»å åå |
| | | </el-button> |
| | | </div> |
| | | <el-table :data="formData.returnItems" border style="width: 100%"> |
| | | <el-table-column label="åååç§°" width="180"> |
| | | <template #default="scope"> |
| | | <el-select |
| | | v-model="scope.row.coalId" |
| | | placeholder="è¯·éæ©åå" |
| | | style="width: 100%" |
| | | filterable |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in coalList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è§æ ¼åå·" width="120"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model="scope.row.specification" |
| | | placeholder="è§æ ¼åå·" |
| | | /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æ°é" width="120"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model.number="scope.row.quantity" |
| | | placeholder="æ°é" |
| | | type="number" |
| | | > |
| | | <template v-slot:suffix> |
| | | <span>å¨</span> |
| | | </template> |
| | | </el-input> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä»·" width="120"> |
| | | <template #default="scope"> |
| | | <el-input |
| | | v-model.number="scope.row.unitPrice" |
| | | placeholder="åä»·" |
| | | type="number" |
| | | > |
| | | <template v-slot:suffix> |
| | | <span>å
/å¨</span> |
| | | </template> |
| | | </el-input> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å°è®¡" width="120"> |
| | | <template #default="scope"> |
| | | <span>{{ (scope.row.quantity * scope.row.unitPrice).toFixed(2) }} å
</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="removeReturnItem(scope.$index)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input |
| | | v-model="formData.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å¤æ³¨ä¿¡æ¯" |
| | | /> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleSubmit" :loading="submitLoading"> |
| | | æäº¤å®¡æ ¸ |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, watch, nextTick } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogFormVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | form: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "" |
| | | }, |
| | | isEdit: { |
| | | type: Boolean, |
| | | default: false |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogFormVisible', 'update:form', 'submit', 'success']); |
| | | |
| | | // ååºå¼æ°æ® |
| | | const formRef = ref(null); |
| | | const submitLoading = ref(false); |
| | | |
| | | // è¡¨åæ°æ® |
| | | const formData = reactive({ |
| | | supplierId: "", |
| | | returnDate: "", |
| | | operatorId: "", |
| | | returnReason: "", |
| | | returnQuantity: "", |
| | | returnAmount: "", |
| | | returnItems: [], |
| | | remark: "" |
| | | }); |
| | | |
| | | // åå§åè¡¨åæ°æ® |
| | | const initFormData = () => { |
| | | Object.assign(formData, { |
| | | supplierId: "", |
| | | returnDate: "", |
| | | operatorId: "", |
| | | returnReason: "", |
| | | returnQuantity: "", |
| | | returnAmount: "", |
| | | returnItems: [], |
| | | remark: "" |
| | | }); |
| | | }; |
| | | |
| | | // ä¾åºåå表 |
| | | const supplierList = ref([ |
| | | { value: "1", label: "ä¾åºåA" }, |
| | | { value: "2", label: "ä¾åºåB" }, |
| | | { value: "3", label: "ä¾åºåC" } |
| | | ]); |
| | | |
| | | // æä½åå表 |
| | | const operatorList = ref([ |
| | | { value: "1", label: "éå¿å¼º" }, |
| | | { value: "2", label: "åç¾ç²" }, |
| | | { value: "3", label: "ç建å½" } |
| | | ]); |
| | | |
| | | // éè´§åå å表 |
| | | const returnReasonList = ref([ |
| | | { value: "è´¨éä¸åæ ¼", label: "è´¨éä¸åæ ¼" }, |
| | | { value: "交货æ»å", label: "交货æ»å" }, |
| | | { value: "è§æ ¼ä¸ç¬¦", label: "è§æ ¼ä¸ç¬¦" }, |
| | | { value: "æ°éä¸ç¬¦", label: "æ°éä¸ç¬¦" }, |
| | | { value: "å
¶ä»åå ", label: "å
¶ä»åå " } |
| | | ]); |
| | | |
| | | // ååå表 |
| | | const coalList = ref([ |
| | | { value: "1", label: "æ çç
¤" }, |
| | | { value: "2", label: "çç
¤" }, |
| | | { value: "3", label: "è¤ç
¤" }, |
| | | { value: "4", label: "ç¦ç
¤" } |
| | | ]); |
| | | |
| | | // 表åéªè¯è§å |
| | | const rules = { |
| | | supplierId: [ |
| | | { required: true, message: "è¯·éæ©ä¾åºå", trigger: "change" } |
| | | ], |
| | | returnDate: [ |
| | | { required: true, message: "è¯·éæ©åæ®æ¥æ", trigger: "change" } |
| | | ], |
| | | operatorId: [ |
| | | { required: true, message: "è¯·éæ©æä½å", trigger: "change" } |
| | | ], |
| | | returnReason: [ |
| | | { required: true, message: "è¯·éæ©éè´§åå ", trigger: "change" } |
| | | ], |
| | | returnQuantity: [ |
| | | { required: true, message: "请è¾å
¥éè´§æ°é", trigger: "blur" }, |
| | | { type: "number", min: 0, message: "æ°éå¿
须大äº0", trigger: "blur" } |
| | | ], |
| | | returnItems: [ |
| | | { |
| | | type: "array", |
| | | required: true, |
| | | message: "请è³å°æ·»å ä¸ä¸ªéè´§åå", |
| | | trigger: "change", |
| | | validator: (rule, value, callback) => { |
| | | if (!value || value.length === 0) { |
| | | callback(new Error("请è³å°æ·»å ä¸ä¸ªéè´§åå")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | } |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | // çå¬è¡¨åæ°æ®åå |
| | | watch(() => props.form, (newVal) => { |
| | | Object.assign(formData, newVal); |
| | | if (!formData.returnItems || formData.returnItems.length === 0) { |
| | | formData.returnItems = []; |
| | | } |
| | | }, { deep: true, immediate: true }); |
| | | |
| | | // æ·»å éè´§åå |
| | | const addReturnItem = () => { |
| | | formData.returnItems.push({ |
| | | coalId: "", |
| | | specification: "", |
| | | quantity: 0, |
| | | unitPrice: 0 |
| | | }); |
| | | }; |
| | | |
| | | // å é¤éè´§åå |
| | | const removeReturnItem = (index) => { |
| | | formData.returnItems.splice(index, 1); |
| | | }; |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | emit('update:dialogFormVisible', false); |
| | | formRef.value?.resetFields(); |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = async () => { |
| | | if (!formRef.value) return; |
| | | |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | // éªè¯éè´§ååä¿¡æ¯ |
| | | if (formData.returnItems.length === 0) { |
| | | ElMessage.warning("请è³å°æ·»å ä¸ä¸ªéè´§åå"); |
| | | return; |
| | | } |
| | | |
| | | for (let item of formData.returnItems) { |
| | | if (!item.coalId) { |
| | | ElMessage.warning("è¯·éæ©åå"); |
| | | return; |
| | | } |
| | | if (!item.quantity || item.quantity <= 0) { |
| | | ElMessage.warning("请è¾å
¥ææçååæ°é"); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | submitLoading.value = true; |
| | | |
| | | // 模ææäº¤ |
| | | setTimeout(() => { |
| | | submitLoading.value = false; |
| | | ElMessage.success("æäº¤æå"); |
| | | emit('submit', { ...formData }); |
| | | handleClose(); |
| | | }, 1000); |
| | | |
| | | } catch (error) { |
| | | console.error('表åéªè¯å¤±è´¥:', error); |
| | | } |
| | | }; |
| | | |
| | | // è®¡ç®æ»æ°é |
| | | const calculateTotalQuantity = () => { |
| | | return formData.returnItems.reduce((total, item) => total + (item.quantity || 0), 0); |
| | | }; |
| | | |
| | | // è®¡ç®æ»éé¢ |
| | | const calculateTotalAmount = () => { |
| | | return formData.returnItems.reduce((total, item) => { |
| | | return total + ((item.quantity || 0) * (item.unitPrice || 0)); |
| | | }, 0); |
| | | }; |
| | | |
| | | // çå¬éè´§ååååï¼èªå¨è®¡ç®æ»æ°éåæ»éé¢ |
| | | watch(() => formData.returnItems, () => { |
| | | formData.returnQuantity = calculateTotalQuantity(); |
| | | formData.returnAmount = calculateTotalAmount(); |
| | | }, { deep: true }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .purchase-return-form { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .return-items-container { |
| | | border: 1px solid #dcdfe6; |
| | | border-radius: 4px; |
| | | padding: 15px; |
| | | } |
| | | |
| | | .return-items-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | margin-bottom: 15px; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-table { |
| | | margin-top: 10px; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :model-value="dialogViewVisible" |
| | | @update:model-value="$emit('update:dialogViewVisible', $event)" |
| | | :title="title" |
| | | width="900px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div class="view-container"> |
| | | <!-- åºæ¬ä¿¡æ¯ --> |
| | | <el-card class="info-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="éè´§åå·"> |
| | | {{ form.returnNo || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºå"> |
| | | {{ form.supplierName || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="åæ®æ¥æ"> |
| | | {{ form.returnDate || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="æä½å"> |
| | | {{ form.operatorName || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="éè´§åå "> |
| | | {{ form.returnReason || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="ç¶æ"> |
| | | <el-tag :type="getStatusType(form.status)"> |
| | | {{ getStatusText(form.status) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="éè´§æ°é"> |
| | | {{ form.returnQuantity || 0 }} å¨ |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="éè´§éé¢"> |
| | | {{ form.returnAmount || 0 }} å
|
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å建æ¶é´" :span="2"> |
| | | {{ form.createTime || '-' }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="夿³¨" :span="2"> |
| | | {{ form.remark || '-' }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- éè´§ååæç» --> |
| | | <el-card class="info-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>éè´§ååæç»</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="form.returnItems || []" border style="width: 100%"> |
| | | <el-table-column label="åºå·" type="index" width="60" /> |
| | | <el-table-column label="åååç§°" prop="coalName" width="150" /> |
| | | <el-table-column label="è§æ ¼åå·" prop="specification" width="150" /> |
| | | <el-table-column label="æ°é" prop="quantity" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.quantity || 0 }} å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="åä»·" prop="unitPrice" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.unitPrice || 0 }} å
/å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å°è®¡" width="120"> |
| | | <template #default="scope"> |
| | | {{ ((scope.row.quantity || 0) * (scope.row.unitPrice || 0)).toFixed(2) }} å
|
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="table-summary"> |
| | | <span class="summary-item"> |
| | | æ»æ°éï¼<strong>{{ getTotalQuantity() }} å¨</strong> |
| | | </span> |
| | | <span class="summary-item"> |
| | | æ»éé¢ï¼<strong>{{ getTotalAmount() }} å
</strong> |
| | | </span> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- å®¡æ¹æµç¨ --> |
| | | <el-card class="info-card" shadow="never"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>å®¡æ¹æµç¨</span> |
| | | </div> |
| | | </template> |
| | | <el-timeline> |
| | | <el-timeline-item |
| | | v-for="(activity, index) in approvalFlow" |
| | | :key="index" |
| | | :timestamp="activity.timestamp" |
| | | :type="activity.type" |
| | | > |
| | | <h4>{{ activity.title }}</h4> |
| | | <p>{{ activity.content }}</p> |
| | | <p v-if="activity.operator">æä½äººï¼{{ activity.operator }}</p> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | </el-card> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="handleClose">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed } from "vue"; |
| | | |
| | | // Props |
| | | const props = defineProps({ |
| | | dialogViewVisible: { |
| | | type: Boolean, |
| | | default: false |
| | | }, |
| | | form: { |
| | | type: Object, |
| | | default: () => ({}) |
| | | }, |
| | | title: { |
| | | type: String, |
| | | default: "éè´§å详æ
" |
| | | } |
| | | }); |
| | | |
| | | // Emits |
| | | const emit = defineEmits(['update:dialogViewVisible']); |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const statusMap = { |
| | | draft: "", |
| | | pending: "warning", |
| | | approved: "success", |
| | | rejected: "danger", |
| | | completed: "info" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | draft: "è稿", |
| | | pending: "å¾
å®¡æ ¸", |
| | | approved: "å·²å®¡æ ¸", |
| | | rejected: "å·²æç»", |
| | | completed: "已宿" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // è®¡ç®æ»æ°é |
| | | const getTotalQuantity = () => { |
| | | if (!props.form.returnItems || props.form.returnItems.length === 0) { |
| | | return 0; |
| | | } |
| | | return props.form.returnItems.reduce((total, item) => total + (item.quantity || 0), 0); |
| | | }; |
| | | |
| | | // è®¡ç®æ»éé¢ |
| | | const getTotalAmount = () => { |
| | | if (!props.form.returnItems || props.form.returnItems.length === 0) { |
| | | return 0; |
| | | } |
| | | return props.form.returnItems.reduce((total, item) => { |
| | | return total + ((item.quantity || 0) * (item.unitPrice || 0)); |
| | | }, 0).toFixed(2); |
| | | }; |
| | | |
| | | // å®¡æ¹æµç¨æ°æ® |
| | | const approvalFlow = computed(() => { |
| | | const flow = []; |
| | | |
| | | // å建 |
| | | flow.push({ |
| | | title: "å建éè´§å", |
| | | content: "éè´§åå·²å建ï¼çå¾
æäº¤å®¡æ ¸", |
| | | timestamp: props.form.createTime || new Date().toLocaleString(), |
| | | operator: props.form.operatorName || "ç³»ç»", |
| | | type: "primary" |
| | | }); |
| | | |
| | | // æ ¹æ®ç¶ææ·»å å®¡æ¹æµç¨ |
| | | if (props.form.status === "pending") { |
| | | flow.push({ |
| | | title: "æäº¤å®¡æ ¸", |
| | | content: "éè´§åå·²æäº¤ï¼çå¾
å®¡æ ¸", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: props.form.operatorName || "ç³»ç»", |
| | | type: "warning" |
| | | }); |
| | | } else if (props.form.status === "approved") { |
| | | flow.push({ |
| | | title: "æäº¤å®¡æ ¸", |
| | | content: "éè´§åå·²æäº¤ï¼çå¾
å®¡æ ¸", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: props.form.operatorName || "ç³»ç»", |
| | | type: "warning" |
| | | }); |
| | | flow.push({ |
| | | title: "å®¡æ ¸éè¿", |
| | | content: "éè´§åå®¡æ ¸éè¿", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: "å®¡æ ¸å", |
| | | type: "success" |
| | | }); |
| | | } else if (props.form.status === "rejected") { |
| | | flow.push({ |
| | | title: "æäº¤å®¡æ ¸", |
| | | content: "éè´§åå·²æäº¤ï¼çå¾
å®¡æ ¸", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: props.form.operatorName || "ç³»ç»", |
| | | type: "warning" |
| | | }); |
| | | flow.push({ |
| | | title: "å®¡æ ¸æç»", |
| | | content: "éè´§åå®¡æ ¸è¢«æç»", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: "å®¡æ ¸å", |
| | | type: "danger" |
| | | }); |
| | | } else if (props.form.status === "completed") { |
| | | flow.push({ |
| | | title: "æäº¤å®¡æ ¸", |
| | | content: "éè´§åå·²æäº¤ï¼çå¾
å®¡æ ¸", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: props.form.operatorName || "ç³»ç»", |
| | | type: "warning" |
| | | }); |
| | | flow.push({ |
| | | title: "å®¡æ ¸éè¿", |
| | | content: "éè´§åå®¡æ ¸éè¿", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: "å®¡æ ¸å", |
| | | type: "success" |
| | | }); |
| | | flow.push({ |
| | | title: "éè´§å®æ", |
| | | content: "éè´§æµç¨å·²å®æ", |
| | | timestamp: new Date().toLocaleString(), |
| | | operator: "ç³»ç»", |
| | | type: "info" |
| | | }); |
| | | } |
| | | |
| | | return flow; |
| | | }); |
| | | |
| | | // å
³éå¯¹è¯æ¡ |
| | | const handleClose = () => { |
| | | emit('update:dialogViewVisible', false); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .view-container { |
| | | padding: 0; |
| | | } |
| | | |
| | | .info-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .info-card:last-child { |
| | | margin-bottom: 0; |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: bold; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .table-summary { |
| | | margin-top: 15px; |
| | | text-align: right; |
| | | padding: 10px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .summary-item { |
| | | margin-left: 20px; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .summary-item strong { |
| | | color: #409eff; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | .el-timeline { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .el-timeline-item h4 { |
| | | margin: 0 0 8px 0; |
| | | font-size: 14px; |
| | | color: #303133; |
| | | } |
| | | |
| | | .el-timeline-item p { |
| | | margin: 4px 0; |
| | | font-size: 12px; |
| | | color: #606266; |
| | | } |
| | | </style> |
¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <el-form :inline="true" :model="queryParams" class="search-form"> |
| | | <el-form-item label="éè´§åå·"> |
| | | <el-input |
| | | v-model="queryParams.returnNo" |
| | | placeholder="请è¾å
¥éè´§åå·" |
| | | clearable |
| | | :style="{ width: '200px' }" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ä¾åºå"> |
| | | <el-select |
| | | v-model="queryParams.supplierId" |
| | | placeholder="è¯·éæ©ä¾åºå" |
| | | clearable |
| | | :style="{ width: '200px' }" |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in supplierList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ"> |
| | | <el-select |
| | | v-model="queryParams.status" |
| | | placeholder="è¯·éæ©ç¶æ" |
| | | clearable |
| | | :style="{ width: '150px' }" |
| | | > |
| | | <el-option |
| | | :label="item.label" |
| | | v-for="item in statusList" |
| | | :key="item.value" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="åæ®æ¥æ"> |
| | | <el-date-picker |
| | | v-model="queryParams.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | :style="{ width: '240px' }" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <el-card> |
| | | <!-- æä½æé®åº --> |
| | | <el-row :gutter="24" class="table-toolbar" justify="space-between"> |
| | | <el-button type="primary" :icon="Plus" @click="handleAdd"> |
| | | æ°å¢éè´§å |
| | | </el-button> |
| | | <el-button type="success" :icon="Refresh" @click="handleGenerateReturn"> |
| | | ä¸é®çæéè´§å |
| | | </el-button> |
| | | <el-button type="danger" :icon="Delete" @click="handleBatchDelete" :disabled="selectedIds.length === 0"> |
| | | æ¹éå é¤ |
| | | </el-button> |
| | | </el-row> |
| | | |
| | | <!-- è¡¨æ ¼ç»ä»¶ --> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="tableData" |
| | | @selection-change="handleSelectionChange" |
| | | border |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column type="selection" width="55" /> |
| | | <el-table-column label="éè´§åå·" prop="returnNo" width="180" /> |
| | | <el-table-column label="ä¾åºå" prop="supplierName" width="200" /> |
| | | <el-table-column label="åæ®æ¥æ" prop="returnDate" width="120" /> |
| | | <el-table-column label="æä½å" prop="operatorName" width="120" /> |
| | | <el-table-column label="éè´§åå " prop="returnReason" width="200" show-overflow-tooltip /> |
| | | <el-table-column label="éè´§æ°é" prop="returnQuantity" width="120"> |
| | | <template #default="scope"> |
| | | {{ scope.row.returnQuantity }} å¨ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="ç¶æ" prop="status" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)"> |
| | | {{ getStatusText(scope.row.status) }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å建æ¶é´" prop="createTime" width="160" /> |
| | | <el-table-column label="æä½" width="200" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | size="small" |
| | | type="primary" |
| | | @click="handleView(scope.row)" |
| | | > |
| | | æ¥ç |
| | | </el-button> |
| | | <el-button |
| | | size="small" |
| | | type="warning" |
| | | @click="handleEdit(scope.row)" |
| | | v-if="scope.row.status === 'draft'" |
| | | > |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button |
| | | size="small" |
| | | type="danger" |
| | | @click="handleDelete(scope.row)" |
| | | v-if="scope.row.status === 'draft'" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- å页ç»ä»¶ --> |
| | | <pagination |
| | | v-if="total > 0" |
| | | :page="current" |
| | | :limit="pageSize" |
| | | :total="total" |
| | | @pagination="handlePagination" |
| | | :layout="'total, prev, pager, next, jumper'" |
| | | /> |
| | | </el-card> |
| | | |
| | | <!-- æ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <PurchaseReturnDialog |
| | | v-model:dialogFormVisible="dialogFormVisible" |
| | | v-model:form="form" |
| | | :title="title" |
| | | :is-edit="isEdit" |
| | | @submit="handleSubmit" |
| | | @success="handleSuccess" |
| | | ref="purchaseReturnDialog" |
| | | /> |
| | | |
| | | <!-- æ¥ç详æ
å¯¹è¯æ¡ --> |
| | | <PurchaseReturnViewDialog |
| | | v-model:dialogViewVisible="dialogViewVisible" |
| | | :form="viewForm" |
| | | title="éè´§å详æ
" |
| | | /> |
| | | |
| | | <!-- ä¸é®çæéè´§åå¯¹è¯æ¡ --> |
| | | <GenerateReturnDialog |
| | | v-model:dialogGenerateVisible="dialogGenerateVisible" |
| | | @success="handleGenerateSuccess" |
| | | /> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, getCurrentInstance } from "vue"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { Plus, Edit, Delete, Refresh, View } from "@element-plus/icons-vue"; |
| | | import Pagination from "@/components/Pagination"; |
| | | import PurchaseReturnDialog from "./components/PurchaseReturnDialog.vue"; |
| | | import PurchaseReturnViewDialog from "./components/PurchaseReturnViewDialog.vue"; |
| | | import GenerateReturnDialog from "./components/GenerateReturnDialog.vue"; |
| | | |
| | | // ååºå¼æ°æ® |
| | | const loading = ref(false); |
| | | const tableData = ref([]); |
| | | const selectedIds = ref([]); |
| | | const current = ref(1); |
| | | const pageSize = ref(10); |
| | | const total = ref(0); |
| | | const dialogFormVisible = ref(false); |
| | | const dialogViewVisible = ref(false); |
| | | const dialogGenerateVisible = ref(false); |
| | | const isEdit = ref(false); |
| | | const title = ref(""); |
| | | const form = ref({}); |
| | | const viewForm = ref({}); |
| | | |
| | | // æ¥è¯¢åæ° |
| | | const queryParams = reactive({ |
| | | returnNo: "", |
| | | supplierId: "", |
| | | status: "", |
| | | dateRange: [] |
| | | }); |
| | | |
| | | // ä¾åºåå表 |
| | | const supplierList = ref([ |
| | | { value: "1", label: "ä¾åºåA" }, |
| | | { value: "2", label: "ä¾åºåB" }, |
| | | { value: "3", label: "ä¾åºåC" } |
| | | ]); |
| | | |
| | | // ç¶æå表 |
| | | const statusList = ref([ |
| | | { value: "draft", label: "è稿" }, |
| | | { value: "pending", label: "å¾
å®¡æ ¸" }, |
| | | { value: "approved", label: "å·²å®¡æ ¸" }, |
| | | { value: "rejected", label: "å·²æç»" }, |
| | | { value: "completed", label: "已宿" } |
| | | ]); |
| | | |
| | | // æ¨¡ææ°æ® |
| | | const mockData = [ |
| | | { |
| | | id: "1", |
| | | returnNo: "TH20241201001", |
| | | supplierName: "ä¾åºåA", |
| | | returnDate: "2024-12-01", |
| | | operatorName: "éå¿å¼º", |
| | | returnReason: "è´¨éä¸åæ ¼ï¼ç
¤è´¨ä¸ç¬¦åè¦æ±", |
| | | returnQuantity: 50, |
| | | status: "pending", |
| | | createTime: "2024-12-01 10:00:00" |
| | | }, |
| | | { |
| | | id: "2", |
| | | returnNo: "TH20241201002", |
| | | supplierName: "ä¾åºåB", |
| | | returnDate: "2024-12-01", |
| | | operatorName: "åç¾ç²", |
| | | returnReason: "交货æ»åï¼å½±åç产计å", |
| | | returnQuantity: 30, |
| | | status: "approved", |
| | | createTime: "2024-12-01 14:30:00" |
| | | } |
| | | ]; |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = (status) => { |
| | | const statusMap = { |
| | | draft: "", |
| | | pending: "warning", |
| | | approved: "success", |
| | | rejected: "danger", |
| | | completed: "info" |
| | | }; |
| | | return statusMap[status] || ""; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusMap = { |
| | | draft: "è稿", |
| | | pending: "å¾
å®¡æ ¸", |
| | | approved: "å·²å®¡æ ¸", |
| | | rejected: "å·²æç»", |
| | | completed: "已宿" |
| | | }; |
| | | return statusMap[status] || status; |
| | | }; |
| | | |
| | | // æ¥è¯¢ |
| | | const handleQuery = () => { |
| | | current.value = 1; |
| | | loadData(); |
| | | }; |
| | | |
| | | // éç½®æ¥è¯¢ |
| | | const resetQuery = () => { |
| | | Object.assign(queryParams, { |
| | | returnNo: "", |
| | | supplierId: "", |
| | | status: "", |
| | | dateRange: [] |
| | | }); |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // å è½½æ°æ® |
| | | const loadData = () => { |
| | | loading.value = true; |
| | | // 模æAPIè°ç¨ |
| | | setTimeout(() => { |
| | | tableData.value = mockData; |
| | | total.value = mockData.length; |
| | | loading.value = false; |
| | | }, 500); |
| | | }; |
| | | |
| | | // å页å¤ç |
| | | const handlePagination = (pagination) => { |
| | | current.value = pagination.page; |
| | | pageSize.value = pagination.limit; |
| | | loadData(); |
| | | }; |
| | | |
| | | // éæ©åå |
| | | const handleSelectionChange = (selection) => { |
| | | selectedIds.value = selection.map(item => item.id); |
| | | }; |
| | | |
| | | // æ°å¢ |
| | | const handleAdd = () => { |
| | | isEdit.value = false; |
| | | title.value = "æ°å¢éè´§å"; |
| | | form.value = { |
| | | supplierId: "", |
| | | returnDate: "", |
| | | operatorId: "", |
| | | returnReason: "", |
| | | returnQuantity: "", |
| | | returnAmount: "", |
| | | returnItems: [], |
| | | remark: "" |
| | | }; |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | |
| | | // ç¼è¾ |
| | | const handleEdit = (row) => { |
| | | isEdit.value = true; |
| | | title.value = "ç¼è¾éè´§å"; |
| | | form.value = { ...row }; |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | |
| | | // æ¥ç |
| | | const handleView = (row) => { |
| | | viewForm.value = { ...row }; |
| | | dialogViewVisible.value = true; |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = (row) => { |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤éè´§å ${row.returnNo} åï¼`, |
| | | "æç¤º", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | // 模æå é¤ |
| | | const index = tableData.value.findIndex(item => item.id === row.id); |
| | | if (index > -1) { |
| | | tableData.value.splice(index, 1); |
| | | total.value--; |
| | | ElMessage.success("å 餿å"); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æ¹éå é¤ |
| | | const handleBatchDelete = () => { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©è¦å é¤çè®°å½"); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤éä¸ç ${selectedIds.value.length} æ¡è®°å½åï¼`, |
| | | "æç¤º", |
| | | { |
| | | confirmButtonText: "ç¡®å®", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | // æ¨¡ææ¹éå é¤ |
| | | tableData.value = tableData.value.filter(item => !selectedIds.value.includes(item.id)); |
| | | total.value = tableData.value.length; |
| | | selectedIds.value = []; |
| | | ElMessage.success("æ¹éå 餿å"); |
| | | }); |
| | | }; |
| | | |
| | | // ä¸é®çæéè´§å |
| | | const handleGenerateReturn = () => { |
| | | dialogGenerateVisible.value = true; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const handleSubmit = (formData) => { |
| | | if (isEdit.value) { |
| | | // ç¼è¾ |
| | | const index = tableData.value.findIndex(item => item.id === formData.id); |
| | | if (index > -1) { |
| | | tableData.value[index] = { ...formData }; |
| | | ElMessage.success("ç¼è¾æå"); |
| | | } |
| | | } else { |
| | | // æ°å¢ |
| | | const newItem = { |
| | | id: Date.now().toString(), |
| | | returnNo: `TH${Date.now()}`, |
| | | supplierName: supplierList.value.find(item => item.value === formData.supplierId)?.label || "", |
| | | returnDate: formData.returnDate, |
| | | operatorName: "å½åç¨æ·", |
| | | returnReason: formData.returnReason, |
| | | returnQuantity: formData.returnQuantity, |
| | | status: "draft", |
| | | createTime: new Date().toLocaleString() |
| | | }; |
| | | tableData.value.unshift(newItem); |
| | | total.value++; |
| | | ElMessage.success("æ°å¢æå"); |
| | | } |
| | | dialogFormVisible.value = false; |
| | | }; |
| | | |
| | | // 表åæååè° |
| | | const handleSuccess = () => { |
| | | loadData(); |
| | | }; |
| | | |
| | | // çæéè´§åæååè° |
| | | const handleGenerateSuccess = (returnOrder) => { |
| | | dialogGenerateVisible.value = false; |
| | | // å°çæçéè´§åæ·»å å°åè¡¨ä¸ |
| | | if (returnOrder) { |
| | | const newItem = { |
| | | id: Date.now().toString(), |
| | | returnNo: returnOrder.returnNo, |
| | | supplierName: returnOrder.supplierName, |
| | | returnDate: returnOrder.returnDate, |
| | | operatorName: returnOrder.operatorName, |
| | | returnReason: returnOrder.returnReason, |
| | | returnQuantity: returnOrder.returnQuantity, |
| | | status: returnOrder.status, |
| | | createTime: returnOrder.createTime, |
| | | returnItems: returnOrder.returnItems |
| | | }; |
| | | tableData.value.unshift(newItem); |
| | | total.value++; |
| | | } |
| | | loadData(); |
| | | ElMessage.success("éè´§åçææå"); |
| | | }; |
| | | |
| | | // 页é¢å è½½ |
| | | onMounted(() => { |
| | | loadData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .search-form { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .table-toolbar { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .el-card { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |
| | |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="éä»¶ä¸ä¼ ï¼" prop="receiptPaymentDate"> |
| | | <SimpleMultiFileUpload |
| | | style="width: 100%" |
| | | v-model:ids="ids" |
| | | v-model:file-list="fileList" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | |
| | | |
| | | <script setup> |
| | | import {ref, reactive, onMounted, nextTick} from "vue"; |
| | | import SimpleMultiFileUpload from "@/components/FileUpload/SimpleMultiFileUpload.vue" |
| | | const { proxy } = getCurrentInstance() |
| | | const { receipt_payment_type } = proxy.useDict("receipt_payment_type"); |
| | | import {Delete, Download, Plus} from "@element-plus/icons-vue"; |
| | |
| | | |
| | | const formDia = ref() |
| | | const activeTab = ref("out"); |
| | | const ids = ref([]) |
| | | const fileList = ref([]) |
| | | // æ ç¾é¡µæ°æ® |
| | | const tabs = reactive([ |
| | | { name: "out", label: "éå®åºåº" }, |
| | |
| | | const submitForm = () => { |
| | | proxy.$refs["formRef"].validate((valid) => { |
| | | if (valid) { |
| | | former.value.attachUpload = ids.value.join(",") |
| | | receiptPaymentSaveOrUpdate(former.value).then((res) => { |
| | | proxy.$modal.msgSuccess("æäº¤æå"); |
| | | closeDia(); |
| | |
| | | align="center" |
| | | /> |
| | | <el-table-column |
| | | prop="code" |
| | | label="ç¼å·" |
| | | width="180" |
| | | /> |
| | | <el-table-column |
| | | prop="supplierName" |
| | | label="ä¾è´§ååç§°" |
| | | width="180" |
| | |
| | | /> |
| | | <el-table-column prop="type" label="ç
¤æç±»å"> |
| | | <template #default="scope"> |
| | | {{scope.row.type === 1 ? 'æå' : 'åæ'}} |
| | | {{scope.row.type == 1 ? 'æå' : 'åæ'}} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="coal" label="ç
¤ç§" sortable /> |