feat(costStatistics):
- 新增全局汇总信息,展示筛选结果条数与全部费用合计
- 保存费用后自动刷新数据并同步已勾选行状态
已添加1个文件
已修改4个文件
359 ■■■■■ 文件已修改
src/api/business/insOrderRates.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Table/lims-table.vue 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/orderRates.js 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/CNAS/systemManagement/customerSatisfaction/index.vue 206 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/business/costStatistics/index.vue 127 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/business/insOrderRates.js
@@ -1,6 +1,10 @@
// è´¹ç”¨ç»Ÿè®¡é¡µé¢ç›¸å…³æŽ¥å£
import request from '@/utils/request'
export function selectRatesSummary(query) {
  return request({ url: '/insOrderRates/selectRatesSummary', method: 'get', params: query })
}
// æŸ¥è¯¢è®¢å•列表
export function selectRatesPage(query) {
  return request({
src/components/Table/lims-table.vue
@@ -20,7 +20,7 @@
      @current-change="currentChange"
      @selection-change="handleSelectionChange"
    >
      <el-table-column v-if="isSelection" align="center" type="selection" width="55" />
      <el-table-column v-if="isSelection" align="center" type="selection" width="55" :reserve-selection="reserveSelection" />
      <el-table-column align="center" label="序号" type="index" width="60" :index="indexMethod" />
      <el-table-column
@@ -122,6 +122,10 @@
const props = defineProps({
  isSelection: {
    type: Boolean,
    default: false
  },
  reserveSelection: {
    type: Boolean,
    default: false
  },
@@ -268,7 +272,11 @@
  })
})
defineExpose({ setCurrent })
function clearSelection() {
  multipleTable.value?.clearSelection()
}
defineExpose({ setCurrent, clearSelection })
</script>
<style scoped>
src/utils/orderRates.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,10 @@
import Big from 'big.js'
export function orderAmount(row) {
  return new Big(Number(row.isDiscount) === 1 && row.discountAmount != null && row.discountAmount !== ''
    ? row.discountAmount : (row.totalPrice || 0))
}
export function sumOrderAmounts(rows) {
  return rows.reduce((total, row) => total.plus(orderAmount(row)), new Big(0)).toFixed(2)
}
src/views/CNAS/systemManagement/customerSatisfaction/index.vue
@@ -4,11 +4,8 @@
      <div class="search">
        <div>
          <el-form :model="searchForm" ref="searchForm" size="small" :inline="true">
            <el-form-item label="客户名称" prop="unitName" v-if="tabIndex === '0'">
            <el-form-item label="客户名称" prop="unitName">
              <el-input v-model="searchForm.unitName" clearable size="small"></el-input>
            </el-form-item>
            <el-form-item label="文件名称" prop="fileName" v-if="tabIndex === '1'">
              <el-input v-model="searchForm1.fileName" clearable size="small"></el-input>
            </el-form-item>
            <el-form-item>
              <el-button type="primary" size="small" @click="searchList">查询</el-button>
@@ -17,39 +14,17 @@
          </el-form>
        </div>
        <div>
          <el-button v-if="tabIndex === '0'" size="small" type="primary" @click="openFormDia('add')">新 å¢ž</el-button>
          <el-button v-if="tabIndex === '0'" size="small" type="primary" @click="openExportDialog">导 å‡º</el-button>
          <el-upload v-if="tabIndex === '1'" ref='upload' :action="action" :before-upload="beforeUpload"
                     :headers="uploadHeader" :on-error="onError" :on-success="handleSuccessUp" :show-file-list="false"
                     accept='.jpg,.jpeg,.png,.gif,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.pdf,.zip,.rar'>
            <el-button :loading="upLoading" size="small" type="primary">导入</el-button>
          </el-upload>
          <el-button size="small" type="primary" @click="openFormDia('add')">新 å¢ž</el-button>
          <el-button size="small" type="primary" @click="openExportDialog">导 å‡º</el-button>
        </div>
      </div>
      <div class="table">
        <div class="table-tab">
          <el-radio-group v-model="tabIndex" @change="searchList" size="small">
            <el-radio-button value="0">客户满意度</el-radio-button>
            <el-radio-button value="1">综合分析</el-radio-button>
          </el-radio-group>
        </div>
        <div v-if="tabIndex === '0'">
          <limsTable :column="tableColumn" :height="'calc(100vh - 22em)'" :table-data="tableData"
                     :table-loading="tableLoading" @pagination="pagination"
                     :page="page">
          </limsTable>
        </div>
        <div v-if="tabIndex === '1'">
          <limsTable :column="tableColumn1" :height="'calc(100vh - 22em)'" :table-data="tableData1"
                     :table-loading="tableLoading1" @pagination="pagination0">
          </limsTable>
        </div>
        <limsTable :column="tableColumn" :height="'calc(100vh - 22em)'" :table-data="tableData"
                   :table-loading="tableLoading" @pagination="pagination"
                   :page="page">
        </limsTable>
      </div>
    </div>
    <el-dialog v-model="lookDialogVisible" fullscreen title="查看附件" top="5vh" width="800px">
      <filePreview v-if="lookDialogVisible" :currentFile="{}" :fileUrl="javaApi + '/word/' + currentInfo.fileUrl"
        style="height: 90vh;overflow-y: auto;" />
    </el-dialog>
    <el-dialog v-model="exportDialogVisible" title="导出年度汇总" width="360px" :close-on-click-modal="false">
      <el-form label-width="80px">
        <el-form-item label="汇总年度">
@@ -72,30 +47,23 @@
<script>
import limsTable from "@/components/Table/lims-table.vue";
import FormDialog from './components/formDialog.vue';
import filePreview from '@/components/Preview/filePreview.vue'
import limsAttachment from '@/components/Attachment/lims-attachment.vue'
import {
  pageClientSatisfaction,
  pageAnalyseFile,
  delClientSatisfaction,
  delAnalyseFile,
  exportAnnualSummary,
} from '@/api/cnas/systemManagement/customerSatisfaction.js'
export default {
  name: 'CustomerSatisfaction',
  // import å¼•入的组件需要注入到对象中才能使用
  components: { filePreview, FormDialog, limsTable, limsAttachment },
  components: { FormDialog, limsTable, limsAttachment },
  data() {
    // è¿™é‡Œå­˜æ”¾æ•°æ®
    return {
      searchForm: {
        unitName: '',
      },
      searchForm1: {
        fileName: '',
      },
      tabIndex: '0',
      tableColumn: [
        {
          label: '客户名称',
@@ -192,62 +160,7 @@
        current: 1,
        total: 0,
      },
      tableColumn1: [
        {
          label: '附件名称',
          prop: 'fileName',
          minWidth: '100'
        },
        {
          label: '创建人',
          prop: 'userName',
          minWidth: '100'
        },
        {
          label: '创建时间',
          prop: 'createTime',
          minWidth: '100'
        },
        {
          dataType: 'action',
          label: '操作',
          operation: [
            {
              name: '预览',
              type: 'text',
              clickFun: (row) => {
                this.handleLook(row)
              }
            },
            {
              name: '下载',
              type: 'text',
              clickFun: (row) => {
                this.upload(row);
              },
            },
            {
              name: '删除',
              type: 'text',
              color: 'var(--el-color-danger)',
              clickFun: (row) => {
                this.delFile(row)
              }
            }
          ]
        }
      ],
      tableData1: [],
      tableLoading1: false,
      page1: {
        size: 20,
        current: 1,
        total: 0,
      },
      formDialog: false,
      upLoading: false,
      currentInfo: {},
      lookDialogVisible: false,
      exportDialogVisible: false,
      exportYear: String(new Date().getFullYear()),
      exportLoading: false,
@@ -260,29 +173,15 @@
  methods: {
    // æŸ¥è¯¢åˆ—表
    searchList() {
      const entity = this.tabIndex === '0' ? this.searchForm : this.searchForm1
      const page = this.tabIndex === '0' ? this.page : this.page1
      if (this.tabIndex === '0') {
        this.tableLoading = true
        pageClientSatisfaction({ ...entity, ...page }).then(res => {
          this.tableLoading = false
          this.tableData = res.data.records
          this.page.total = res.data.total
        }).catch(err => {
          console.log('err---', err);
          this.tableLoading = false
        })
      } else {
        this.tableLoading1 = true
        pageAnalyseFile({ ...entity, ...page }).then(res => {
          this.tableLoading1 = false
          this.tableData1 = res.data.records
          this.page1.total = res.data.total
        }).catch(err => {
          console.log('err---', err);
          this.tableLoading1 = false
        })
      }
      this.tableLoading = true
      pageClientSatisfaction({ ...this.searchForm, ...this.page }).then(res => {
        this.tableLoading = false
        this.tableData = res.data.records
        this.page.total = res.data.total
      }).catch(err => {
        console.log('err---', err);
        this.tableLoading = false
      })
    },
    openFormDia(type, row) {
      this.formDialog = true
@@ -333,30 +232,7 @@
    // é‡ç½®æŸ¥è¯¢æ¡ä»¶
    resetSearchForm() {
      this.searchForm.unitName = '';
      this.searchForm1.fileName = '';
      this.searchList()
    },
    // å¯¼å…¥æµç¨‹
    beforeUpload(file) {
      if (file.size > 1024 * 1024 * 10) {
        this.$message.error('上传文件不超过10M');
        this.$refs.upload.clearFiles()
        return false;
      } else {
        this.upLoading = true;
        return true;
      }
    },
    onError(err, file, fileList) {
      this.$message.error('上传失败')
      this.$refs.upload.clearFiles()
    },
    handleSuccessUp(response) {
      this.upLoading = false;
      if (response.code == 200) {
        this.$message.success('上传成功');
        this.searchList()
      }
    },
    // åˆ é™¤å®¢æˆ·æ»¡æ„åº¦
    delPlan(row) {
@@ -381,56 +257,12 @@
        });
      });
    },
    // æŸ¥çœ‹æ–‡ä»¶
    handleLook(row) {
      this.currentInfo = row
      this.lookDialogVisible = true
    },
    // ä¸‹è½½å®¢æˆ·ç¦å»º
    upload(row) {
      this.$download.saveAs(row.fileUrl, row.fileName);
    },
    // åˆ é™¤å®¢æˆ·åˆ†æžé™„ä»¶
    delFile(row) {
      this.$confirm('此操作将永久删除该数据, æ˜¯å¦ç»§ç»­?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.tableLoading = true
        delAnalyseFile({ analyseFileId: row.analyseFileId }).then(res => {
          this.tableLoading = false
          this.$message.success('删除成功')
          this.searchList()
        }).catch(err => {
          this.tableLoading = false
          console.log('err---', err);
        })
      }).catch(() => {
        this.$message({
          type: 'info',
          message: '已取消删除'
        });
      });
    },
    // åˆ†é¡µ
    pagination({ page, limit }) {
      this.page.current = page;
      this.page.size = limit;
      this.searchList();
    },
    // åˆ†é¡µ
    pagination0({ page, limit }) {
      this.page.current = page;
      this.page.size = limit;
      this.searchList();
    },
  },
  // ç”¨äºŽä¸Šä¼ æ–‡ä»¶çš„信息
  computed: {
    action() {
      return this.javaApi + '/clientSatisfaction/uploadAnalyseFile'
    }
  },
};
</script>
@@ -440,9 +272,5 @@
  height: 46px;
  display: flex;
  justify-content: space-between;
}
.table-tab {
  margin: 0 0 20px 0;
}
</style>
src/views/business/costStatistics/index.vue
@@ -1,6 +1,6 @@
<template>
<!--  è´¹ç”¨ç»Ÿè®¡é¡µé¢-->
  <div class="capacity-scope">
  <div class="capacity-scope" v-loading="feeSaving">
    <div class="search">
      <div>
        <el-form :model="entity" ref="entity" size="small" :inline="true">
