已删除1个文件
已修改10个文件
已添加13个文件
6392 ■■■■■ 文件已修改
package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/personnelManagement/employeeRecord.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/FileUpload/SimpleMultiFileUpload.vue 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 76 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/plugins/download.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/accountReceivableLedger/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/BatchDownloadDialog.vue 704 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/DownloadDialog.vue 580 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/InvoiceDialog.vue 484 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/InvoiceViewDialog.vue 291 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue 362 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/invoiceCollaboration/index.vue 554 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/marketAnalysis/index.vue 1082 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/payable/components/PayableDialog.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/components/filesDia.vue 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/filesDia.vue 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/contractManagement/index.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/production/productionReporting/components/ProductionDialog.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/components/GenerateReturnDialog.vue 528 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/components/PurchaseReturnDialog.vue 453 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/components/PurchaseReturnViewDialog.vue 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/sales/purchaseReturn.vue 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesOutbound/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/warehouseManagement/index.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json
@@ -33,6 +33,7 @@
    "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",
src/api/personnelManagement/employeeRecord.js
@@ -15,4 +15,12 @@
        method: 'get',
        params: query,
    })
}
}
export function save(data){
    return request({
        url: '/staff/staffOnJob/save',
        method: 'post',
        data: data
    })
}
src/components/FileUpload/SimpleMultiFileUpload.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
<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>
src/components/PIMTable/PIMTable.vue
@@ -40,7 +40,7 @@
      :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"
@@ -80,43 +80,22 @@
            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>
        <!-- æŒ‰é’® -->
@@ -351,6 +330,29 @@
    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") {
src/plugins/download.js
@@ -3,6 +3,7 @@
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
@@ -77,3 +78,13 @@
  }
}
export function findFileListByIds(data){
  return request({
    url: "/common/findFileListByIds",
    method: "post",
    data: data
  })
}
src/views/accountReceivableLedger/index.vue
@@ -131,6 +131,13 @@
    width:200
  },
  {
    label: "附件",
    prop: "attachFileList",
    width:220,
    dataType: "multiTagLink"
  }
  ,
  {
    label: "登记日期",
    prop: "createTime",
    width:300
@@ -180,6 +187,7 @@
    tableData.value = res.records;
        page.total = res.total;
  });
  tableLoading.value = false;
};
// å­è¡¨åˆè®¡æ–¹æ³•
const summarizeMainTable1 = (param) => {
src/views/invoiceCollaboration/components/BatchDownloadDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,704 @@
<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>
src/views/invoiceCollaboration/components/DownloadDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,580 @@
<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>
src/views/invoiceCollaboration/components/InvoiceDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,484 @@
<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>
src/views/invoiceCollaboration/components/InvoiceViewDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,291 @@
<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>
src/views/invoiceCollaboration/components/TaxControlSyncDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,362 @@
<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>
src/views/invoiceCollaboration/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,554 @@
<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>
src/views/marketAnalysis/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1082 @@
<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>
src/views/payable/components/PayableDialog.vue
@@ -150,8 +150,8 @@
  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
    }
src/views/personnelManagement/contractManagement/components/filesDia.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,76 @@
<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>
src/views/personnelManagement/contractManagement/filesDia.vue
ÎļþÒÑɾ³ý
src/views/personnelManagement/contractManagement/index.vue
@@ -65,7 +65,7 @@
        </div>
      </template>
    </el-dialog>
    <files-dia ref="filesDia"></files-dia>
    <files-dia ref="filesDia" @close="filesClose"></files-dia>
  </div>
</template>
@@ -78,7 +78,7 @@
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: "",
@@ -250,18 +250,23 @@
  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;
src/views/production/productionReporting/components/ProductionDialog.vue
@@ -30,7 +30,7 @@
      <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">
@@ -85,12 +85,14 @@
  },
});
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({
src/views/sales/components/GenerateReturnDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,528 @@
<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>
src/views/sales/components/PurchaseReturnDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,453 @@
<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>
src/views/sales/components/PurchaseReturnViewDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,324 @@
<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>
src/views/sales/purchaseReturn.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,462 @@
<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>
src/views/salesOutbound/index.vue
@@ -166,6 +166,15 @@
              />
            </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>
@@ -180,6 +189,7 @@
<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";
@@ -199,6 +209,8 @@
const formDia = ref()
const activeTab = ref("out");
const ids = ref([])
const fileList = ref([])
// æ ‡ç­¾é¡µæ•°æ®
const tabs = reactive([
  { name: "out", label: "销售出库" },
@@ -342,6 +354,7 @@
const submitForm = () => {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      former.value.attachUpload = ids.value.join(",")
      receiptPaymentSaveOrUpdate(former.value).then((res) => {
        proxy.$modal.msgSuccess("提交成功");
        closeDia();
src/views/warehouseManagement/index.vue
@@ -77,6 +77,11 @@
            align="center"
          />
          <el-table-column
              prop="code"
              label="编号"
              width="180"
          />
          <el-table-column
            prop="supplierName"
            label="供货商名称"
            width="180"
@@ -84,7 +89,7 @@
          />
          <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 />