@@ -34,9 +34,10 @@
      </div>
    </div>
    <div>
      <lims-table :tableData="tableData" :column="column"
      <lims-table ref="ratesTable" :tableData="tableData" :column="column"
                  :height="'calc(100vh - 300px)'" @pagination="pagination"
                  :isSelection="true" :handleSelectionChange="handleSelectionChange"
                  rowKey="id" :reserveSelection="true"
                  :page="page" :tableLoading="tableLoading">
        <template #isDiscount="{ row }">
          <el-select v-model="row.isDiscount" size="small" style="width: 80px"
@@ -57,9 +58,14 @@
        </template>
      </lims-table>
    </div>
    <div class="sum-bar">
    <div class="sum-bar" v-loading="tableLoading">
      <span class="all-summary">筛选结果 <b class="sum-count">{{ summary ? summary.count : '—' }}</b> æ¡ï¼Œ
        å…¨éƒ¨è´¹ç”¨ï¼š<b class="sum-amount">Â¥{{ summary ? formatAmount(summary.totalAmount) : '—' }}</b>
        <small>(包含所有分页)</small>
      </span>
      <span>已勾选 <b class="sum-count">{{ selectedRows.length }}</b> æ¡</span>
      <span>总金额:<b class="sum-amount">Â¥{{ selectedTotal }}</b></span>
      <span>勾选合计:<b class="sum-amount">Â¥{{ selectedTotal }}</b></span>
      <el-button link type="primary" :disabled="!selectedRows.length" @click="clearSelection">清空勾选</el-button>
    </div>
    <viewInfoDia ref="viewInfoDia" v-if="viewInfoDia"></viewInfoDia>
  </div>
@@ -68,7 +74,9 @@
<script>
import limsTable from "@/components/Table/lims-table.vue";
import viewInfoDia from "./components/viewInfoDia.vue";
import {selectRatesPage, updateOrderFee} from "@/api/business/insOrderRates";
import {selectRatesPage, selectRatesSummary, updateOrderFee} from "@/api/business/insOrderRates";
import {sumOrderAmounts} from "@/utils/orderRates";
import Big from 'big.js';
import {selectCustomPageList} from "@/api/system/customer";
import { ElMessage } from "element-plus";
@@ -77,10 +85,16 @@
  data() {
    return {
      entity: {
        entrustCode: null,
        sample: null,
        company: null,
      },
      monthRange: [],
      appliedFilters: {},
      summary: null,
      savedRows: {},
      requestVersion: 0,
      feeSaving: false,
      tableData: [],
      tableLoading: false,
      column: [
@@ -165,12 +179,7 @@
  computed: {
    // å‹¾é€‰åˆè®¡ï¼šæœ‰ä¼˜æƒ ä¸”填了优惠后金额的按优惠后金额算,否则按总价算
    selectedTotal() {
      const total = this.selectedRows.reduce((sum, row) => {
        const useDiscount = row.isDiscount === 1 && row.discountAmount !== null && row.discountAmount !== undefined
        const amount = Number(useDiscount ? row.discountAmount : row.totalPrice)
        return sum + (isFinite(amount) ? amount : 0)
      }, 0)
      return total.toFixed(2)
      return sumOrderAmounts(this.selectedRows)
    }
  },
  mounted() {
@@ -179,19 +188,45 @@
  },
  methods :{
    refreshTable() {
      this.tableLoading = true
      const [startMonth, endMonth] = this.monthRange || []
      selectRatesPage({ ...this.page, ...this.entity, startMonth, endMonth }).then(res => {
        this.tableLoading = false
        this.tableData = (res.data.records || []).map(item => {
          item.isDiscount = item.isDiscount === 1 ? 1 : 0
          return item
        })
      this.appliedFilters = { ...this.entity, startMonth, endMonth }
      this.page.current = 1
      this.clearSelection()
      this.savedRows = {}
      this.tableData = []
      return this.loadTable()
    },
    async loadTable() {
      const version = ++this.requestVersion
      this.tableLoading = true
      this.summary = null
      try {
        const [res, summary] = await Promise.all([
          selectRatesPage({ current: this.page.current, size: this.page.size, ...this.appliedFilters }),
          selectRatesSummary(this.appliedFilters),
        ])
        if (version !== this.requestVersion) return
        this.tableData = (res.data.records || []).map(row => ({ ...row, isDiscount: Number(row.isDiscount || 0) }))
        // ä¿ç•™æœåŠ¡ç«¯å¿«ç…§ï¼Œè¾“å…¥æ¡†ä¸­å°šæœªä¿å­˜çš„é‡‘é¢ä¸è¿›å…¥åˆè®¡ã€‚
        this.tableData.forEach(row => { this.savedRows[row.id] = { ...row } })
        this.selectedRows = this.selectedRows.map(row => this.savedRows[row.id] || row)
        this.page.total = res.data.total
        this.selectedRows = []
      }).catch(err => {
        this.tableLoading = false
      })
        this.summary = summary.data
      } catch (err) {
        if (version !== this.requestVersion) return
        this.tableData = []
        this.page.total = 0
        this.clearSelection()
      } finally {
        if (version === this.requestVersion) this.tableLoading = false
      }
    },
    clearSelection() {
      this.$refs.ratesTable?.clearSelection()
      this.selectedRows = []
    },
    formatAmount(value) {
      return new Big(value || 0).toFixed(2)
    },
    // é‡ç½®
    refresh() {
@@ -218,12 +253,13 @@
    },
    // åˆ†é¡µåˆ‡æ¢
    pagination(page) {
      this.page.current = page.page
      this.page.size = page.limit
      this.refreshTable()
      return this.loadTable()
    },
    // å‹¾é€‰è¡Œå˜åŒ–
    handleSelectionChange(selection) {
      this.selectedRows = selection
      this.selectedRows = selection.map(row => this.savedRows[row.id] || { ...row })
    },
    // åˆ‡æ¢æ˜¯å¦ä¼˜æƒ 
    handleDiscountChange(row) {
@@ -235,7 +271,7 @@
      } else {
        row.discountAmount = null
      }
      this.saveFee(row)
      return this.saveFee(row)
    },
    // ä¼˜æƒ åŽé‡‘额改完保存
    handleAmountChange(row) {
@@ -247,26 +283,39 @@
        ElMessage.warning('有优惠时请填写优惠后金额')
        return
      }
      this.saveFee(row)
      return this.saveFee(row)
    },
    // å¤‡æ³¨æ”¹å®Œä¿å­˜
    handleFeeRemarkChange(row) {
      this.saveFee(row)
      return this.saveFee(row)
    },
    amountOf(value) {
      const num = Number(value)
      if (!isFinite(num)) return 0
      return Math.round(num * 100) / 100
    },
    saveFee(row) {
      updateOrderFee({
        id: row.id,
        isDiscount: row.isDiscount,
        discountAmount: row.isDiscount === 1 ? row.discountAmount : null,
        feeRemark: row.feeRemark || null
      }).catch(() => {
        this.refreshTable()
      })
    async saveFee(row) {
      if (this.feeSaving) return
      if (row.isDiscount === 1 && (row.discountAmount == null || row.discountAmount === '')) {
        ElMessage.warning('有优惠时请填写优惠后金额')
        return
      }
      this.feeSaving = true
      // ä½¿ä¿®æ”¹å‰ä»åœ¨é€”的查询失效,避免旧金额覆盖保存后的状态。
      ++this.requestVersion
      try {
        await updateOrderFee({
          id: row.id,
          isDiscount: row.isDiscount,
          discountAmount: row.isDiscount === 1 ? row.discountAmount : null,
          feeRemark: row.feeRemark || null
        })
      } catch (err) {
        // è¯·æ±‚拦截器提示失败原因;下面重新读取实际保存的金额。
      } finally {
        await this.loadTable()
        this.feeSaving = false
      }
    },
    // æŸ¥çœ‹è¯¦æƒ…
    openDia (row) {
@@ -282,6 +331,8 @@
.search {
  min-height: 46px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: space-between;
}
.sum-bar {
@@ -289,6 +340,8 @@
  bottom: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 24px;
  margin-top: 8px;
@@ -300,6 +353,8 @@
.sum-count {
  color: var(--el-color-primary);
}
.all-summary { margin-right: auto; }
.all-summary small { color: var(--el-text-color-secondary); }
.sum-amount {
  color: var(--el-color-danger);
  font-size: 16px;