hsy
8 小时以前 2d3530a7c11557a40807ad7c0b9e302ce6ce3a9f
feat: 综合业务模块页面重构与交互完善

- feat(sales): 完善招投标、诉讼管理、发货台账及收付款台账的前端页面支持
- feat(project): 新增项目配置页面,补充项目归档与合同列表弹窗交互
- fix(equipment): 优化设备台账、校准、维修(新增验收/维修弹窗)及保养计划的表单交互
- fix(office): 优化审批列表及费用报销的详情面板(DetailPanel)与相关工具函数
- fix(quality): 调整原材、过程、成品检验的表单视图(formDia)及指标维护页面
- fix(inventory): 优化车辆管理、车辆加油及发料管理的页面展示逻辑
已添加7个文件
已修改42个文件
5744 ■■■■ 文件已修改
.gitignore 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/device/statusChangeRecord.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/inventoryManagement/vehicle.js 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/config.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/projectManagement/project.js 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/qualityManagement/rawMaterialInspection.js 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/salesManagement/indicatorStats.js 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/system/user.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/PIMTable/PIMTable.vue 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/calibration/index.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/Form.vue 219 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/ledger/index.vue 228 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/operationManagement/index.vue 22 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue 19 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/MaintainModal.vue 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/Modal/RepairModal.vue 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/repair/index.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/PlanModal.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/Form/formDia.vue 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/upkeep/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/fuelCardManagement/index.vue 430 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/issueManagement/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleFuelManagement/index.vue 757 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleManagement/index.vue 883 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue 68 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js 139 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js 66 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/ArchiveDialog.vue 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/components/ContractListDialog.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/Management/index.vue 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/projectManagement/config/index.vue 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/components/formDia.vue 172 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/finalInspection/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/metricMaintenance/ParamFormDialog.vue 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/metricMaintenance/index.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/processInspection/components/formDia.vue 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/biddingManagement/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/deliveryLedger/index.vue 203 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/lawsuitManagement/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/receiptPaymentLedger/index.vue 788 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesLedger/index.vue 435 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/salesManagement/salesQuotation/index.vue 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -26,3 +26,6 @@
package-lock.json
yarn.lock
/graphify-out
build_graph.py
src/api/device/statusChangeRecord.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
import request from '@/utils/request'
export function listStatusChangeRecord(query) {
  return request({
    url: '/device/statusChangeRecord/list',
    method: 'get',
    params: query
  })
}
src/api/inventoryManagement/vehicle.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,144 @@
import request from '@/utils/request'
// è½¦è¾†ä¿¡æ¯
export function listVehicleInfo(query) {
  return request({
    url: '/vehicle/info/listPage',
    method: 'get',
    params: query
  })
}
export function getWarningList() {
  return request({
    url: '/vehicle/info/warningList',
    method: 'get'
  })
}
export function addVehicleInfo(data) {
  return request({
    url: '/vehicle/info',
    method: 'post',
    data: data
  })
}
export function updateVehicleInfo(data) {
  return request({
    url: '/vehicle/info',
    method: 'put',
    data: data
  })
}
export function delVehicleInfo(id) {
  return request({
    url: '/vehicle/info/' + id,
    method: 'delete'
  })
}
// åŠ æ²¹å¡
export function listFuelCardPage(query) {
  return request({
    url: '/vehicle/fuelCard/listPage',
    method: 'get',
    params: query
  })
}
export function listAllFuelCard() {
  return request({
    url: '/vehicle/fuelCard/listAll',
    method: 'get'
  })
}
export function addFuelCard(data) {
  return request({
    url: '/vehicle/fuelCard',
    method: 'post',
    data: data
  })
}
export function updateFuelCard(data) {
  return request({
    url: '/vehicle/fuelCard',
    method: 'put',
    data: data
  })
}
export function delFuelCard(id) {
  return request({
    url: '/vehicle/fuelCard/' + id,
    method: 'delete'
  })
}
// åŠ æ²¹è®°å½•
export function listFuelRecord(query) {
  return request({
    url: '/vehicle/fuelRecord/listPage',
    method: 'get',
    params: query
  })
}
export function addFuelRecord(data) {
  return request({
    url: '/vehicle/fuelRecord',
    method: 'post',
    data: data
  })
}
export function updateFuelRecord(data) {
  return request({
    url: '/vehicle/fuelRecord',
    method: 'put',
    data: data
  })
}
export function delFuelRecord(id) {
  return request({
    url: '/vehicle/fuelRecord/' + id,
    method: 'delete'
  })
}
// åˆ†é¡µæŸ¥è¯¢åŠ æ²¹å¡å……å€¼è®°å½•
export function listRecharge(query) {
  return request({
    url: '/vehicle/fuelCardRecharge/list',
    method: 'get',
    params: query
  })
}
// æ–°å¢žåŠ æ²¹å¡å……å€¼
export function addRecharge(data) {
  return request({
    url: '/vehicle/fuelCardRecharge/add',
    method: 'post',
    data: data
  })
}
// åˆ é™¤åŠ æ²¹å¡å……å€¼
export function delRecharge(id) {
  return request({
    url: '/vehicle/fuelCardRecharge/' + id,
    method: 'delete'
  })
}
// æäº¤åŠ æ²¹è®°å½•
export function submitFuelRecord(id) {
  return request({
    url: '/vehicle/fuelRecord/submit/' + id,
    method: 'post'
  })
}
// æ’¤å›žåŠ æ²¹è®°å½•
export function revokeFuelRecord(id) {
  return request({
    url: '/vehicle/fuelRecord/revoke/' + id,
    method: 'post'
  })
}
src/api/projectManagement/config.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
import request from '@/utils/request'
// æŸ¥è¯¢é¡¹ç›®ç®¡ç†å…¨å±€é…ç½®
export function getConfig() {
  return request({
    url: '/projectManagement/config',
    method: 'get'
  })
}
// æ›´æ–°é¡¹ç›®ç®¡ç†å…¨å±€é…ç½®
export function updateConfig(data) {
  return request({
    url: '/projectManagement/config',
    method: 'put',
    data: data
  })
}
src/api/projectManagement/project.js
@@ -116,3 +116,11 @@
    method: 'post'
  })
}
// èŽ·å–é¡¹ç›®å½’æ¡£é™„ä»¶
export function getProjectAttachments(id) {
  return request({
    url: `/projectManagement/info/attachments/${id}`,
    method: 'get'
  })
}
src/api/qualityManagement/rawMaterialInspection.js
@@ -45,7 +45,7 @@
    })
}
// æäº¤åŽŸææ–™æ£€éªŒ
// ä¸‹è½½åŽŸææ–™æ£€éªŒWord
export function downloadQualityInspect(data) {
    return request({
        url: '/quality/qualityInspect/down',
@@ -55,3 +55,13 @@
    })
}
// ä¸‹è½½åŽŸææ–™æ£€éªŒPDF
export function downloadQualityInspectPdf(data) {
    return request({
        url: '/quality/qualityInspect/downPdf',
        method: 'post',
        data: data,
        responseType: "blob",
    })
}
src/api/salesManagement/indicatorStats.js
@@ -19,6 +19,15 @@
  });
}
// é¡¹ç›®å¾€æ¥æ±‡æ€»ï¼ˆé¡¶å±‚)
export function projectTransactions(query) {
  return request({
    url: "/metricStatistics/projectTransactions",
    method: "get",
    params: query,
  });
}
// å®¢æˆ·å¾€æ¥åˆ—表
export function customewTransactions(query) {
  return request({
@@ -36,3 +45,11 @@
    params: query,
  });
}
// å¤§ç›˜æ€»è§ˆ
export function transactionsSummary() {
  return request({
    url: "/metricStatistics/transactionsSummary",
    method: "get"
  });
}
src/api/system/user.js
@@ -135,10 +135,11 @@
  })
}
// æŸ¥è¯¢ç”¨æˆ·åˆ—表
export function userListNoPageByTenantId() {
export function userListNoPageByTenantId(params) {
  return request({
    url: '/system/user/userListNoPageByTenantId',
    method: 'get'
    method: 'get',
    params
  })
}
src/components/PIMTable/PIMTable.vue
@@ -117,7 +117,33 @@
             @click.stop>
          <template v-for="(o, key) in item.operation"
                    :key="key">
            <el-button v-show="o.type != 'upload'"
            <el-dropdown v-if="o.type == 'dropdown'"
                         v-show="o.showHide ? o.showHide(scope.row) : true"
                         trigger="click"
                         @command="(command) => o.commandFun(command, scope.row)"
                         style="margin-left: 12px; margin-right: 12px; vertical-align: middle;">
              <el-button :disabled="isOperationDisabled(o, scope.row)"
                         :plain="o.plain"
                         type="primary"
                         :style="{
                  color: getOperationColor(o, scope.row),
                  fontWeight: 'bold',
                }"
                         link>
                {{ o.name }}<el-icon class="el-icon--right"><ArrowDown /></el-icon>
              </el-button>
              <template #dropdown>
                <el-dropdown-menu>
                  <el-dropdown-item v-for="(dItem, dIdx) in (typeof o.dropdownItems === 'function' ? o.dropdownItems(scope.row) : o.dropdownItems)"
                                    :key="dIdx"
                                    :command="dItem.command"
                                    :disabled="dItem.disabled ? dItem.disabled(scope.row) : false">
                    {{ dItem.label }}
                  </el-dropdown-item>
                </el-dropdown-menu>
              </template>
            </el-dropdown>
            <el-button v-show="o.type != 'upload' && o.type != 'dropdown'"
                       v-if="o.showHide ? o.showHide(scope.row) : true"
                       :disabled="isOperationDisabled(o, scope.row)"
                       :plain="o.plain"
@@ -204,6 +230,7 @@
  import pagination from "./Pagination.vue";
  import { computed, ref, inject, getCurrentInstance } from "vue";
  import { ElMessage } from "element-plus";
  import { ArrowDown } from "@element-plus/icons-vue";
  // èŽ·å–å…¨å±€çš„ uploadHeader
  const { proxy } = getCurrentInstance();
src/views/equipmentManagement/calibration/index.vue
@@ -50,7 +50,13 @@
                @selection-change="handleSelectionChange"
                :tableLoading="tableLoading"
                @pagination="pagination"
            ></PIMTable>
            >
                <template #validUntilSlot="{ row }">
                    <span>{{ row.validUntil ? row.validUntil.split(' ')[0] : '' }}</span>
                    <el-tag v-if="isWarning(row.validUntil)" type="warning" class="ml10">即将到期</el-tag>
                    <el-tag v-else-if="isOverdue(row.validUntil)" type="danger" class="ml10">已逾期</el-tag>
                </template>
            </PIMTable>
        </div>
        <calibration-dia ref="calibrationDia" @close="handleQuery"></calibration-dia>
    </div>
@@ -59,6 +65,7 @@
<script setup>
import {onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick} from "vue";
import {ElMessageBox, ElMessage} from "element-plus";
import dayjs from "dayjs";
import useUserStore from "@/store/modules/user.js";
import CalibrationDia from "@/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue";
import {ledgerRecordListPage, ledgerRecordDelete} from "@/api/equipmentManagement/calibration.js";
@@ -119,9 +126,46 @@
        width:200
    },
    {
        label: "有效期",
        label: "有效期(天)",
        prop: "valid",
        width: 100,
    },
    {
        label: "有效期至",
        prop: "validUntil",
        dataType: "slot",
        slot: "validUntilSlot",
        width: 160,
    },
    {
        label: "溯源/核查类型",
        prop: "traceabilityType",
        width: 150,
    },
    {
        label: "实施机构",
        prop: "implementingAgency",
        width: 150,
    },
    {
        label: "证书编号",
        prop: "certificateNumber",
        width: 150,
    },
    {
        label: "核心结果",
        prop: "coreResult",
        width: 150,
    },
    {
        label: "结果确认",
        prop: "resultConfirmation",
        width: 150,
    },
    {
        label: "确认人",
        prop: "confirmerName",
        width: 120,
    },
    {
        label: "录入人",
@@ -197,12 +241,27 @@
const getList = () => {
    tableLoading.value = true;
    ledgerRecordListPage({ ...searchForm.value, ...page }).then((res) => {
        tableLoading.value = false;
        tableData.value = res.data.records;
        page.total = res.data.total;
        tableLoading.value = false;
    }).catch((err) => {
        tableLoading.value = false;
    })
    });
};
const isWarning = (validUntil) => {
    if (!validUntil) return false;
    const now = dayjs().startOf('day');
    const until = dayjs(validUntil).startOf('day');
    const diffDays = until.diff(now, 'day');
    return diffDays >= 0 && diffDays <= 30;
};
const isOverdue = (validUntil) => {
    if (!validUntil) return false;
    const now = dayjs().startOf('day');
    const until = dayjs(validUntil).startOf('day');
    return until.isBefore(now);
};
// æ‰“开检定校准弹框
src/views/equipmentManagement/ledger/Form.vue
@@ -1,6 +1,24 @@
<template>
  <el-form :model="form" label-width="120px" :rules="formRules" ref="formRef">
    <el-divider content-position="left">基础信息</el-divider>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="设备编号" prop="uniqueCode">
          <div style="display: flex; gap: 10px; width: 100%;">
            <el-checkbox
              v-model="form.autoGenerateUniqueCode"
              v-if="operationType === 'add'"
              >自动生成
            </el-checkbox>
            <el-input
              v-model="form.uniqueCode"
              :placeholder="form.autoGenerateUniqueCode ? '保存后自动生成' : '请输入设备编号'"
              :disabled="form.autoGenerateUniqueCode || operationType === 'view'"
              clearable
            />
          </div>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="设备名称" prop="deviceName">
          <el-input v-model="form.deviceName" placeholder="请输入设备名称" />
@@ -38,8 +56,8 @@
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="供应商" prop="supplierName">
          <el-input v-model="form.supplierName" placeholder="请输入供应商" />
        <el-form-item label="出厂编号" prop="factoryNumber">
          <el-input v-model="form.factoryNumber" placeholder="请输入出厂编号" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -48,24 +66,61 @@
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="单位" prop="unit">
          <el-input v-model="form.unit" placeholder="请输入单位" />
        <el-form-item label="责任人" prop="responsiblePerson">
          <el-input v-model="form.responsiblePerson" placeholder="请输入责任人" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="启用折旧" prop="isDepr">
          <el-switch v-model="form.isDepr" :active-value="1" :inactive-value="2" />
        <el-form-item label="建档日期" prop="filingDate">
          <el-date-picker
            style="width: 100%"
            v-model="form.filingDate"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD"
            type="date"
            placeholder="请选择建档日期"
            clearable
          />
        </el-form-item>
      </el-col>
      <el-col :span="12" v-if="form.isDepr === 1">
        <el-form-item label="每年折旧金额" prop="annualDepreciationAmount">
          <el-input-number
            :step="0.01"
            :min="0"
    </el-row>
    <el-divider content-position="left">采购信息</el-divider>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="供应商" prop="supplierName">
          <el-input v-model="form.supplierName" placeholder="请输入供应商" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="制造厂商" prop="manufacturer">
          <el-input v-model="form.manufacturer" placeholder="请输入制造厂商" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="购置日期" prop="purchaseDate">
          <el-date-picker
            style="width: 100%"
            v-model="form.annualDepreciationAmount"
            placeholder="请输入每年折旧金额"
            v-model="form.purchaseDate"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD"
            type="date"
            placeholder="请选择购置日期"
            clearable
          />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="资产原值" prop="originalAssetValue">
          <el-input-number :step="0.01" :min="0" style="width: 100%"
            v-model="form.originalAssetValue"
            placeholder="请输入资产原值"
          />
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="单位" prop="unit">
          <el-input v-model="form.unit" placeholder="请输入单位" />
        </el-form-item>
      </el-col>
      <el-col :span="12">
@@ -125,20 +180,46 @@
          />
        </el-form-item>
      </el-col>
      <!-- <el-col :span="12">
        <el-form-item label="录入人" prop="createUser">
          <el-input v-model="form.createUser" placeholder="请输入录入人" />
    </el-row>
    <el-divider content-position="left">技术参数</el-divider>
    <el-row :gutter="20">
      <el-col :span="24">
        <el-form-item label="主要技术参数" prop="technicalParameters">
          <el-input type="textarea" :rows="3" v-model="form.technicalParameters" placeholder="请输入主要技术参数" />
        </el-form-item>
      </el-col> -->
      </el-col>
      <el-col :span="24">
        <el-form-item label="主要用途" prop="mainPurpose">
          <el-input type="textarea" :rows="3" v-model="form.mainPurpose" placeholder="请输入主要用途" />
        </el-form-item>
      </el-col>
    </el-row>
    <el-divider content-position="left">运行管理</el-divider>
    <el-row :gutter="20">
      <el-col :span="12">
        <el-form-item label="录入日期" prop="createTime">
        <el-form-item label="设备状态" prop="status">
          <el-select
            v-model="form.status"
            placeholder="请选择"
            :disabled="operationType === 'edit'"
            style="width: 100%"
          >
            <el-option label="停用" :value="0" />
            <el-option label="启用" :value="1" />
          </el-select>
        </el-form-item>
      </el-col>
      <el-col :span="12">
        <el-form-item label="启用日期" prop="activationDate">
          <el-date-picker
            style="width: 100%"
            v-model="form.createTime"
            v-model="form.activationDate"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD HH:mm:ss"
            value-format="YYYY-MM-DD"
            type="date"
            placeholder="请选择录入日期"
            placeholder="请选择启用日期"
            clearable
          />
        </el-form-item>
@@ -151,11 +232,28 @@
                        format="YYYY-MM-DD"
                        value-format="YYYY-MM-DD"
                        type="date"
                        placeholder="请选择录入日期"
            placeholder="请选择预计运行时间"
                        clearable
                    />
                </el-form-item>
            </el-col>
      <el-col :span="12">
        <el-form-item label="启用折旧" prop="isDepr">
          <el-switch v-model="form.isDepr" :active-value="1" :inactive-value="2" />
        </el-form-item>
      </el-col>
      <el-col :span="12" v-if="form.isDepr === 1">
        <el-form-item label="每年折旧金额" prop="annualDepreciationAmount">
          <el-input-number
            :step="0.01"
            :min="0"
            style="width: 100%"
            v-model="form.annualDepreciationAmount"
            placeholder="请输入每年折旧金额"
          />
        </el-form-item>
      </el-col>
      <el-col :span="24">
        <el-form-item label="设备图片" prop="storageBlobDTOs">
          <AttachmentUploadImage
@@ -172,7 +270,6 @@
<script setup>
import useFormData from "@/hooks/useFormData";
// import useUserStore from "@/store/modules/user";
import { getLedgerById } from "@/api/equipmentManagement/ledger";
import dayjs from "dayjs";
import {
@@ -190,7 +287,7 @@
  name: "设备台账表单",
});
const formRef = ref(null);
const operationType = ref('');
const operationType = ref('add');
// è®¾å¤‡ç±»åž‹å›ºå®šé€‰é¡¹
const deviceTypeOptions = ref([
  '生产设备',
@@ -200,6 +297,18 @@
  '其他设备'
]);
const formRules = {
  uniqueCode: [
    {
      validator: (rule, value, callback) => {
        if (!form.autoGenerateUniqueCode && (value === undefined || value === null || value === '')) {
          callback(new Error('请输入设备编号'));
        } else {
          callback();
        }
      },
      trigger: "blur"
    }
  ],
    deviceName: [{ required: true, trigger: "blur", message: "请输入" }],
    deviceModel: [{ required: true, trigger: "blur", message: "请输入" }],
    type: [{ required: true, trigger: "change", message: "请选择或输入设备类型" }],
@@ -209,6 +318,7 @@
    taxIncludingPriceUnit: [{ required: true, trigger: "blur", message: "请输入" }],
    taxRate: [{ required: true, trigger: "change", message: "请输入" }],
    planRuntimeTime: [{ required: true, trigger: "change", message: "请选择" }],
  status: [{ required: true, trigger: "change", message: "请选择" }],
    annualDepreciationAmount: [
        { 
            validator: (rule, value, callback) => {
@@ -224,23 +334,40 @@
}
const { form, resetForm } = useFormData({
  // Basic Info
  autoGenerateUniqueCode: true,
  uniqueCode: undefined, // å”¯ä¸€æ€§ç¼–号
  deviceName: undefined, // è®¾å¤‡åç§°
  deviceModel: undefined, // è§„格型号
  deviceBrand: undefined, // è®¾å¤‡å“ç‰Œ
  type: undefined, // è®¾å¤‡ç±»åž‹
  supplierName: undefined, // ä¾›åº”商
  factoryNumber: undefined, // å‡ºåŽ‚ç¼–å·
  storageLocation: undefined, // å­˜æ”¾ä½ç½®
  isDepr: 2, // æ˜¯å¦å¯ç”¨æŠ˜æ—§ 1-是 2-否
  annualDepreciationAmount: undefined, // æ¯å¹´æŠ˜æ—§é‡‘额
  responsiblePerson: undefined, // è´£ä»»äºº
  filingDate: dayjs().format("YYYY-MM-DD"), // å»ºæ¡£æ—¥æœŸ
  // Purchase Info
  supplierName: undefined, // ä¾›åº”商
  manufacturer: undefined, // åˆ¶é€ åނ商
  purchaseDate: undefined, // è´­ç½®æ—¥æœŸ
  originalAssetValue: undefined, // èµ„产原值
  unit: undefined, // å•位
  number: 1, // æ•°é‡
  taxIncludingPriceUnit: undefined, // å«ç¨Žå•ä»·
  taxIncludingPriceTotal: undefined, // å«ç¨Žæ€»ä»·
  taxRate: undefined, // ç¨Žçއ
  unTaxIncludingPriceTotal: undefined, // ä¸å«ç¨Žæ€»ä»·
  // createUser: useUserStore().nickName, // å½•入人
  createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // å½•入日期
    planRuntimeTime: dayjs().format("YYYY-MM-DD"), // å½•入日期
  // Technical Specs
  technicalParameters: undefined, // ä¸»è¦æŠ€æœ¯å‚æ•°
  mainPurpose: undefined, // ä¸»è¦ç”¨é€”
  // Operation Info
  status: 1, // è®¾å¤‡çŠ¶æ€ (0停用 1启用)
  activationDate: undefined, // å¯ç”¨æ—¥æœŸ
  planRuntimeTime: dayjs().format("YYYY-MM-DD"), // é¢„计运行时间
  isDepr: 2, // æ˜¯å¦å¯ç”¨æŠ˜æ—§ 1-是 2-否
  annualDepreciationAmount: undefined, // æ¯å¹´æŠ˜æ—§é‡‘额
  storageBlobDTOs: undefined, // è®¾å¤‡å›¾ç‰‡æäº¤
  storageBlobVOs: undefined, // è®¾å¤‡å›¾ç‰‡å±•示
});
@@ -257,25 +384,46 @@
const loadForm = async (id) => {
    if (id) {
        operationType.value = 'edit'
    operationType.value = 'edit';
  } else {
    operationType.value = 'add';
    }
  const { code, data } = await getLedgerById(id);
  if (code == 200) {
    form.autoGenerateUniqueCode = false;
    form.uniqueCode = data.uniqueCode;
    form.deviceName = data.deviceName;
    form.deviceModel = data.deviceModel;
    form.deviceBrand = data.deviceBrand;
    form.type = data.type;
    form.supplierName = data.supplierName;
    form.factoryNumber = data.factoryNumber;
    form.storageLocation = data.storageLocation;
    form.isDepr = data.isDepr;
    form.annualDepreciationAmount = data.annualDepreciationAmount;
    form.responsiblePerson = data.responsiblePerson;
    form.filingDate = data.filingDate;
    form.supplierName = data.supplierName;
    form.manufacturer = data.manufacturer;
    if (data.purchaseDate) {
      form.purchaseDate = dayjs(data.purchaseDate).format('YYYY-MM-DD');
    }
    form.originalAssetValue = data.originalAssetValue;
    form.unit = data.unit;
    form.number = 1;
    form.taxIncludingPriceUnit = data.taxIncludingPriceUnit;
    form.taxIncludingPriceTotal = data.taxIncludingPriceTotal;
    form.taxRate = data.taxRate;
    form.unTaxIncludingPriceTotal = data.unTaxIncludingPriceTotal;
    form.createTime = data.createTime;
    form.technicalParameters = data.technicalParameters;
    form.mainPurpose = data.mainPurpose;
    form.status = data.status != null ? data.status : 1;
    if (data.activationDate) {
      form.activationDate = dayjs(data.activationDate).format('YYYY-MM-DD');
    }
    form.isDepr = data.isDepr;
    form.annualDepreciationAmount = data.annualDepreciationAmount;
    // é¢„计运行时间:后端返回后转为 YYYY-MM-DD ä»¥ä¾¿æ—¥æœŸé€‰æ‹©å™¨æ­£ç¡®å±•示
    if (data.planRuntimeTime) {
      form.planRuntimeTime = dayjs(data.planRuntimeTime).format('YYYY-MM-DD');
@@ -319,6 +467,7 @@
// é‡ç½®è¡¨å•数据和校验状态
const resetFormAndValidate = () => {
  resetForm();
  operationType.value = 'add'; // é‡ç½®ä¸ºæ–°å¢ž
  clearValidate();
};
src/views/equipmentManagement/ledger/index.vue
@@ -1,6 +1,15 @@
<template>
  <div class="app-container">
    <el-form :model="filters" :inline="true">
      <el-form-item label="设备编号">
        <el-input
          v-model="filters.uniqueCode"
          style="width: 200px"
          placeholder="请输入设备编号"
          clearable
          @change="getTableData"
        />
      </el-form-item>
      <el-form-item label="设备名称">
        <el-input
          v-model="filters.deviceName"
@@ -113,6 +122,7 @@
    <!-- è¯¦æƒ…对话框 -->
    <el-dialog v-model="detailDialogVisible" title="设备台账详情" width="60%" draggable>
      <el-descriptions :column="2" border>
        <el-descriptions-item label="设备编号">{{ detailData.uniqueCode }}</el-descriptions-item>
        <el-descriptions-item label="设备名称">{{ detailData.deviceName }}</el-descriptions-item>
        <el-descriptions-item label="规格型号">{{ detailData.deviceModel }}</el-descriptions-item>
        <el-descriptions-item label="设备品牌">{{ detailData.deviceBrand }}</el-descriptions-item>
@@ -148,13 +158,88 @@
        <el-button @click="detailDialogVisible = false">关闭</el-button>
      </template>
    </el-dialog>
    <!-- çŠ¶æ€å˜æ›´å¯¹è¯æ¡† -->
    <el-dialog v-model="statusDialog.visible" title="设备状态变更申请" width="500px" draggable>
      <el-form label-width="100px">
        <el-form-item label="当前状态">
          <el-tag :type="statusDialog.row?.status == 1 ? 'success' : 'danger'">
            {{ statusDialog.row?.status == 1 ? '启用' : '停用' }}
          </el-tag>
        </el-form-item>
        <el-form-item label="目标状态" required>
          <el-radio-group v-model="statusDialog.targetStatus">
            <el-radio :label="1">启用</el-radio>
            <el-radio :label="0">停用</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="变更原因" required>
          <el-input type="textarea" :rows="3" v-model="statusDialog.reason" placeholder="请输入状态变更原因" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="statusDialog.visible = false">取消</el-button>
        <el-button type="primary" :loading="statusDialog.loading" @click="submitStatusChange">提交审批</el-button>
      </template>
    </el-dialog>
    <!-- çŠ¶æ€å˜æ›´è®°å½•å¯¹è¯æ¡† -->
    <el-dialog v-model="recordDialog.visible" title="设备状态变更记录" width="800px" draggable>
      <el-table v-loading="recordDialog.loading" :data="recordDialog.list" border>
        <el-table-column label="变更时间" prop="createTime" width="160" />
        <el-table-column label="原状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.originalStatus == 1 ? 'success' : 'danger'">
              {{ scope.row.originalStatus == 1 ? '启用' : '停用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="目标状态" width="100">
          <template #default="scope">
            <el-tag :type="scope.row.targetStatus == 1 ? 'success' : 'danger'">
              {{ scope.row.targetStatus == 1 ? '启用' : '停用' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="变更原因" prop="reason" show-overflow-tooltip />
        <el-table-column label="申请人" prop="createUserName" width="100" />
        <el-table-column label="审批状态" width="100">
          <template #default="scope">
            <el-tag v-if="scope.row.approvalStatus === 'PENDING'" type="warning">审批中</el-tag>
            <el-tag v-else-if="scope.row.approvalStatus === 'APPROVED'" type="success">已通过</el-tag>
            <el-tag v-else-if="scope.row.approvalStatus === 'REJECTED'" type="danger">已驳回</el-tag>
            <span v-else>{{ scope.row.approvalStatus }}</span>
          </template>
        </el-table-column>
        <el-table-column label="驳回原因" prop="rejectReason" show-overflow-tooltip>
          <template #default="scope">
            <span v-if="scope.row.approvalStatus === 'REJECTED'">{{ scope.row.rejectReason || '--' }}</span>
            <span v-else>--</span>
          </template>
        </el-table-column>
      </el-table>
      <div style="display: flex; justify-content: flex-end; margin-top: 15px;">
        <el-pagination
          v-model:current-page="recordDialog.queryParams.pageNum"
          v-model:page-size="recordDialog.queryParams.pageSize"
          :total="recordDialog.total"
          layout="total, prev, pager, next"
          @current-change="getRecordList"
        />
      </div>
      <template #footer>
        <el-button @click="recordDialog.visible = false">关闭</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { usePaginationApi } from "@/hooks/usePaginationApi";
// import { Search } from "@element-plus/icons-vue";
import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger";
import { listStatusChangeRecord } from "@/api/device/statusChangeRecord";
import { saveApprovalInstance } from "@/api/officeProcessAutomation/approvalInstance";
import { listApprovalTemplate, getApprovalTemplateDetail, TEMPLATE_TYPE_CUSTOM } from "@/api/officeProcessAutomation/approvalTemplate";
import { onMounted, getCurrentInstance, ref, reactive } from "vue";
import Modal from "./Modal.vue";
import { ElMessageBox, ElMessage } from "element-plus";
@@ -204,6 +289,7 @@
} = usePaginationApi(
  getLedgerPage,
  {
    uniqueCode: undefined,
    deviceName: undefined,
    deviceModel: undefined,
    supplierName: undefined,
@@ -211,6 +297,10 @@
    entryDateEnd: undefined,
  },
  [
    {
      label: "设备编号",
      prop: "uniqueCode",
    },
    {
      label: "设备名称",
      prop: "deviceName",
@@ -240,18 +330,34 @@
      prop: "number",
    },
    {
      label: "设备状态",
      prop: "status",
      width: 100,
      dataType: "tag",
      formatData: (v) => v == 1 ? '启用' : '停用',
      formatType: (v) => v == 1 ? 'success' : 'danger',
    },
    {
      label: "启用日期",
      prop: "activationDate",
      width: 120,
      formatData: (v) => {
        if (!v) return '';
        if (v.includes(' ')) return v.split(' ')[0];
        return v;
      },
    },
    {
      label: "录入人",
      prop: "createUser",
    },
    {
      label: "录入日期",
      prop: "createTime",
      label: "建档日期",
      prop: "filingDate",
      width: 120,
      formatData: (v) => {
        if (!v) return '';
        // å¦‚果包含时分秒,只取日期部分
        if (v.includes(' ')) {
          return v.split(' ')[0];
        }
        if (v.includes(' ')) return v.split(' ')[0];
        return v;
      },
    },
@@ -266,6 +372,18 @@
          name: "详情",
          clickFun: (row) => {
            handleDetail(row);
          },
        },
        {
          name: "状态变更",
          clickFun: (row) => {
            openStatusChange(row);
          },
        },
        {
          name: "变更记录",
          clickFun: (row) => {
            openRecordDialog(row);
          },
        },
                {
@@ -303,6 +421,102 @@
    detailDialogVisible.value = true;
  }
};
const statusDialog = reactive({ visible: false, row: null, targetStatus: 1, reason: '', loading: false });
const openStatusChange = (row) => {
  statusDialog.row = row;
  statusDialog.targetStatus = row.status == 1 ? 0 : 1;
  statusDialog.reason = '';
  statusDialog.visible = true;
};
const submitStatusChange = async () => {
  if (!statusDialog.reason.trim()) {
    ElMessage.error("请填写状态变更原因");
    return;
  }
  if (statusDialog.targetStatus === statusDialog.row.status) {
    ElMessage.warning("目标状态与当前状态一致");
    return;
  }
  statusDialog.loading = true;
  try {
    const listRes = await listApprovalTemplate(TEMPLATE_TYPE_CUSTOM);
    const templates = listRes.data?.records || listRes.data || [];
    const tpl = templates.find(t => t.businessType === 19 && (t.enabled === 1 || t.enabled === true));
    if (!tpl) {
      ElMessage.error("未找到启用的设备状态变更审批模板");
      return;
    }
    const detailRes = await getApprovalTemplateDetail(tpl.id);
    const detail = detailRes.data;
    const payload = {
      templateId: detail.id,
      templateName: detail.templateName,
      businessType: 19,
      templateSnapshot: JSON.stringify(detail),
      title: `设备状态变更审批 - ${statusDialog.row.deviceName}`,
      reason: statusDialog.reason,
      businessId: statusDialog.row.id,
      formConfig: JSON.stringify({
        targetStatus: statusDialog.targetStatus,
        targetStatusName: statusDialog.targetStatus == 1 ? '启用' : '停用',
        deviceId: statusDialog.row.id,
        deviceName: statusDialog.row.deviceName,
        reason: statusDialog.reason
      }),
      flowNodes: detail.flowNodes || []
    };
    const res = await saveApprovalInstance(payload);
    if (res.code === 200) {
      ElMessage.success("已发起状态变更审批");
      statusDialog.visible = false;
      getTableData();
    }
  } catch (error) {
    console.error(error);
  } finally {
    statusDialog.loading = false;
  }
};
const recordDialog = reactive({
  visible: false,
  loading: false,
  list: [],
  total: 0,
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    deviceId: null
  }
});
const openRecordDialog = (row) => {
  recordDialog.queryParams.deviceId = row.id;
  recordDialog.queryParams.pageNum = 1;
  recordDialog.visible = true;
  getRecordList();
};
const getRecordList = async () => {
  recordDialog.loading = true;
  try {
    const res = await listStatusChangeRecord(recordDialog.queryParams);
    if (res.code === 200) {
      recordDialog.list = res.data.records;
      recordDialog.total = res.data.total;
    }
  } catch (error) {
    console.error(error);
  } finally {
    recordDialog.loading = false;
  }
};
const changePage = ({ page, limit }) => {
  pagination.currentPage = page;
    pagination.pageSize = limit;
src/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue
@@ -47,6 +47,7 @@
                                type="date"
                                placeholder="请选择"
                                clearable
                                @change="calculateValidUntil"
                            />
                        </el-form-item>
                    </el-col>
@@ -67,6 +68,85 @@
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="有效期至:" prop="validUntil">
                            <el-input
                                v-model="form.validUntil"
                                placeholder="自动计算"
                                disabled
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="溯源/核查类型:" prop="traceabilityType">
                            <el-input
                                v-model="form.traceabilityType"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="实施机构:" prop="implementingAgency">
                            <el-input
                                v-model="form.implementingAgency"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="证书编号:" prop="certificateNumber">
                            <el-input
                                v-model="form.certificateNumber"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="核心结果:" prop="coreResult">
                            <el-input
                                v-model="form.coreResult"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="结果确认:" prop="resultConfirmation">
                            <el-input
                                v-model="form.resultConfirmation"
                                placeholder="请输入"
                                clearable
                            />
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="确认人:" prop="confirmerId">
                            <el-select
                                v-model="form.confirmerId"
                                placeholder="请选择"
                                filterable
                                default-first-option
                                :reserve-keyword="false"
                                clearable
                            >
                                <el-option
                                    v-for="item in userList"
                                    :key="item.userId"
                                    :label="item.nickName"
                                    :value="item.userId"
                                ></el-option>
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="12">
                        <el-form-item label="录入人:" prop="userId">
                            <el-select
                                v-model="form.userId"
@@ -85,6 +165,8 @@
                            </el-select>
                        </el-form-item>
                    </el-col>
                </el-row>
                <el-row :gutter="30">
                    <el-col :span="12">
                        <el-form-item label="录入日期:" prop="entryDate">
                            <el-date-picker
@@ -128,6 +210,7 @@
</template>
<script setup>
import dayjs from "dayjs";
import {ref, reactive, toRefs, getCurrentInstance} from "vue";
import useUserStore from "@/store/modules/user.js";
import {userListNoPageByTenantId} from "@/api/system/user.js";
@@ -150,7 +233,14 @@
        recordDate: "",
        userId: "",
        entryDate: "",
    tempFileIds: []
    tempFileIds: [],
        traceabilityType: "",
        implementingAgency: "",
        certificateNumber: "",
        coreResult: "",
        resultConfirmation: "",
        validUntil: "",
        confirmerId: ""
    },
    rules: {
        code: [{required: true, message: "请输入", trigger: "blur"}],
@@ -211,6 +301,13 @@
    if (type === "edit") {
        form.value.valid = row.valid;
        form.value.recordDate = row.recordDate;
        form.value.traceabilityType = row.traceabilityType;
        form.value.implementingAgency = row.implementingAgency;
        form.value.certificateNumber = row.certificateNumber;
        form.value.coreResult = row.coreResult;
        form.value.resultConfirmation = row.resultConfirmation;
        form.value.validUntil = row.validUntil;
        form.value.confirmerId = row.confirmerId;
    fileList.value = row.commonFiles;
    }
    if(type === "add"){
@@ -219,6 +316,14 @@
    if(type === "verifying"){
        form.value.valid = row.valid;
        form.value.recordDate = row.mostDate;
        // Initialize empty for new verifying record
        form.value.traceabilityType = "";
        form.value.implementingAgency = "";
        form.value.certificateNumber = "";
        form.value.coreResult = "";
        form.value.resultConfirmation = "";
        form.value.confirmerId = "";
        calculateValidUntil();
    }
    form.value.id = row.id;
@@ -232,12 +337,14 @@
const handleValidInput = (value) => {
    if (value === '' || value === null || value === undefined) {
        form.value.valid = '';
        calculateValidUntil();
        return;
    }
    // è½¬æ¢ä¸ºå­—符串并移除所有非数字字符(包括负号、小数点等)
    const numStr = String(value).replace(/[^0-9]/g, '');
    if (numStr === '') {
        form.value.valid = '';
        calculateValidUntil();
        return;
    }
    const numValue = parseInt(numStr, 10);
@@ -247,6 +354,16 @@
    } else {
        form.value.valid = '';
    }
    calculateValidUntil();
}
// è‡ªåŠ¨è®¡ç®—æœ‰æ•ˆæœŸè‡³
const calculateValidUntil = () => {
    if (form.value.recordDate && form.value.valid) {
        form.value.validUntil = dayjs(form.value.recordDate).add(form.value.valid, 'day').format('YYYY-MM-DD');
    } else {
        form.value.validUntil = '';
    }
}
const submitForm = () => {
src/views/equipmentManagement/operationManagement/index.vue
@@ -61,9 +61,9 @@
              :type="getDeviceStatusType(scope.row.status)"
              size="small"
            >
              <el-icon v-if="scope.row.status === '运行中'"><VideoPlay /></el-icon>
              <el-icon v-if="String(scope.row.status) === '1'"><VideoPlay /></el-icon>
              <el-icon v-else><VideoPause /></el-icon>
              {{ scope.row.status || '未知' }}
              {{ String(scope.row.status) === '1' ? '运行中' : '停止运行' }}
            </el-tag>
          </template>
        </el-table-column>
@@ -126,7 +126,7 @@
            <!-- æ­£å¸¸çŠ¶æ€æ—¶æ˜¾ç¤ºå¯¹åº”çš„æ“ä½œæŒ‰é’® -->
            <template v-else>
              <el-button
                v-if="scope.row.status === '运行中'"
                v-if="String(scope.row.status) === '1'"
                type="danger"
                size="small"
                @click="changeDeviceStatus(scope.row, '停止运行')"
@@ -186,9 +186,9 @@
  // æ ¹æ®è®¾å¤‡çŠ¶æ€ç­›é€‰
  if (deviceFilter.value !== 'all') {
    if (deviceFilter.value === 'start') {
      filtered = filtered.filter(device => device.status === '运行中')
      filtered = filtered.filter(device => String(device.status) === '1')
    } else if (deviceFilter.value === 'stop') {
      filtered = filtered.filter(device => device.status === '停止运行')
      filtered = filtered.filter(device => String(device.status) === '0')
    }
  }
@@ -264,7 +264,7 @@
  }
  const statusStr = String(row?.status ?? '').trim()
  const isRunning = statusStr === '运行中' || statusStr === '1'
  const isRunning = statusStr === '1'
  const endRaw = pickEndTime(row)
  const hasEnd = hasMeaningfulEnd(endRaw)
@@ -289,7 +289,7 @@
// æ£€æŸ¥è®¾å¤‡æ˜¯å¦è¶…时未启动
const isOverdue = (device) => {
  if (!device.planRuntimeTime || device.status === '运行中' || device.startRuntimeTime) {
  if (!device.planRuntimeTime || String(device.status) === '1' || device.startRuntimeTime) {
    return false
  }
  
@@ -330,12 +330,12 @@
    
    // æ›´æ–°è®¾å¤‡çŠ¶æ€å’Œç›¸å…³æ—¶é—´å­—æ®µ
    if (status === '启动运行') {
      device.status = '运行中'
      device.status = 1
      device.startRuntimeTime = currentTime
      device.endRuntimeTime = null // æ¸…空结束时间
      device.runtimeDuration = null // æ¸…空运行时长
    } else {
      device.status = '停止运行'
      device.status = 0
      device.endRuntimeTime = currentTime
      // è®¡ç®—运行时长
      if (device.startRuntimeTime) {
@@ -370,9 +370,9 @@
}
const getDeviceStatusType = (status) => {
  if (status === '运行中') {
  if (String(status) === '1') {
    return 'success'
  } else if (status === '停止运行') {
  } else if (String(status) === '0') {
    return 'danger'
  } else {
    return 'info'
src/views/equipmentManagement/repair/Modal/AcceptanceModal.vue
@@ -8,6 +8,14 @@
    @close="handleCancel"
  >
    <el-form :model="form" :rules="rules" label-width="100px">
      <!-- é…ä»¶ä½¿ç”¨æƒ…况展示 -->
      <el-form-item label="配件使用明细" v-if="sparePartsDetails.length > 0">
        <el-table :data="sparePartsDetails" size="small" border style="width: 100%">
          <el-table-column prop="sparePartName" label="配件名称" />
          <el-table-column prop="quantity" label="使用数量" width="100" />
        </el-table>
      </el-form-item>
      <el-form-item label="验收人" prop="acceptanceName">
        <el-select
          v-model="form.acceptanceName"
@@ -50,7 +58,7 @@
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { repairAcceptance } from "@/api/equipmentManagement/repair";
import { repairAcceptance, getRepairById } from "@/api/equipmentManagement/repair";
import dayjs from "dayjs";
defineOptions({
@@ -63,6 +71,7 @@
const loading = ref(false);
const repairId = ref(null);
const userList = ref([]);
const sparePartsDetails = ref([]);
const form = reactive({
  acceptanceName: undefined,
@@ -92,10 +101,16 @@
const open = async (row) => {
  repairId.value = row.id;
  visible.value = true;
  // é‡ç½®è¡¨å•
  form.acceptanceName = undefined;
  form.acceptanceTime = dayjs().format("YYYY-MM-DD HH:mm:ss");
  form.acceptanceRemark = undefined;
  sparePartsDetails.value = [];
  try {
    const { data } = await getRepairById(row.id);
    if (data && data.sparePartsUseListDetails) {
      sparePartsDetails.value = data.sparePartsUseListDetails;
    }
  } catch(e) {}
  await loadUserList();
};
src/views/equipmentManagement/repair/Modal/MaintainModal.vue
@@ -14,6 +14,12 @@
      <el-form-item label="维修结果">
        <el-input v-model="form.maintenanceResult" placeholder="请输入维修结果" />
      </el-form-item>
      <el-form-item label="原因分析">
        <el-input v-model="form.reasonAnalysis" type="textarea" :rows="2" placeholder="请输入原因分析" />
      </el-form-item>
      <el-form-item label="处理措施">
        <el-input v-model="form.handlingMeasures" type="textarea" :rows="2" placeholder="请输入处理措施" />
      </el-form-item>
      <el-form-item label="维修状态">
        <el-select v-model="form.status">
          <el-option label="待报修" :value="0"></el-option>
@@ -97,6 +103,8 @@
const { form, resetForm } = useFormData({
  maintenanceName: undefined, // ç»´ä¿®åç§°
  maintenanceResult: undefined, // ç»´ä¿®ç»“æžœ
  reasonAnalysis: undefined, // åŽŸå› åˆ†æž
  handlingMeasures: undefined, // å¤„理措施
  maintenanceTime: undefined, // ç»´ä¿®æ—¥æœŸ
  status: 0,
  sparePartsIds: [],
@@ -114,6 +122,8 @@
const setForm = (data) => {
  form.maintenanceName = data.maintenanceName ?? userStore.nickName;
  form.maintenanceResult = data.maintenanceResult;
  form.reasonAnalysis = data.reasonAnalysis;
  form.handlingMeasures = data.handlingMeasures;
  form.maintenanceTime =
    data.maintenanceTime 
      ? dayjs(data.maintenanceTime).format("YYYY-MM-DD HH:mm:ss")
src/views/equipmentManagement/repair/Modal/RepairModal.vue
@@ -58,6 +58,18 @@
      </el-row>
      <el-row>
        <el-col :span="12">
          <el-form-item label="报修类型">
            <el-select v-model="form.repairType"
                       placeholder="请选择报修类型"
                       :disabled="operationType === 'view'">
              <el-option v-for="dict in device_repair_type"
                         :key="dict.value"
                         :label="dict.label"
                         :value="Number(dict.value)"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12">
          <el-form-item label="维修人">
            <el-input v-model="form.maintenanceName"
                      placeholder="请输入维修人姓名"
@@ -76,7 +88,26 @@
                         :value="1"></el-option>
              <el-option label="失败"
                         :value="2"></el-option>
              <el-option label="待验收"
                         :value="3"></el-option>
            </el-select>
          </el-form-item>
        </el-col>
        <el-col :span="12" v-if="form.status !== 0">
          <el-form-item label="维修时间">
            <el-input v-model="form.maintenanceTime"
                      disabled />
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="form.status !== 0">
        <el-col :span="24">
          <el-form-item label="维修结果">
            <el-input v-model="form.maintenanceResult"
                      :rows="2"
                      type="textarea"
                      placeholder="暂无维修结果"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
@@ -114,6 +145,37 @@
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="form.status !== 0">
        <el-col :span="24">
          <el-form-item label="原因分析">
            <el-input v-model="form.reasonAnalysis"
                      :rows="2"
                      type="textarea"
                      placeholder="请输入原因分析"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
        <el-col :span="24">
          <el-form-item label="处理措施">
            <el-input v-model="form.handlingMeasures"
                      :rows="2"
                      type="textarea"
                      placeholder="请输入处理措施"
                      :disabled="operationType === 'view'" />
          </el-form-item>
        </el-col>
      </el-row>
      <!-- é…ä»¶ä½¿ç”¨æ˜Žç»†å±•示 -->
      <el-row v-if="sparePartsDetails.length > 0">
        <el-col :span="24">
          <el-form-item label="配件使用明细">
            <el-table :data="sparePartsDetails" size="small" border style="width: 100%">
              <el-table-column prop="sparePartName" label="配件名称" />
              <el-table-column prop="quantity" label="使用数量" width="100" />
            </el-table>
          </el-form-item>
        </el-col>
      </el-row>
      <el-row v-if="operationType !== 'view'"
              :gutter="30">
        <el-col :span="24">
@@ -147,6 +209,8 @@
  });
  const emits = defineEmits(["ok"]);
  const { proxy } = getCurrentInstance();
  const { device_repair_type } = proxy.useDict("device_repair_type");
  const id = ref();
  const visible = ref(false);
@@ -163,6 +227,7 @@
  const userStore = useUserStore();
  const deviceOptions = ref([]);
  const fileList = ref([]);
  const sparePartsDetails = ref([]);
  const loadDeviceName = async () => {
    const { data } = await getDeviceLedger();
@@ -174,12 +239,17 @@
    deviceName: undefined, // è®¾å¤‡åç§°
    deviceModel: undefined, // è§„格型号
    repairTime: dayjs().format("YYYY-MM-DD"), // æŠ¥ä¿®æ—¥æœŸï¼Œé»˜è®¤å½“天
    repairType: undefined, // æŠ¥ä¿®ç±»åž‹
    repairName: userStore.nickName, // æŠ¥ä¿®äºº
    remark: undefined, // æ•…障现象
    reasonAnalysis: undefined, // åŽŸå› åˆ†æž
    handlingMeasures: undefined, // å¤„理措施
    status: 0, // æŠ¥ä¿®çŠ¶æ€
    machineryCategory: undefined,
    storageBlobDTOs: [],
    maintenanceName: undefined, // ç»´ä¿®äºº
    maintenanceTime: undefined,
    maintenanceResult: undefined,
  });
  const setDeviceModel = deviceId => {
@@ -192,15 +262,21 @@
    form.deviceName = data.deviceName;
    form.deviceModel = data.deviceModel;
    form.repairTime = data.repairTime;
    form.repairType = data.repairType;
    form.repairName = data.repairName;
    form.remark = data.remark;
    form.reasonAnalysis = data.reasonAnalysis;
    form.handlingMeasures = data.handlingMeasures;
    form.status = data.status;
    form.machineryCategory = data.machineryCategory;
    form.storageBlobDTOs = data.storageBlobVOs || [];
    form.maintenanceName = data.maintenanceName;
    form.maintenanceTime = data.maintenanceTime;
    form.maintenanceResult = data.maintenanceResult;
    form.acceptanceName = data.acceptanceName;
    form.acceptanceTime = data.acceptanceTime;
    form.acceptanceRemark = data.acceptanceRemark;
    sparePartsDetails.value = data.sparePartsUseListDetails || [];
  };
  const sendForm = async () => {
@@ -234,6 +310,7 @@
    operationType.value = "add";
    visible.value = true;
    fileList.value = [];
    sparePartsDetails.value = [];
    await nextTick();
    await loadDeviceName();
  };
src/views/equipmentManagement/repair/index.vue
@@ -96,6 +96,9 @@
          <el-tag v-if="row.status === 0"
                  type="warning">待维修</el-tag>
        </template>
        <template #repairTypeRef="{ row }">
          <dict-tag :options="device_repair_type" :value="row.repairType" />
        </template>
        <template #operation="{ row }">
          <el-button type="primary"
                     link
@@ -171,6 +174,7 @@
  });
  const { proxy } = getCurrentInstance();
  const { device_repair_type } = proxy.useDict("device_repair_type");
  // æ¨¡æ€æ¡†å®žä¾‹
  const repairModalRef = ref();
@@ -222,6 +226,13 @@
        prop: "repairName",
      },
      {
        label: "报修类型",
        align: "center",
        prop: "repairType",
        dataType: "slot",
        slot: "repairTypeRef",
      },
      {
        label: "状态",
        align: "center",
        prop: "status",
src/views/equipmentManagement/upkeep/Form/MaintenanceModal.vue
@@ -35,6 +35,11 @@
                  placeholder="请输入保养结果"
                  type="text" />
      </el-form-item>
      <el-form-item label="运行状况">
        <el-input v-model="form.runningStatus"
                  placeholder="请输入运行状况"
                  type="text" />
      </el-form-item>
      <el-form-item label="设备备件">
        <el-select v-model="form.sparePartsIds"
                   :loading="loadingSparePartOptions"
@@ -104,6 +109,7 @@
    maintenanceActuallyName: undefined, // å®žé™…保养人
    maintenanceActuallyTime: undefined, // å®žé™…保养日期
    maintenanceResult: undefined, // ä¿å…»ç»“æžœ
    runningStatus: undefined, // è¿è¡Œçж况
    status: 0, // ä¿å…»çŠ¶æ€
    sparePartsIds: [],
    storageBlobDTOs: [],
@@ -126,6 +132,7 @@
      ? dayjs(data.maintenanceActuallyTime).format("YYYY-MM-DD HH:mm:ss")
      : dayjs().format("YYYY-MM-DD HH:mm:ss");
    form.maintenanceResult = data.maintenanceResult;
    form.runningStatus = data.runningStatus;
    form.status = 1; // é»˜è®¤çŠ¶æ€ä¸ºå®Œç»“
    // multiple é€‰æ‹©å™¨è¦æ±‚数组;后端常返回 "1,2,3"
    if (Array.isArray(data?.sparePartsIds)) {
src/views/equipmentManagement/upkeep/Form/PlanModal.vue
@@ -57,9 +57,30 @@
      </el-form-item>
      <el-form-item v-if="id" label="保修状态">
        <el-select v-model="form.status">
          <el-option label="待保修" :value="0"></el-option>
          <el-option label="待保养" :value="0"></el-option>
          <el-option label="完结" :value="1"></el-option>
          <el-option label="失败" :value="2"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item label="保养类型" prop="maintenanceType">
        <el-select v-model="form.maintenanceType" placeholder="请选择保养类型" clearable>
          <el-option label="日常使用" value="1"/>
          <el-option label="定期保养" value="2"/>
        </el-select>
      </el-form-item>
      <el-form-item label="执行人" prop="executorId">
        <el-select
          v-model="form.executorId"
          filterable
          placeholder="请选择执行人"
          clearable
        >
          <el-option
            v-for="item in userList"
            :label="item.nickName"
            :value="item.userId"
            :key="item.userId"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="保养人">
@@ -132,6 +153,8 @@
  machineryCategory: undefined,
  storageBlobDTOs: [],
  maintenancePerson: undefined, // ä¿å…»äºº
  maintenanceType: undefined,
  executorId: undefined
});
const setDeviceModel = (deviceId) => {
@@ -151,6 +174,8 @@
  form.status = data.status;
  form.machineryCategory = data.machineryCategory;
  form.maintenancePerson = data.maintenancePerson;
  form.maintenanceType = data.maintenanceType;
  form.executorId = data.executorId;
  if (data.maintenancePlanTime) {
    form.maintenancePlanTime = dayjs(data.maintenancePlanTime).format(
      "YYYY-MM-DD HH:mm:ss"
src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -70,7 +70,7 @@
                    <el-form-item label="保养项目" prop="machineryCategory">
                        <el-input
                            v-model.trim="form.machineryCategory"
                            placeholder="请输入保养项目"
                            placeholder="请输入工作内容描述(保养项目)"
                            maxlength="100"
                            clearable
                        />
@@ -84,6 +84,33 @@
                            maxlength="100"
                            clearable
                        />
                    </el-form-item>
                </el-col>
            </el-row>
            <el-row>
                <el-col :span="12">
                    <el-form-item label="保养类型" prop="maintenanceType">
                        <el-select v-model="form.maintenanceType" placeholder="请选择保养类型" clearable>
                            <el-option label="日常使用" value="1"/>
                            <el-option label="定期保养" value="2"/>
                        </el-select>
                    </el-form-item>
                </el-col>
                <el-col :span="12">
                    <el-form-item label="执行人" prop="executorId">
                        <el-select
                            v-model="form.executorId"
                            filterable
                            placeholder="请选择执行人"
                            clearable
                        >
                            <el-option
                                v-for="item in userList"
                                :label="item.nickName"
                                :value="item.userId"
                                :key="item.userId"
                            />
                        </el-select>
                    </el-form-item>
                </el-col>
            </el-row>
@@ -184,13 +211,17 @@
        time: '',
        deviceModel: undefined, // è§„格型号
        registrationDate: '',
        maintenancePerson: '' // ä¿å…»äºº
        maintenancePerson: '', // ä¿å…»äºº
        maintenanceType: undefined,
        executorId: undefined
    },
    rules: {
        taskId: [{ required: true, message: "请选择设备", trigger: "change" },],
        inspector: [{ required: true, message: "请选择录入人", trigger: "blur" },],
        registrationDate: [{ required: true, message: "请选择登记时间", trigger: "change" }],
        machineryCategory: [{ required: true, message: "请输入保养项目", trigger: "blur" }]
        machineryCategory: [{ required: true, message: "请输入保养项目", trigger: "blur" }],
        maintenanceType: [{ required: true, message: "请选择保养类型", trigger: "change" }],
        executorId: [{ required: true, message: "请选择执行人", trigger: "change" }]
    }
})
const { form, rules } = toRefs(data)
@@ -271,7 +302,9 @@
        time: '',
        deviceModel: undefined,
        registrationDate: '',
        maintenancePerson: ''
        maintenancePerson: '',
        maintenanceType: undefined,
        executorId: undefined
    }
}
src/views/equipmentManagement/upkeep/index.vue
@@ -241,6 +241,7 @@
    deviceMaintenanceTaskList,
    deviceMaintenanceTaskDel,
  } from "@/api/equipmentManagement/upkeep";
  import { userListNoPageByTenantId } from "@/api/system/user.js";
  import dayjs from "dayjs";
  const { proxy } = getCurrentInstance();
@@ -261,6 +262,13 @@
  const fileListDialogRef = ref(null);
  const fileDialogVisible = ref(false);
  const currentMaintenanceTaskId = ref(null);
  const userList = ref([]);
  const getUserName = (userId) => {
    if (!userId) return "--";
    const user = userList.value.find(u => String(u.userId) === String(userId));
    return user ? user.nickName : userId;
  };
  // ä¿å…»è®°å½•tab(原设备保养页面)相关变量
  const filters = reactive({
@@ -304,6 +312,18 @@
      prop: "machineryCategory",
      minWidth: 120,
      formatData: cell => cell || "--",
    },
    {
      label: "保养类型",
      prop: "maintenanceType",
      minWidth: 100,
      formatData: cell => (cell === "1" ? "日常使用" : cell === "2" ? "定期保养" : "--")
    },
    {
      prop: "executorId",
      label: "执行人",
      minWidth: 100,
      formatData: cell => getUserName(cell)
    },
    {
      prop: "frequencyType",
@@ -391,6 +411,18 @@
      align: "center",
      prop: "machineryCategory",
      formatData: cell => cell || "--",
    },
    {
      label: "保养类型",
      align: "center",
      prop: "maintenanceType",
      formatData: cell => (cell === "1" ? "日常使用" : cell === "2" ? "定期保养" : "--")
    },
    {
      label: "执行人",
      align: "center",
      prop: "executorId",
      formatData: cell => getUserName(cell)
    },
    // {
    //   label: "录入日期",
@@ -628,7 +660,9 @@
    fileDialogVisible.value = true;
  };
  onMounted(() => {
  onMounted(async () => {
    const { data } = await userListNoPageByTenantId();
    userList.value = data || [];
    // æ ¹æ®é»˜è®¤æ¿€æ´»çš„ Tab è°ƒç”¨å¯¹åº”的查询接口
    if (activeTab.value === "scheduled") {
      getScheduledTableData();
src/views/inventoryManagement/fuelCardManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,430 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div style="flex: 1; padding-right: 20px;">
        <el-row :gutter="16">
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">加油卡号:</span>
            <el-input v-model="queryParams.cardNumber" placeholder="请输入加油卡号" clearable style="flex: 1;" @keyup.enter.native="handleQuery" />
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">所属部门:</span>
            <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" check-strictly clearable filterable style="flex: 1;" />
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <el-button type="primary" @click="handleQuery">搜索</el-button>
            <el-button @click="resetQuery">重置</el-button>
          </el-col>
        </el-row>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="handleAdd">新增加油卡</el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        v-loading="loading"
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 22em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="cardNumber" label="加油卡号" min-width="150" />
        <el-table-column prop="deptName" label="所属部门" min-width="150" show-overflow-tooltip />
        <el-table-column prop="issuer" label="发卡单位" min-width="120" />
        <el-table-column prop="initialBalance" label="期初余额" min-width="100" align="right" />
        <el-table-column prop="currentBalance" label="当前余额" min-width="100" align="right" />
        <el-table-column prop="status" label="状态" min-width="100" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.status === 'ACTIVE' ? 'success' : 'danger'">
              {{ scope.row.status === 'ACTIVE' ? '正常' : '注销' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
        <el-table-column label="操作" fixed="right" width="200" align="center">
          <template #default="scope">
            <el-button type="success" link size="small" @click="handleRecharge(scope.row)">充值管理</el-button>
            <el-button type="primary" link size="small" @click="handleUpdate(scope.row)">编辑</el-button>
            <el-button v-if="scope.row.status !== 'CANCELLED'" type="warning" link size="small" @click="handleCancel(scope.row)">注销</el-button>
            <el-button type="danger" link size="small" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </div>
<!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
        <el-form-item label="加油卡号" prop="cardNumber">
          <el-input v-model="form.cardNumber" placeholder="请输入加油卡号" />
        </el-form-item>
        <el-form-item label="所属部门" prop="deptId">
          <el-tree-select
            v-model="form.deptId"
            :data="deptOptions"
            :props="{ value: 'id', label: 'label', children: 'children' }"
            value-key="id"
            placeholder="请选择部门"
            check-strictly
            clearable
            filterable
            style="width: 100%"
          />
        </el-form-item>
        <el-form-item label="发卡单位" prop="issuer">
          <el-input v-model="form.issuer" placeholder="请输入发卡单位" />
        </el-form-item>
        <el-form-item label="期初余额" prop="initialBalance" v-if="!form.id">
          <el-input-number v-model="form.initialBalance" :precision="2" :min="0" :step="100" style="width: 100%" />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio label="ACTIVE">正常</el-radio>
            <el-radio label="CANCELLED">注销</el-radio>
          </el-radio-group>
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- å……值管理抽屉 -->
    <el-drawer v-model="rechargeDrawer.visible" :title="'充值管理 - ' + rechargeDrawer.cardNumber" size="50%">
      <div style="padding: 0 20px;">
        <el-row style="margin-bottom: 15px;">
          <el-button type="primary" icon="Plus" @click="handleAddRecharge">新增充值</el-button>
        </el-row>
        <el-table v-loading="rechargeDrawer.loading" :data="rechargeDrawer.list" border>
          <el-table-column type="index" label="序号" width="60" align="center" />
          <el-table-column prop="rechargeDate" label="充值时间" width="160" />
          <el-table-column prop="rechargeAmount" label="充值金额(元)" width="120" align="right">
            <template #default="scope">
              <span style="color: #67C23A; font-weight: bold;">+{{ scope.row.rechargeAmount }}</span>
            </template>
          </el-table-column>
          <el-table-column prop="createUserName" label="操作人" width="100" />
          <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
          <el-table-column label="操作" width="100" align="center">
            <template #default="scope">
              <el-button type="danger" link size="small" @click="handleDeleteRecharge(scope.row)">删除</el-button>
            </template>
          </el-table-column>
        </el-table>
        <pagination
          v-show="rechargeDrawer.total > 0"
          :total="rechargeDrawer.total"
          v-model:page="rechargeDrawer.queryParams.current"
          v-model:limit="rechargeDrawer.queryParams.size"
          @pagination="getRechargeList"
        />
      </div>
    </el-drawer>
    <!-- æ–°å¢žå……值弹窗 -->
    <el-dialog :title="rechargeDialog.title" v-model="rechargeDialog.visible" width="500px" append-to-body>
      <el-form ref="rechargeFormRef" :model="rechargeDialog.form" :rules="rechargeDialog.rules" label-width="100px">
        <el-form-item label="充值金额" prop="rechargeAmount">
          <el-input-number v-model="rechargeDialog.form.rechargeAmount" :min="0.01" :precision="2" :step="100" style="width: 100%;" placeholder="请输入充值金额" />
        </el-form-item>
        <el-form-item label="充值日期" prop="rechargeDate">
          <el-date-picker v-model="rechargeDialog.form.rechargeDate" type="datetime" placeholder="选择充值时间" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%;" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="rechargeDialog.form.remark" type="textarea" placeholder="请输入备注内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitRechargeForm">ç¡® å®š</el-button>
          <el-button @click="rechargeDialog.visible = false">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listFuelCardPage, addFuelCard, updateFuelCard, delFuelCard } from '@/api/inventoryManagement/vehicle';
import request from '@/utils/request';
import { listAllFuelCard, listRecharge, addRecharge, delRecharge } from '@/api/inventoryManagement/vehicle';
const statTotal = ref(0);
const statActive = ref(0);
const statCancelled = ref(0);
const statBalance = ref(0);
function getStats() {
  listAllFuelCard().then(res => {
    const arr = res.data || [];
    statTotal.value = arr.length;
    statActive.value = arr.filter(i => i.status === 'ACTIVE').length;
    statCancelled.value = arr.filter(i => i.status === 'CANCELLED').length;
    statBalance.value = arr.reduce((sum, item) => sum + (item.currentBalance || 0), 0);
  });
}
const loading = ref(true);
const tableData = ref([]);
const total = ref(0);
const open = ref(false);
const title = ref("");
const formRef = ref(null);
const deptOptions = ref([]);
const queryParams = reactive({
  current: 1,
  size: 10,
  cardNumber: undefined,
  deptId: undefined
});
const form = reactive({
  id: undefined,
  cardNumber: undefined,
  deptId: undefined,
  issuer: undefined,
  initialBalance: 0,
  status: 'ACTIVE',
  remark: undefined
});
const rules = {
  cardNumber: [{ required: true, message: "加油卡号不能为空", trigger: "blur" }],
  deptId: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
  issuer: [{ required: true, message: "发卡单位不能为空", trigger: "blur" }]
};
function getDicts() {
  request({url: '/system/user/deptTree', method: 'get'}).then(res => {
    deptOptions.value = res.data || [];
  });
}
function getList() {
  loading.value = true;
  listFuelCardPage(queryParams).then(response => {
    tableData.value = response.data.records;
    total.value = response.data.total;
    loading.value = false;
  }).catch(() => {
    loading.value = false;
  });
}
function handleQuery() {
  queryParams.current = 1;
  getList();
  getStats();
}
function resetQuery() {
  queryParams.cardNumber = undefined;
  queryParams.deptId = undefined;
  handleQuery();
}
function reset() {
  Object.assign(form, {
    id: undefined,
    cardNumber: undefined,
    deptId: undefined,
    issuer: undefined,
    initialBalance: 0,
    status: 'ACTIVE',
    remark: undefined
  });
  if (formRef.value) formRef.value.resetFields();
}
function handleAdd() {
  reset();
  open.value = true;
  title.value = "新增加油卡";
}
function handleUpdate(row) {
  reset();
  Object.assign(form, row);
  open.value = true;
  title.value = "修改加油卡";
}
function submitForm() {
  formRef.value.validate(valid => {
    if (valid) {
      if (form.id != null) {
        updateFuelCard(form).then(response => {
          ElMessage.success("修改成功");
          open.value = false;
          getList();
  getStats();
        });
      } else {
        addFuelCard(form).then(response => {
          ElMessage.success("新增成功");
          open.value = false;
          getList();
  getStats();
        });
      }
    }
  });
}
function handleCancel(row) {
  ElMessageBox.confirm('是否确认注销加油卡号为"' + row.cardNumber + '"的卡片?注销后该卡不能用于新增加油记录,但余额将保留。', "提示", {
    confirmButtonText: "确定注销",
    cancelButtonText: "取消",
    type: "warning"
  }).then(function() {
    return updateFuelCard({ id: row.id, status: 'CANCELLED' });
  }).then(() => {
    getList();
  getStats();
    ElMessage.success("注销成功");
  });
}
function handleDelete(row) {
  ElMessageBox.confirm('是否确认删除加油卡号为"' + row.cardNumber + '"的数据项?', "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning"
  }).then(function() {
    return delFuelCard(row.id);
  }).then(() => {
    getList();
  getStats();
    ElMessage.success("删除成功");
  });
}
function cancel() {
  open.value = false;
  reset();
}
onMounted(() => {
  getDicts();
  getList();
  getStats();
});
// --- å……值管理逻辑 ---
const rechargeDrawer = reactive({
  visible: false,
  loading: false,
  cardNumber: '',
  cardId: null,
  list: [],
  total: 0,
  queryParams: {
    current: 1,
    size: 10,
    cardId: null
  }
});
const rechargeDialog = reactive({
  visible: false,
  title: '新增充值',
  form: {
    cardId: null,
    rechargeAmount: null,
    rechargeDate: '',
    remark: ''
  },
  rules: {
    rechargeAmount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }],
    rechargeDate: [{ required: true, message: '请选择充值日期', trigger: 'change' }]
  }
});
const rechargeFormRef = ref(null);
function handleRecharge(row) {
  rechargeDrawer.cardId = row.id;
  rechargeDrawer.cardNumber = row.cardNumber;
  rechargeDrawer.queryParams.cardId = row.id;
  rechargeDrawer.visible = true;
  getRechargeList();
}
function getRechargeList() {
  rechargeDrawer.loading = true;
  listRecharge(rechargeDrawer.queryParams).then(res => {
    rechargeDrawer.list = res.data.records;
    rechargeDrawer.total = res.data.total;
    rechargeDrawer.loading = false;
  }).catch(() => {
    rechargeDrawer.loading = false;
  });
}
function handleAddRecharge() {
  rechargeDialog.form = {
    cardId: rechargeDrawer.cardId,
    rechargeAmount: undefined,
    rechargeDate: new Date().toISOString().slice(0,19).replace('T', ' '),
    remark: ''
  };
  rechargeDialog.visible = true;
  if (rechargeFormRef.value) {
    rechargeFormRef.value.clearValidate();
  }
}
function submitRechargeForm() {
  rechargeFormRef.value.validate(valid => {
    if (valid) {
      addRecharge(rechargeDialog.form).then(res => {
        ElMessage.success('充值成功');
        rechargeDialog.visible = false;
        getRechargeList();
        getList(); // æ›´æ–°ä¸»åˆ—表金额
        getStats();
      });
    }
  });
}
function handleDeleteRecharge(row) {
  ElMessageBox.confirm('确认删除该笔充值记录吗?(删除后余额将自动扣回)', '提示', { type: 'warning' }).then(() => {
    return delRecharge(row.id);
  }).then(() => {
    ElMessage.success('删除成功');
    getRechargeList();
    getList();
    getStats();
  });
}
// --- å……值管理逻辑结束 ---
</script>
src/views/inventoryManagement/issueManagement/index.vue
@@ -22,8 +22,8 @@
        <el-button @click="handleOut">导出</el-button>
        <!-- <el-button type="danger" plain @click="handleDelete">删除</el-button> -->
      </div>
    </div>
    <div class="table_list">
    </div>    <div class="table_list">
      <el-table :data="tableData" border v-loading="tableLoading" @selection-change="handleSelectionChange"
        :expand-row-keys="expandedRowKeys" :row-key="row => row.id" show-summary style="width: 100%"
        :summary-method="summarizeMainTable" height="calc(100vh - 18.5em)">
src/views/inventoryManagement/vehicleFuelManagement/index.vue
@@ -1,281 +1,179 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">车辆编号:</span>
        <el-input
          v-model="searchForm.vehicleCode"
          style="width: 200px"
          placeholder="请输入车辆编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">加油时间:</span>
        <el-date-picker
          v-model="searchForm.dateRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
          @change="handleQuery"
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      <div style="flex: 1; padding-right: 20px;">
        <el-row :gutter="16" style="margin-bottom: 16px;">
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">所属部门:</span>
            <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" check-strictly clearable filterable style="flex: 1;" />
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">加油车辆:</span>
            <el-select v-model="queryParams.vehicleId" placeholder="按车辆筛选" clearable filterable style="flex: 1;">
              <el-option v-for="item in vehicleList" :key="item.id" :label="item.plateNumber" :value="item.id" />
            </el-select>
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">加油卡:</span>
            <el-select v-model="queryParams.fuelCardId" placeholder="请选择加油卡" clearable style="flex: 1;">
              <el-option v-for="item in fuelCardList" :key="item.id" :label="item.cardNumber + ' - ' + item.issuer" :value="item.id" />
            </el-select>
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">状态:</span>
            <el-select v-model="queryParams.status" placeholder="状态筛选" clearable style="flex: 1;">
              <el-option label="草稿" value="0" />
              <el-option label="已提交" value="1" />
            </el-select>
          </el-col>
        </el-row>
        <el-row :gutter="16">
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">加油时间:</span>
            <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始" end-placeholder="结束" value-format="YYYY-MM-DD" clearable style="flex: 1; width: 100%;" />
          </el-col>
          <el-col :span="18" style="display: flex; align-items: center;">
            <el-button type="primary" @click="handleQuery">搜索</el-button>
            <el-button @click="resetQuery">重置</el-button>
          </el-col>
        </el-row>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å¢žåŠ æ²¹è®°å½•
        </el-button>
      <div style="display: flex; align-items: flex-start; height: 100%;">
        <el-button type="primary" icon="Plus" @click="handleAdd">新增加油记录</el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        v-loading="loading"
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        height="calc(100vh - 22em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="tableRowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="130"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="fuelDate"
          label="加油日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="gunNo"
          label="油枪号"
          width="90"
          align="center"
        />
        <el-table-column
          prop="amount"
          label="金额(元)"
          width="100"
          align="right"
        >
        <el-table-column prop="fuelDate" label="加油日期" width="120" />
        <el-table-column prop="deptName" label="所属部门" min-width="150" show-overflow-tooltip />
        <el-table-column prop="plateNumber" label="加油车辆(牌照)" width="140" />
        <el-table-column prop="cardNumber" label="加油卡号" min-width="150" />
        <el-table-column prop="amount" label="金额(元)" width="100" align="right" />
        <el-table-column prop="liters" label="升数(L)" width="100" align="right" />
        <el-table-column prop="startMileage" label="起始里程(km)" width="120" align="right" />
        <el-table-column prop="endMileage" label="结束里程(km)" width="120" align="right" />
        <el-table-column prop="invoiceNo" label="发票号码" width="140" />
        <el-table-column prop="status" label="状态" width="100" align="center">
          <template #default="scope">
            <span>{{ scope.row.amount?.toFixed(2) }}</span>
            <el-tag v-if="scope.row.status === '0'" type="info">草稿</el-tag>
            <el-tag v-else-if="scope.row.status === '1'" type="success">已提交</el-tag>
            <el-tag v-else type="info">草稿</el-tag>
          </template>
        </el-table-column>
        <el-table-column
          prop="liters"
          label="升数(L)"
          width="90"
          align="right"
        >
        <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
        <el-table-column label="操作" fixed="right" width="200" align="center">
          <template #default="scope">
            <span>{{ scope.row.liters?.toFixed(2) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="startMileage"
          label="起始里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="endMileage"
          label="结束里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="distance"
          label="行驶里程(km)"
          width="120"
          align="right"
        />
        <el-table-column
          prop="fuelConsumption"
          label="油耗(L/100km)"
          width="130"
          align="center"
        >
          <template #default="scope">
            <span
              :style="scope.row.isAbnormal ? 'color:#F56C6C;font-weight:600;' : ''"
            >
              {{ scope.row.fuelConsumption != null ? scope.row.fuelConsumption.toFixed(2) : '-' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column
          prop="avgConsumption"
          label="车辆平均油耗"
          width="130"
          align="center"
        >
          <template #default="scope">
            <span>
              {{ scope.row.avgConsumption != null ? scope.row.avgConsumption.toFixed(2) : '-' }}
            </span>
          </template>
        </el-table-column>
        <el-table-column label="异常预警" width="100" align="center">
          <template #default="scope">
            <el-tag v-if="scope.row.isAbnormal" type="danger" size="small">
              å¼‚常
            </el-tag>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="140" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
            <el-button v-if="scope.row.status === '0' || !scope.row.status" type="success" link size="small" @click="handleSubmitRow(scope.row)">提交</el-button>
            <el-button v-if="scope.row.status === '0' || !scope.row.status" type="primary" link size="small" @click="handleUpdate(scope.row)">编辑</el-button>
            <el-button v-if="scope.row.status === '0' || !scope.row.status" type="danger" link size="small" @click="handleDelete(scope.row)">删除</el-button>
            <el-button v-if="scope.row.status === '1'" type="warning" link size="small" @click="handleRevokeRow(scope.row)">撤回</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="640px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
        label-position="right"
      >
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </div>
<!-- å¼¹çª— -->
    <el-dialog :title="title" v-model="open" width="640px" append-to-body>
      <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input
                v-model="form.vehicleCode"
                placeholder="请输入车辆编号"
            <el-form-item label="加油车辆" prop="vehicleId">
              <el-select v-model="form.vehicleId" placeholder="请选择车辆(牌照)" style="width: 100%" filterable>
                <el-option
                  v-for="item in vehicleList"
                  :key="item.id"
                  :label="item.plateNumber"
                  :value="item.id"
              />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
              <el-input
                v-model="form.plateNumber"
                placeholder="请输入车牌号码"
            <el-form-item label="关联加油卡" prop="fuelCardId">
              <el-select v-model="form.fuelCardId" placeholder="请选择加油卡" style="width: 100%" filterable>
                <el-option
                  v-for="item in availableFuelCardList"
                  :key="item.id"
                  :label="item.cardNumber + ' - ' + item.issuer"
                  :value="item.id"
              />
              </el-select>
              <div v-if="form.fuelCardId" style="font-size: 12px; color: #999; margin-top: 5px;">
                å½“前余额: {{ getFuelCardBalance(form.fuelCardId) }} å…ƒ
              </div>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="加油日期:" prop="fuelDate">
            <el-form-item label="加油日期" prop="fuelDate">
              <el-date-picker
                v-model="form.fuelDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择加油日期"
                placeholder="请选择日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="油枪号:" prop="gunNo">
              <el-input
                v-model="form.gunNo"
                placeholder="请输入油枪号"
              />
            <el-form-item label="发票号码" prop="invoiceNo">
              <el-input v-model="form.invoiceNo" placeholder="请输入发票号码" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="金额(元):" prop="amount">
              <el-input-number
                v-model="form.amount"
                :min="0"
                :step="0.01"
                :precision="2"
                placeholder="请输入金额"
                style="width: 100%"
              />
            <el-form-item label="金额(元)" prop="amount">
              <el-input-number v-model="form.amount" :min="0" :precision="2" :step="100" style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="升数(L):" prop="liters">
              <el-input-number
                v-model="form.liters"
                :min="0"
                :step="0.01"
                :precision="2"
                placeholder="请输入升数"
                style="width: 100%"
              />
            <el-form-item label="升数(L)" prop="liters">
              <el-input-number v-model="form.liters" :min="0" :precision="2" :step="10" style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="起始里程(km):" prop="startMileage">
              <el-input-number
                v-model="form.startMileage"
                :min="0"
                :step="1"
                placeholder="请输入起始里程"
                style="width: 100%"
              />
            <el-form-item label="起始里程(km)" prop="startMileage">
              <el-input-number v-model="form.startMileage" :min="0" :precision="2" :step="100" style="width: 100%" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="结束里程(km):" prop="endMileage">
              <el-input-number
                v-model="form.endMileage"
                :min="0"
                :step="1"
                placeholder="请输入结束里程"
                style="width: 100%"
              />
            <el-form-item label="结束里程(km)" prop="endMileage">
              <el-input-number v-model="form.endMileage" :min="0" :precision="2" :step="100" style="width: 100%" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
@@ -283,274 +181,229 @@
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { ref, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import request from '@/utils/request';
import {
  listFuelRecord,
  addFuelRecord,
  updateFuelRecord,
  delFuelRecord,
  listVehicleInfo,
  listAllFuelCard,
  submitFuelRecord,
  revokeFuelRecord
} from '@/api/inventoryManagement/vehicle';
// æ¨¡æ‹ŸåŠ æ²¹è®°å½•æ•°æ®
const rawRecords = ref([
  {
    id: 1,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2024-12-01",
    gunNo: "01",
    amount: 500,
    liters: 70,
    startMileage: 12000,
    endMileage: 12600,
  },
  {
    id: 2,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2024-12-15",
    gunNo: "02",
    amount: 520,
    liters: 72,
    startMileage: 12600,
    endMileage: 13250,
  },
  {
    id: 3,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    fuelDate: "2024-12-05",
    gunNo: "03",
    amount: 430,
    liters: 60,
    startMileage: 8000,
    endMileage: 8520,
  },
  {
    id: 4,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    fuelDate: "2024-12-20",
    gunNo: "01",
    amount: 450,
    liters: 63,
    startMileage: 8520,
    endMileage: 9000,
  },
  {
    id: 5,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    fuelDate: "2025-01-05",
    gunNo: "01",
    amount: 700,
    liters: 90,
    startMileage: 13250,
    endMileage: 13600, // æ˜Žæ˜¾å¼‚常油耗
  },
]);
const loading = ref(true);
const tableData = ref([]);
const total = ref(0);
const open = ref(false);
const title = ref("");
const formRef = ref(null);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  vehicleCode: "",
  dateRange: [],
const vehicleList = ref([]);
const fuelCardList = ref([]);
const deptOptions = ref([]);
const dateRange = ref([]);
const availableFuelCardList = computed(() => {
  return fuelCardList.value.filter(item => item.status !== 'CANCELLED');
});
// è¡¨æ ¼æ•°æ®ï¼ˆåŒ…含计算字段)
const tableData = ref([]);
const queryParams = reactive({
  current: 1,
  size: 10,
  deptId: undefined,
  vehicleId: undefined,
  fuelCardId: undefined,
  beginDate: undefined,
  endDate: undefined
});
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新增加油记录");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  vehicleCode: "",
  plateNumber: "",
  fuelDate: "",
  gunNo: "",
  amount: null,
  liters: null,
  startMileage: null,
  endMileage: null,
  id: undefined,
  vehicleId: undefined,
  fuelCardId: undefined,
  fuelDate: undefined,
  amount: undefined,
  liters: undefined,
  startMileage: undefined,
  endMileage: undefined,
  invoiceNo: undefined,
  remark: undefined
});
const rules = {
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  fuelDate: [{ required: true, message: "请选择加油日期", trigger: "change" }],
  gunNo: [{ required: true, message: "请输入油枪号", trigger: "blur" }],
  amount: [{ required: true, message: "请输入金额", trigger: "blur" }],
  liters: [{ required: true, message: "请输入升数", trigger: "blur" }],
  startMileage: [{ required: true, message: "请输入起始里程", trigger: "blur" }],
  endMileage: [{ required: true, message: "请输入结束里程", trigger: "blur" }],
  vehicleId: [{ required: true, message: "加油车辆不能为空", trigger: "change" }],
  fuelCardId: [{ required: true, message: "加油卡不能为空", trigger: "change" }],
  fuelDate: [{ required: true, message: "加油日期不能为空", trigger: "change" }],
  amount: [{ required: true, message: "金额不能为空", trigger: "blur" }]
};
// é‡æ–°è®¡ç®—油耗、平均油耗和异常预警
const recomputeTable = () => {
  const records = rawRecords.value;
  // 1. å…ˆæŒ‰è½¦è¾†ç»Ÿè®¡å¹³å‡æ²¹è€—
  const stats = {};
  records.forEach((r) => {
    const distance = r.endMileage - r.startMileage;
    if (distance <= 0 || !r.liters) return;
    const cons = (r.liters / distance) * 100; // L/100km
    if (!stats[r.vehicleCode]) {
      stats[r.vehicleCode] = { totalCons: 0, count: 0 };
    }
    stats[r.vehicleCode].totalCons += cons;
    stats[r.vehicleCode].count += 1;
function getDictData() {
  request({url: '/system/user/deptTree', method: 'get'}).then(res => {
    deptOptions.value = res.data || [];
  });
  const avgMap = {};
  Object.keys(stats).forEach((key) => {
    avgMap[key] = stats[key].totalCons / stats[key].count;
  listVehicleInfo({current: 1, size: 1000}).then(res => {
    vehicleList.value = res.data.records;
  });
  // 2. æŒ‰ç­›é€‰æ¡ä»¶è¿‡æ»¤å¹¶è¡¥å……计算字段
  const filtered = records
    .filter((r) => {
      if (
        searchForm.vehicleCode &&
        !r.vehicleCode.includes(searchForm.vehicleCode.trim())
      ) {
        return false;
      }
      if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
        const [start, end] = searchForm.dateRange;
        if (r.fuelDate < start || r.fuelDate > end) {
          return false;
        }
      }
      return true;
    })
    .map((r) => {
      const distance = r.endMileage - r.startMileage;
      const fuelConsumption =
        distance > 0 && r.liters
          ? (r.liters / distance) * 100
          : null;
      const avgConsumption =
        avgMap[r.vehicleCode] != null ? avgMap[r.vehicleCode] : null;
      const isAbnormal =
        avgConsumption != null &&
        fuelConsumption != null &&
        fuelConsumption > avgConsumption * 1.2;
      return {
        ...r,
        distance,
        fuelConsumption,
        avgConsumption,
        isAbnormal,
      };
  listAllFuelCard().then(res => {
    fuelCardList.value = res.data;
    });
  tableData.value = filtered;
};
// æŸ¥è¯¢
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.vehicleCode = "";
  searchForm.dateRange = [];
  recomputeTable();
};
// è¡Œæ ·å¼ï¼ˆå¼‚常高亮)
const tableRowClassName = ({ row }) => {
  if (row.isAbnormal) {
    return "row-abnormal";
  }
  return "";
};
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增加油记录";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    fuelDate: "",
    gunNo: "",
    amount: null,
    liters: null,
    startMileage: null,
    endMileage: null,
function getFuelCardBalance(id) {
  const c = fuelCardList.value.find(item => item.id === id);
  return c ? c.currentBalance : 0;
}
const statTotalAmount = ref(0);
const statTotalLiters = ref(0);
function getStats() {
  const query = { ...queryParams, current: 1, size: 10000 };
  listFuelRecord(query).then(res => {
    const arr = res.data.records || [];
    statTotalAmount.value = arr.reduce((sum, item) => sum + (item.amount || 0), 0);
    statTotalLiters.value = arr.reduce((sum, item) => sum + (item.liters || 0), 0);
  });
  dialogVisible.value = true;
};
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑加油记录";
  isEdit.value = true;
  Object.assign(form, row);
  dialogVisible.value = true;
};
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (form.endMileage <= form.startMileage) {
      ElMessage.warning("结束里程必须大于起始里程");
      return;
    }
    if (isEdit.value) {
      const index = rawRecords.value.findIndex((r) => r.id === form.id);
      if (index !== -1) {
        rawRecords.value[index] = { ...form };
      }
      ElMessage.success("加油记录已更新");
function getList() {
  loading.value = true;
  if (dateRange.value && dateRange.value.length === 2) {
    queryParams.beginDate = dateRange.value[0];
    queryParams.endDate = dateRange.value[1];
    } else {
      const newId = rawRecords.value.length
        ? Math.max(...rawRecords.value.map((r) => r.id)) + 1
        : 1;
      rawRecords.value.push({ ...form, id: newId });
      ElMessage.success("加油记录已新增");
    queryParams.beginDate = undefined;
    queryParams.endDate = undefined;
    }
    dialogVisible.value = false;
    recomputeTable();
  listFuelRecord(queryParams).then(response => {
    tableData.value = response.data.records;
    total.value = response.data.total;
    loading.value = false;
    getStats();
  }).catch(() => {
    loading.value = false;
    getStats();
  });
};
}
const handleCancel = () => {
  dialogVisible.value = false;
};
function handleQuery() {
  queryParams.current = 1;
  getList();
}
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该加油记录?", "删除提示", {
    confirmButtonText: "确认",
function resetQuery() {
  queryParams.deptId = undefined;
  queryParams.vehicleId = undefined;
  queryParams.fuelCardId = undefined;
  dateRange.value = [];
  handleQuery();
}
function reset() {
  Object.assign(form, {
    id: undefined,
    vehicleId: undefined,
    fuelCardId: undefined,
    fuelDate: undefined,
    amount: undefined,
    liters: undefined,
    startMileage: undefined,
    endMileage: undefined,
    invoiceNo: undefined,
    remark: undefined
  });
  if (formRef.value) formRef.value.resetFields();
}
function handleAdd() {
  reset();
  open.value = true;
  title.value = "新增加油记录";
}
function handleUpdate(row) {
  reset();
  Object.assign(form, row);
  open.value = true;
  title.value = "修改加油记录";
}
function submitForm() {
  formRef.value.validate(valid => {
    if (valid) {
      if (!form.id) {
        const bal = getFuelCardBalance(form.fuelCardId);
        if (bal < form.amount) {
          ElMessage.warning("该加油卡余额不足!当前余额: " + bal);
        }
      }
      if (form.id != null) {
        updateFuelRecord(form).then(response => {
          ElMessage.success("修改成功");
          open.value = false;
          getList();
          getDictData();
        });
      } else {
        addFuelRecord(form).then(response => {
          ElMessage.success("新增成功");
          open.value = false;
          getList();
          getDictData();
        });
      }
    }
  });
}
function handleDelete(row) {
  ElMessageBox.confirm('是否确认删除该草稿记录?', "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawRecords.value = rawRecords.value.filter((r) => r.id !== row.id);
      recomputeTable();
    type: "warning"
  }).then(function() {
    return delFuelRecord(row.id);
  }).then(() => {
    getList();
    getDictData();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
  });
}
function cancel() {
  open.value = false;
  reset();
}
onMounted(() => {
  recomputeTable();
  getDictData();
  getList();
});
function handleSubmitRow(row) {
  ElMessageBox.confirm('确认提交该笔加油记录吗?提交后将从加油卡中扣除余额', '提示', { type: 'warning' }).then(() => {
    return submitFuelRecord(row.id);
  }).then(() => {
    ElMessage.success('提交成功');
    getList();
    getStats();
  }).catch(() => {});
}
function handleRevokeRow(row) {
  ElMessageBox.confirm('确认撤回该笔加油记录吗?撤回后将退还加油卡余额', '提示', { type: 'warning' }).then(() => {
    return revokeFuelRecord(row.id);
  }).then(() => {
    ElMessage.success('撤回成功');
    getList();
    getStats();
  }).catch(() => {});
}
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
:deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/inventoryManagement/vehicleManagement/index.vue
@@ -1,268 +1,130 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">车牌号码:</span>
        <el-input
          v-model="searchForm.plateNumber"
          style="width: 180px"
          placeholder="请输入车牌号码"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">车辆类型:</span>
        <el-select
          v-model="searchForm.vehicleType"
          style="width: 160px"
          placeholder="请选择车辆类型"
          clearable
        >
          <el-option
            v-for="item in vehicleTypeOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
      <div style="flex: 1; padding-right: 20px;">
        <el-row :gutter="16">
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">车牌号码:</span>
            <el-input v-model="queryParams.plateNumber" placeholder="请输入车牌号码" clearable style="flex: 1;" @keyup.enter.native="handleQuery" />
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">所属部门:</span>
            <el-tree-select v-model="queryParams.deptId" :data="deptOptions" :props="{ value: 'id', label: 'label', children: 'children' }" value-key="id" placeholder="请选择部门" check-strictly clearable filterable style="flex: 1;" />
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <span class="search_title" style="white-space: nowrap;">状态:</span>
            <el-select v-model="queryParams.vehicleStatus" placeholder="请选择状态" clearable style="flex: 1;">
              <el-option label="在用" value="NORMAL" />
              <el-option label="闲置" value="IDLE" />
              <el-option label="ç»´ä¿®" value="REPAIR" />
        </el-select>
        <span class="search_title ml10">所属部门:</span>
        <el-select
          v-model="searchForm.department"
          style="width: 160px"
          placeholder="请选择所属部门"
          clearable
        >
          <el-option
            v-for="item in departmentOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">状态:</span>
        <el-select
          v-model="searchForm.status"
          style="width: 140px"
          placeholder="请选择状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">归档状态:</span>
        <el-select
          v-model="searchForm.archived"
          style="width: 140px"
          placeholder="请选择归档状态"
          clearable
        >
          <el-option label="未归档" value="false" />
          <el-option label="已归档" value="true" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
          </el-col>
          <el-col :span="6" style="display: flex; align-items: center;">
            <el-button type="primary" @click="handleQuery">搜索</el-button>
            <el-button @click="resetQuery">重置</el-button>
          </el-col>
        </el-row>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">新增车辆</el-button>
        <el-button type="primary" icon="Plus" @click="handleAdd">新增车辆</el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        v-loading="loading"
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        height="calc(100vh - 22em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="vehicleCode"
          label="车辆编号"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          prop="plateNumber"
          label="车牌号码"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="vehicleType"
          label="车辆类型"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="department"
          label="所属部门"
          width="140"
          show-overflow-tooltip
        />
        <el-table-column
          prop="purchaseDate"
          label="购置日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseNumber"
          label="行驶证编号"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseIssueDate"
          label="发证日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column
          prop="licenseExpireDate"
          label="到期日期"
          width="120"
          show-overflow-tooltip
        />
        <el-table-column label="状态" width="100" align="center">
        <el-table-column prop="plateNumber" label="车牌号码" min-width="120" show-overflow-tooltip />
        <el-table-column prop="deptName" label="所属部门" min-width="150" show-overflow-tooltip />
        <el-table-column prop="userName" label="责任人" min-width="120" />
        <el-table-column prop="propertyOwnership" label="产权所有" min-width="120" show-overflow-tooltip />
        <el-table-column label="年检到期日期" min-width="140">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            <span>{{ scope.row.inspectionExpireDate }}</span>
            <el-tag v-if="isExpired(scope.row.inspectionExpireDate)" type="danger" size="small" style="margin-left:5px;">已超期</el-tag>
            <el-tag v-else-if="isExpiringSoon(scope.row.inspectionExpireDate)" type="warning" size="small" style="margin-left:5px;">即将到期</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="保险到期日期" min-width="140">
          <template #default="scope">
            <span>{{ scope.row.insuranceExpireDate }}</span>
            <el-tag v-if="isExpired(scope.row.insuranceExpireDate)" type="danger" size="small" style="margin-left:5px;">已超期</el-tag>
            <el-tag v-else-if="isExpiringSoon(scope.row.insuranceExpireDate)" type="warning" size="small" style="margin-left:5px;">即将到期</el-tag>
          </template>
        </el-table-column>
        <el-table-column label="状态" min-width="100" align="center">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.vehicleStatus)">
              {{ statusLabel(scope.row.vehicleStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="归档状态" width="100" align="center">
          <template #default="scope">
            <el-tag :type="scope.row.archived ? 'info' : 'success'">
              {{ scope.row.archived ? '已归档' : '未归档' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
        <el-table-column label="操作" fixed="right" width="220" align="center">
          <template #default="scope">
            <el-button
              type="primary"
              link
              size="small"
              @click="openEdit(scope.row)"
            >
              ç¼–辑
            </el-button>
            <el-button
              type="warning"
              link
              size="small"
              :disabled="scope.row.archived"
              @click="archiveRow(scope.row)"
            >
              å½’æ¡£
            </el-button>
            <el-button
              type="danger"
              link
              size="small"
              @click="removeRow(scope.row)"
            >
              åˆ é™¤
            </el-button>
            <el-button type="success" link size="small" @click="handleCost(scope.row)">费用管理</el-button>
            <el-button type="primary" link size="small" @click="handleUpdate(scope.row)">编辑</el-button>
            <el-button type="danger" link size="small" @click="handleDelete(scope.row)">删除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="600px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
        label-position="right"
      >
      <!-- åˆ†é¡µ -->
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </div>
<!-- æ–°å¢ž/编辑车辆弹窗 -->
    <el-dialog :title="title" v-model="open" width="600px" append-to-body>
      <el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆编号:" prop="vehicleCode">
              <el-input v-model="form.vehicleCode" placeholder="请输入车辆编号" />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="车牌号码:" prop="plateNumber">
            <el-form-item label="车牌号码" prop="plateNumber">
              <el-input v-model="form.plateNumber" placeholder="请输入车牌号码" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="车辆类型:" prop="vehicleType">
              <el-select
                v-model="form.vehicleType"
                placeholder="请选择车辆类型"
                clearable
              >
                <el-option
                  v-for="item in vehicleTypeOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="所属部门:" prop="department">
              <el-select
                v-model="form.department"
                placeholder="请选择所属部门"
                clearable
              >
                <el-option
                  v-for="item in departmentOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            <el-form-item label="产权所有" prop="propertyOwnership">
              <el-input v-model="form.propertyOwnership" placeholder="如:自有,租赁等" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="购置日期:" prop="purchaseDate">
              <el-date-picker
                v-model="form.purchaseDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择购置日期"
            <el-form-item label="所属部门" prop="deptId">
              <el-tree-select
                v-model="form.deptId"
                :data="deptOptions"
                :props="{ value: 'id', label: 'label', children: 'children' }"
                value-key="id"
                placeholder="请选择部门"
                check-strictly
                clearable
                filterable
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="状态:" prop="status">
              <el-select v-model="form.status" placeholder="请选择状态">
            <el-form-item label="责任人" prop="userId">
              <el-select v-model="form.userId" placeholder="请选择责任人" style="width: 100%" filterable>
                <el-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                  v-for="item in userList"
                  :key="item.userId"
                  :label="item.userId + ' - ' + (item.nickName || item.userName)"
                  :value="item.userId"
                />
              </el-select>
            </el-form-item>
@@ -270,312 +132,389 @@
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="行驶证编号:" prop="licenseNumber">
              <el-input
                v-model="form.licenseNumber"
                placeholder="请输入行驶证编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="发证日期:" prop="licenseIssueDate">
              <el-date-picker
                v-model="form.licenseIssueDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择发证日期"
                style="width: 100%"
              />
            <el-form-item label="状态" prop="vehicleStatus">
              <el-select v-model="form.vehicleStatus" placeholder="请选择状态" style="width: 100%">
                <el-option label="在用" value="NORMAL" />
                <el-option label="闲置" value="IDLE" />
                <el-option label="ç»´ä¿®" value="REPAIR" />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="到期日期:" prop="licenseExpireDate">
            <el-form-item label="年检到期" prop="inspectionExpireDate">
              <el-date-picker
                v-model="form.licenseExpireDate"
                v-model="form.inspectionExpireDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择到期日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="保险到期" prop="insuranceExpireDate">
              <el-date-picker
                v-model="form.insuranceExpireDate"
                type="date"
                value-format="YYYY-MM-DD"
                placeholder="请选择到期日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
          <el-button type="primary" @click="submitForm">ç¡® å®š</el-button>
          <el-button @click="cancel">取 æ¶ˆ</el-button>
        </div>
      </template>
    </el-dialog>
    <!-- è´¹ç”¨ç®¡ç†æŠ½å±‰ -->
    <el-drawer v-model="costDrawer" :title="'费用管理 - ' + currentVehiclePlate" size="50%">
      <div style="padding: 20px;">
        <div style="margin-bottom: 20px;">
          <el-button type="primary" @click="handleAddCost">新增记录</el-button>
        </div>
        <el-table v-loading="costLoading" :data="costList" border>
          <el-table-column label="费用类型" prop="costType">
            <template #default="scope">
              {{ costTypeLabel(scope.row.costType) }}
            </template>
          </el-table-column>
          <el-table-column label="金额(元)" prop="amount" align="right" />
          <el-table-column label="发生日期" prop="costDate" />
          <el-table-column label="备注" prop="remark" show-overflow-tooltip />
          <el-table-column label="操作" width="100" align="center">
            <template #default="scope">
              <el-button type="danger" link size="small" @click="handleDeleteCost(scope.row)">
                åˆ é™¤
              </el-button>
            </template>
          </el-table-column>
        </el-table>
        <pagination
          v-show="costTotal > 0"
          :total="costTotal"
          v-model:page="costQuery.current"
          v-model:limit="costQuery.size"
          @pagination="getCostList"
        />
      </div>
    </el-drawer>
    <!-- æ–°å¢žè´¹ç”¨å¼¹çª— -->
    <el-dialog title="新增费用" v-model="costOpen" width="400px" append-to-body>
      <el-form ref="costFormRef" :model="costForm" :rules="costRules" label-width="100px">
        <el-form-item label="费用类型" prop="costType">
          <el-select v-model="costForm.costType" placeholder="请选择" style="width:100%">
            <el-option label="保养" value="MAINTENANCE" />
            <el-option label="年检" value="INSPECTION" />
            <el-option label="保险" value="INSURANCE" />
            <el-option label="ç»´ä¿®" value="REPAIR" />
          </el-select>
        </el-form-item>
        <el-form-item label="金额(元)" prop="amount">
          <el-input-number v-model="costForm.amount" :precision="2" :min="0" style="width:100%" />
        </el-form-item>
        <el-form-item label="发生日期" prop="costDate">
          <el-date-picker v-model="costForm.costDate" type="date" value-format="YYYY-MM-DD" style="width:100%" />
        </el-form-item>
        <el-form-item label="备注" prop="remark">
          <el-input v-model="costForm.remark" type="textarea" />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button type="primary" @click="submitCostForm">ç¡® å®š</el-button>
        <el-button @click="costOpen = false">取 æ¶ˆ</el-button>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { ref, reactive, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { listVehicleInfo, getWarningList, addVehicleInfo, updateVehicleInfo, delVehicleInfo } from '@/api/inventoryManagement/vehicle';
import request from '@/utils/request';
// æ¨¡æ‹Ÿè½¦è¾†åŸºç¡€æ•°æ®
const allVehicles = ref([
  {
    id: 1,
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    vehicleType: "厢式货车",
    department: "物流一部",
    purchaseDate: "2022-03-15",
    licenseNumber: "4401-2022-0001",
    licenseIssueDate: "2022-03-10",
    licenseExpireDate: "2026-03-10",
    status: "在用",
    archived: false,
  },
  {
    id: 2,
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    vehicleType: "冷藏车",
    department: "物流二部",
    purchaseDate: "2021-08-01",
    licenseNumber: "4401-2021-0123",
    licenseIssueDate: "2021-07-28",
    licenseExpireDate: "2025-07-28",
    status: "ç»´ä¿®",
    archived: false,
  },
  {
    id: 3,
    vehicleCode: "CL-202403",
    plateNumber: "粤C11223",
    vehicleType: "牵引车",
    department: "项目运输部",
    purchaseDate: "2020-05-20",
    licenseNumber: "4401-2020-0456",
    licenseIssueDate: "2020-05-18",
    licenseExpireDate: "2024-05-18",
    status: "闲置",
    archived: false,
  },
  {
    id: 4,
    vehicleCode: "CL-202404",
    plateNumber: "粤D33445",
    vehicleType: "厢式货车",
    department: "资产管理部",
    purchaseDate: "2019-11-11",
    licenseNumber: "4401-2019-0789",
    licenseIssueDate: "2019-11-08",
    licenseExpireDate: "2023-11-08",
    status: "在用",
    archived: true,
  },
]);
const loading = ref(true);
const tableData = ref([]);
const total = ref(0);
const open = ref(false);
const title = ref("");
const formRef = ref(null);
// ä¸‹æ‹‰æžšä¸¾
const vehicleTypeOptions = [
  { label: "厢式货车", value: "厢式货车" },
  { label: "冷藏车", value: "冷藏车" },
  { label: "牵引车", value: "牵引车" },
  { label: "其他", value: "其他" },
];
const deptOptions = ref([]);
const userList = ref([]);
const departmentOptions = [
  { label: "物流一部", value: "物流一部" },
  { label: "物流二部", value: "物流二部" },
  { label: "项目运输部", value: "项目运输部" },
  { label: "资产管理部", value: "资产管理部" },
];
const statusOptions = [
  { label: "在用", value: "在用" },
  { label: "闲置", value: "闲置" },
  { label: "ç»´ä¿®", value: "ç»´ä¿®" },
];
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  plateNumber: "",
  vehicleType: "",
  department: "",
  status: "",
  archived: "",
const queryParams = reactive({
  current: 1,
  size: 10,
  plateNumber: undefined,
  deptId: undefined,
  vehicleStatus: undefined
});
// è¡¨æ ¼æ•°æ®
const tableData = ref([...allVehicles.value]);
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新增车辆");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  vehicleCode: "",
  plateNumber: "",
  vehicleType: "",
  department: "",
  purchaseDate: "",
  licenseNumber: "",
  licenseIssueDate: "",
  licenseExpireDate: "",
  status: "在用",
  archived: false,
  id: undefined,
  plateNumber: undefined,
  propertyOwnership: undefined,
  deptId: undefined,
  userId: undefined,
  vehicleStatus: 'NORMAL',
  inspectionExpireDate: undefined,
  insuranceExpireDate: undefined,
  remark: undefined
});
const rules = {
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  vehicleType: [{ required: true, message: "请选择车辆类型", trigger: "change" }],
  department: [{ required: true, message: "请选择所属部门", trigger: "change" }],
  purchaseDate: [{ required: true, message: "请选择购置日期", trigger: "change" }],
  status: [{ required: true, message: "请选择状态", trigger: "change" }],
  licenseNumber: [{ required: true, message: "请输入行驶证编号", trigger: "blur" }],
  licenseIssueDate: [{ required: true, message: "请选择发证日期", trigger: "change" }],
  plateNumber: [{ required: true, message: "车牌号码不能为空", trigger: "blur" }],
  deptId: [{ required: true, message: "所属部门不能为空", trigger: "change" }],
  vehicleStatus: [{ required: true, message: "状态不能为空", trigger: "change" }]
};
// æŸ¥è¯¢
const handleQuery = () => {
  tableData.value = allVehicles.value.filter((item) => {
    if (
      searchForm.plateNumber &&
      !item.plateNumber.includes(searchForm.plateNumber.trim())
    ) {
      return false;
    }
    if (searchForm.vehicleType && item.vehicleType !== searchForm.vehicleType) {
      return false;
    }
    if (searchForm.department && item.department !== searchForm.department) {
      return false;
    }
    if (searchForm.status && item.status !== searchForm.status) {
      return false;
    }
    if (searchForm.archived !== "") {
      const targetArchived = searchForm.archived === "true";
      if (item.archived !== targetArchived) return false;
    }
    return true;
// å­—典获取
function getDicts() {
  request({url: '/system/user/deptTree', method: 'get'}).then(res => {
    deptOptions.value = res.data || [];
  });
};
  request({url: '/system/user/list', method: 'get'}).then(res => {
    userList.value = res.rows || res.data || [];
  });
}
const resetSearch = () => {
  searchForm.plateNumber = "";
  searchForm.vehicleType = "";
  searchForm.department = "";
  searchForm.status = "";
  searchForm.archived = "";
const statTotal = ref(0);
const statNormal = ref(0);
const statIdle = ref(0);
const statRepair = ref(0);
function getStats() {
  listVehicleInfo({ current: 1, size: 10000 }).then(res => {
    const arr = res.data.records || [];
    statTotal.value = arr.length;
    statNormal.value = arr.filter(i => i.vehicleStatus === 'NORMAL').length;
    statIdle.value = arr.filter(i => i.vehicleStatus === 'IDLE').length;
    statRepair.value = arr.filter(i => i.vehicleStatus === 'REPAIR').length;
  });
}
function getList() {
  loading.value = true;
  listVehicleInfo(queryParams).then(response => {
    tableData.value = response.data.records;
    total.value = response.data.total;
    loading.value = false;
  }).catch(() => {
    loading.value = false;
  });
}
function handleWarningList() {
  loading.value = true;
  getWarningList().then(response => {
    tableData.value = response.data;
    total.value = response.data.length;
    loading.value = false;
    ElMessage.info("已展示30天内到期的预警车辆");
  }).catch(() => {
    loading.value = false;
  });
}
function handleQuery() {
  queryParams.current = 1;
  getList();
  getStats();
}
function resetQuery() {
  queryParams.plateNumber = undefined;
  queryParams.deptId = undefined;
  queryParams.vehicleStatus = undefined;
  handleQuery();
};
}
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增车辆";
  isEdit.value = false;
function reset() {
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    vehicleType: "",
    department: "",
    purchaseDate: "",
    licenseInfo: "",
    status: "在用",
    archived: false,
    id: undefined,
    plateNumber: undefined,
    propertyOwnership: undefined,
    deptId: undefined,
    userId: undefined,
    vehicleStatus: 'NORMAL',
    inspectionExpireDate: undefined,
    insuranceExpireDate: undefined,
    remark: undefined
  });
  dialogVisible.value = true;
};
  if (formRef.value) formRef.value.resetFields();
}
// ç¼–辑
const openEdit = (row) => {
  dialogTitle.value = "编辑车辆";
  isEdit.value = true;
function handleAdd() {
  reset();
  open.value = true;
  title.value = "新增车辆";
}
function handleUpdate(row) {
  reset();
  Object.assign(form, row);
  dialogVisible.value = true;
};
  open.value = true;
  title.value = "修改车辆";
}
// ä¿å­˜
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid) return;
    if (isEdit.value) {
      const index = allVehicles.value.findIndex((v) => v.id === form.id);
      if (index !== -1) {
        allVehicles.value[index] = { ...form };
      }
      ElMessage.success("车辆信息已更新");
    } else {
      const newId = allVehicles.value.length
        ? Math.max(...allVehicles.value.map((v) => v.id)) + 1
        : 1;
      allVehicles.value.push({ ...form, id: newId });
      ElMessage.success("车辆信息已新增");
    }
    dialogVisible.value = false;
    handleQuery();
function submitForm() {
  formRef.value.validate(valid => {
    if (valid) {
      if (form.id != null) {
        updateVehicleInfo(form).then(response => {
          ElMessage.success("修改成功");
          open.value = false;
          getList();
  getStats();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// å½’æ¡£
const archiveRow = (row) => {
  if (row.archived) return;
  ElMessageBox.confirm(
    "是否确认将该车辆归档?归档后仅保留查询,不再参与运输任务分配。",
    "归档提示",
    {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
      } else {
        addVehicleInfo(form).then(response => {
          ElMessage.success("新增成功");
          open.value = false;
          getList();
  getStats();
        });
    }
  )
    .then(() => {
      row.archived = true;
      if (row.status === "在用") {
        row.status = "闲置";
      }
      ElMessage.success("车辆已归档");
      handleQuery();
    })
    .catch(() => {});
};
  });
}
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该车辆基础信息?", "删除提示", {
    confirmButtonText: "确认",
function handleDelete(row) {
  ElMessageBox.confirm('是否确认删除该车辆数据?', "警告", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      allVehicles.value = allVehicles.value.filter((v) => v.id !== row.id);
      handleQuery();
    type: "warning"
  }).then(function() {
    return delVehicleInfo(row.id);
  }).then(() => {
    getList();
  getStats();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
  });
}
// çŠ¶æ€æ ·å¼
function cancel() {
  open.value = false;
  reset();
}
function isExpired(dateStr) {
  if (!dateStr) return false;
  const targetDate = new Date(dateStr).getTime();
  const now = new Date().getTime();
  return targetDate < now;
}
function isExpiringSoon(dateStr) {
  if (!dateStr) return false;
  const targetDate = new Date(dateStr).getTime();
  const now = new Date().getTime();
  const diffDays = (targetDate - now) / (1000 * 3600 * 24);
  return diffDays >= 0 && diffDays <= 30;
}
const statusTagType = (status) => {
  if (status === "在用") return "success";
  if (status === "闲置") return "info";
  if (status === "ç»´ä¿®") return "warning";
  if (status === "NORMAL") return "success";
  if (status === "IDLE") return "info";
  if (status === "REPAIR") return "warning";
  return "default";
};
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
const statusLabel = (status) => {
  if (status === "NORMAL") return "在用";
  if (status === "IDLE") return "闲置";
  if (status === "REPAIR") return "ç»´ä¿®";
  return status;
};
// è´¹ç”¨ç®¡ç†é€»è¾‘
const costDrawer = ref(false);
const currentVehicleId = ref(null);
const currentVehiclePlate = ref("");
const costLoading = ref(false);
const costList = ref([]);
const costTotal = ref(0);
const costQuery = reactive({ current: 1, size: 10, vehicleId: null });
function costTypeLabel(type) {
  const map = { MAINTENANCE: '保养', INSPECTION: '年检', INSURANCE: '保险', REPAIR: 'ç»´ä¿®' };
  return map[type] || type;
}
</style>
function handleCost(row) {
  currentVehicleId.value = row.id;
  currentVehiclePlate.value = row.plateNumber;
  costQuery.vehicleId = row.id;
  costDrawer.value = true;
  getCostList();
}
function getCostList() {
  costLoading.value = true;
  request({ url: '/vehicle/cost/listPage', method: 'get', params: costQuery }).then(res => {
    costList.value = res.data.records;
    costTotal.value = res.data.total;
    costLoading.value = false;
  });
}
const costOpen = ref(false);
const costFormRef = ref(null);
const costForm = reactive({ vehicleId: null, costType: undefined, amount: undefined, costDate: undefined, remark: undefined });
const costRules = {
  costType: [{ required: true, message: "费用类型不能为空", trigger: "change" }],
  amount: [{ required: true, message: "金额不能为空", trigger: "blur" }],
  costDate: [{ required: true, message: "日期不能为空", trigger: "change" }]
};
function handleAddCost() {
  costForm.vehicleId = currentVehicleId.value;
  costForm.costType = undefined;
  costForm.amount = undefined;
  costForm.costDate = undefined;
  costForm.remark = undefined;
  if(costFormRef.value) costFormRef.value.resetFields();
  costOpen.value = true;
}
function submitCostForm() {
  costFormRef.value.validate(valid => {
    if (valid) {
      request({ url: '/vehicle/cost', method: 'post', data: costForm }).then(res => {
        ElMessage.success("添加成功");
        costOpen.value = false;
        getCostList();
      });
    }
  });
}
function handleDeleteCost(row) {
  ElMessageBox.confirm('确定删除该费用记录?', '提示', { type: 'warning' }).then(() => {
    request({ url: '/vehicle/cost/' + row.id, method: 'delete' }).then(() => {
      ElMessage.success("删除成功");
      getCostList();
    });
  });
}
onMounted(() => {
  getDicts();
  getList();
  getStats();
});
</script>
src/views/officeProcessAutomation/ApproveManage/approve-list/components/ApproveDetailPanel.vue
@@ -145,6 +145,22 @@
          </div>
        </div>
      </template>
      <!-- çŠ¶æ€å˜æ›´å®¡æ‰¹è¯¦æƒ… -->
      <template v-else-if="row.businessType === 19">
        <div class="device-status-detail">
          <el-divider content-position="left">设备状态变更详情</el-divider>
          <el-descriptions :column="2" border>
            <el-descriptions-item label="设备名称">{{ detailData.deviceName || "--" }}</el-descriptions-item>
            <el-descriptions-item label="目标状态">
              <el-tag :type="detailData.targetStatus === 1 ? 'success' : 'danger'">
                {{ detailData.targetStatusName || "--" }}
              </el-tag>
            </el-descriptions-item>
            <el-descriptions-item label="变更原因" :span="2">{{ detailData.reason || "--" }}</el-descriptions-item>
          </el-descriptions>
        </div>
      </template>
    </div>
    <div v-if="attachmentList.length"
         class="detail-block">
@@ -199,9 +215,9 @@
  const formResolved = computed(() => resolveInstanceFormFields(props.row));
  // æ˜¯å¦ä¸ºç‰¹æ®Šå®¡æ‰¹ç±»åž‹ï¼ˆé‡‡è´­ã€å‘货、报价)
  // æ˜¯å¦ä¸ºç‰¹æ®Šå®¡æ‰¹ç±»åž‹ï¼ˆé‡‡è´­ã€å‘货、报价、设备状态变更)
  const isSpecialApprovalType = computed(() => {
    return [5, 7, 6].includes(props.row.businessType);
    return [5, 7, 6, 19].includes(props.row.businessType);
  });
  // è¯¦æƒ…数据(直接使用传入的 detail-data å‚数)
src/views/officeProcessAutomation/ApproveManage/approve-list/useApproveList.js
@@ -327,6 +327,19 @@
      }
    }
    
    // è®¾å¤‡çŠ¶æ€å˜æ›´å®¡æ‰¹
    else if (row.businessType === 19) {
      if (row.formConfig) {
        try {
          detailData.value = JSON.parse(row.formConfig);
        } catch(e) {
          detailData.value = {};
        }
      } else {
        detailData.value = {};
      }
    }
    // å…¶ä»–审批类型
    else {
      detailData.value = {};
@@ -375,6 +388,19 @@
      }
    }
    // è®¾å¤‡çŠ¶æ€å˜æ›´å®¡æ‰¹
    else if (row.businessType === 19) {
      if (row.formConfig) {
        try {
          detailData.value = JSON.parse(row.formConfig);
        } catch(e) {
          detailData.value = {};
        }
      } else {
        detailData.value = {};
      }
    }
    // å…¶ä»–审批类型
    else {
      detailData.value = {};
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/components/DetailPanel.vue
@@ -11,6 +11,8 @@
    <el-descriptions-item label="员工姓名">{{ row.employeeName || row.applicantName || "—" }}</el-descriptions-item>
    <el-descriptions-item label="报销原因" :span="2">{{ row.reimburseReason || "—" }}</el-descriptions-item>
    <el-descriptions-item label="报销金额">{{ row.applyAmount != null ? `${row.applyAmount} å…ƒ` : "—" }}</el-descriptions-item>
    <el-descriptions-item label="部门">{{ row.applicantDeptName || row.deptName || "—" }}</el-descriptions-item>
    <el-descriptions-item label="付款方">{{ row.payer || "—" }}</el-descriptions-item>
    <el-descriptions-item label="收款人">{{ row.payee || "—" }}</el-descriptions-item>
    <el-descriptions-item label="收款账号">{{ row.payeeAccount || "—" }}</el-descriptions-item>
    <el-descriptions-item label="开户支行">{{ row.bankBranch || "—" }}</el-descriptions-item>
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/costReimburseUtils.js
@@ -201,6 +201,7 @@
    reimburseReason: "",
    applyAmount: undefined,
    payee: "",
    payer: "",
    payeeAccount: "",
    bankBranch: "",
    expenseDetails: [],
@@ -295,6 +296,7 @@
    reimburseReason: raw.reimburseReason ?? "",
    applyAmount,
    payee: raw.payee ?? "",
    payer: raw.payer ?? "",
    payeeAccount: raw.payeeAccount ?? "",
    bankBranch: raw.bankBranch ?? "",
    expenseDetails,
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/index.vue
@@ -12,12 +12,43 @@
          :prefix-icon="Search"
          @keyup.enter="handleQuery"
        />
        <span class="search_title" style="margin-left: 10px;">部门:</span>
        <el-tree-select
          v-model="searchForm.deptId"
          :data="deptOptions"
          :props="{ value: 'id', label: 'label', children: 'children' }"
          value-key="id"
          placeholder="选择部门"
          check-strictly
          clearable
          style="width: 180px"
        />
        <span class="search_title" style="margin-left: 10px;">项目:</span>
        <el-select v-model="searchForm.expenseCategory" clearable placeholder="选择项目" style="width: 150px">
          <el-option
            v-for="opt in EXPENSE_CATEGORY_OPTIONS"
            :key="opt.value"
            :label="opt.label"
            :value="opt.value"
          />
        </el-select>
        <span class="search_title" style="margin-left: 10px;">申请时间:</span>
        <el-date-picker
          v-model="searchForm.applyTimeRange"
          type="daterange"
          range-separator="-"
          start-placeholder="开始"
          end-placeholder="结束"
          value-format="YYYY-MM-DD"
          clearable
          style="width: 240px"
        />
        <el-button type="primary" style="margin-left: 10px" @click="handleQuery">搜索</el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div class="search_actions">
<!--        <el-button type="success" plain @click="handleImportClick">导入</el-button>-->
<!--        <el-button type="warning" plain @click="handleExport">导出</el-button>-->
        <el-button type="warning" plain @click="handleExport">导出</el-button>
        <el-button type="primary" @click="openFormDialog('add')">新增费用报销</el-button>
      </div>
    </div>
@@ -79,12 +110,21 @@
        <el-card class="form-section" shadow="never">
          <template #header><span class="card-header-title">基本信息</span></template>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="员工编号">
                <el-input v-model="form.employeeNo" readonly placeholder="选择员工后自动带出" />
            <el-col :span="8">
              <el-form-item label="归属部门" prop="deptId">
                <el-tree-select
                  v-model="form.deptId"
                  :data="deptOptions"
                  :props="{ value: 'id', label: 'label', children: 'children' }"
                  value-key="id"
                  placeholder="请选择部门"
                  check-strictly
                  style="width: 100%"
                  @change="onDeptChange"
                />
              </el-form-item>
            </el-col>
            <el-col :span="12">
            <el-col :span="8">
              <el-form-item label="员工姓名" prop="applicantId">
                <el-select
                  v-model="form.applicantId"
@@ -105,6 +145,11 @@
                    :value="u.userId"
                  />
                </el-select>
              </el-form-item>
            </el-col>
            <el-col :span="8">
              <el-form-item label="员工编号">
                <el-input v-model="form.employeeNo" readonly placeholder="选择员工后自动带出" />
              </el-form-item>
            </el-col>
          </el-row>
@@ -248,17 +293,22 @@
        <el-card class="form-section" shadow="never">
          <template #header><span class="card-header-title">收款信息</span></template>
          <el-row :gutter="20">
            <el-col :span="8">
            <el-col :span="6">
              <el-form-item label="付款方" prop="payer">
                <el-input v-model="form.payer" placeholder="请输入付款方" maxlength="50" />
              </el-form-item>
            </el-col>
            <el-col :span="6">
              <el-form-item label="收款人" prop="payee">
                <el-input v-model="form.payee" placeholder="请输入收款人" maxlength="50" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
            <el-col :span="6">
              <el-form-item label="收款账号" prop="payeeAccount">
                <el-input v-model="form.payeeAccount" placeholder="银行卡号" maxlength="30" />
              </el-form-item>
            </el-col>
            <el-col :span="8">
            <el-col :span="6">
              <el-form-item label="开户支行" prop="bankBranch">
                <el-input v-model="form.bankBranch" placeholder="开户支行全称" maxlength="100" />
              </el-form-item>
@@ -404,6 +454,7 @@
  approveOpinion,
  applicantFormSearchLoading,
  applicantFormOptions,
  deptOptions,
  flowUserOptions,
  detailTotalAmount,
  approvalRuleHint,
@@ -413,6 +464,7 @@
  remoteSearchApplicantForm,
  userSelectLabel,
  onApplicantChange,
  onDeptChange,
  onExpenseCategoryChange,
  applyTemplate,
  onDetailAmountChange,
src/views/officeProcessAutomation/ReimburseManage/cost-reimburse/useCostReimburse.js
@@ -7,7 +7,7 @@
  persistFinReimbursement,
} from "@/api/officeProcessAutomation/finReimbursement.js";
import { ElMessageBox } from "element-plus";
import { userListNoPageByTenantId } from "@/api/system/user.js";
import { userListNoPageByTenantId, deptTreeSelect } from "@/api/system/user.js";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, ref } from "vue";
import {
  buildCostReimbursementSaveDto,
@@ -72,6 +72,9 @@
  const searchForm = reactive({
    applicantKeyword: "",
    deptId: undefined,
    expenseCategory: "",
    applyTimeRange: [],
  });
  const tableLoading = ref(false);
  const page = reactive({ current: 1, size: 10, total: 0 });
@@ -79,6 +82,7 @@
  const allUsersCache = ref([]);
  const applicantFormSearchLoading = ref(false);
  const applicantFormOptions = ref([]);
  const deptOptions = ref([]);
  const formRef = ref();
  const form = reactive(createEmptyForm());
  const formDialog = reactive({ visible: false, title: "", mode: "add", readonly: false });
@@ -151,6 +155,9 @@
    { label: "报销单号", prop: "reimburseNo", width: 150 },
    { label: "申请人编号", prop: "applicantNo", width: 110 },
    { label: "申请人", prop: "applicantName", minWidth: 90 },
    { label: "部门", prop: "applicantDeptName", minWidth: 100 },
    { label: "费用类型", prop: "expenseCategory", minWidth: 90, formatData: (v) => expenseCategoryLabel(v) },
    { label: "付款方", prop: "payer", minWidth: 100 },
    { label: "报销金额(元)", prop: "applyAmount", width: 110 },
    { label: "报销原因", prop: "reimburseReason", minWidth: 160, showOverflowTooltip: true },
    { label: "申请时间", prop: "applyTime", width: 165 },
@@ -194,6 +201,7 @@
  ]);
  const formRules = {
    deptId: [{ required: true, message: "请选择归属部门", trigger: "change" }],
    applicantId: [{ required: true, message: "请选择员工", trigger: "change" }],
    expenseCategory: [{ required: true, message: "请选择费用类型", trigger: "change" }],
    reimburseReason: [{ required: true, message: "请填写报销原因", trigger: "blur" }],
@@ -222,21 +230,39 @@
  async function loadUserPool() {
    try {
      allUsersCache.value = unwrapArray(await userListNoPageByTenantId());
      allUsersCache.value = unwrapArray(await userListNoPageByTenantId({}));
    } catch {
      allUsersCache.value = [];
    }
  }
  function formatDeptOptions(list) {
    return (list || []).map((d) => ({
      ...d,
      rawLabel: d.label,
      label: d.label, // éƒ¨é—¨ä¸‹æ‹‰ä¸è¦æ˜¾ç¤ºid
      children: d.children && d.children.length ? formatDeptOptions(d.children) : undefined,
    }));
  }
  async function loadDeptOptions() {
    try {
      const res = await deptTreeSelect();
      deptOptions.value = formatDeptOptions(res.data || []);
    } catch {
      deptOptions.value = [];
    }
  }
  function userSelectLabel(u) {
    const nick = u.nickName || "";
    const name = u.userName || "";
    if (nick && name && nick !== name) return `${nick}(${name})`;
    return nick || name || `用户${u.userId ?? u.id ?? ""}`;
    const empNo = employeeNoFromUser(u);
    return `${nick || name || "用户"} - ${empNo}`;
  }
  function userById(id) {
    return allUsersCache.value.find((u) => String(u.userId ?? u.id) === String(id));
    return applicantFormOptions.value.find((u) => String(u.userId ?? u.id) === String(id));
  }
  function employeeNoFromUser(u) {
@@ -244,22 +270,21 @@
    return u.userName ?? u.userCode ?? u.jobNumber ?? u.workNo ?? (u.userId != null ? String(u.userId) : "");
  }
  function filterUsersByQuery(query) {
    const list = allUsersCache.value.filter(isActiveUser);
    const q = (query || "").trim().toLowerCase();
    if (!q) return [...list];
    return list.filter((u) => {
      const nick = (u.nickName || "").toLowerCase();
      const uname = (u.userName || "").toLowerCase();
      return nick.includes(q) || uname.includes(q);
    });
  }
  async function remoteSearchApplicantForm(query) {
    applicantFormSearchLoading.value = true;
    try {
      if (!allUsersCache.value.length) await loadUserPool();
      applicantFormOptions.value = filterUsersByQuery(query);
      const q = (query || "").trim();
      const params = {};
      if (form.deptId) {
        params.deptId = form.deptId;
      }
      if (q) {
        params.nickName = q; // You can pass nickName to backend
      }
      const res = await userListNoPageByTenantId(params);
      applicantFormOptions.value = unwrapArray(res).filter(isActiveUser);
    } catch {
      applicantFormOptions.value = [];
    } finally {
      applicantFormSearchLoading.value = false;
    }
@@ -271,11 +296,46 @@
      form.employeeName = u.nickName || u.userName || "";
      form.employeeNo = employeeNoFromUser(u);
      form.payee = form.payee || form.employeeName;
      form.deptId = String(u.deptId ?? u.sysDeptId ?? "");
      form.deptName = u.dept?.deptName ?? u.deptName ?? "";
      const userDeptId = String(u.deptId ?? u.sysDeptId ?? "");
      if (userDeptId && form.deptId !== userDeptId) {
        form.deptId = userDeptId;
        const name = findDeptLabel(deptOptions.value, userDeptId);
        form.deptName = name || (u.dept?.deptName ?? u.deptName ?? "");
      }
    } else {
      form.employeeName = "";
      form.employeeNo = "";
    }
  }
  function findDeptLabel(nodes, id) {
    if (!nodes) return null;
    for (const node of nodes) {
      if (String(node.id) === String(id)) return node.rawLabel || node.label;
      if (node.children) {
        const hit = findDeptLabel(node.children, id);
        if (hit) return hit;
      }
    }
    return null;
  }
  function onDeptChange(deptId) {
    if (deptId) {
      const name = findDeptLabel(deptOptions.value, deptId);
      if (name) form.deptName = name;
      remoteSearchApplicantForm("");
      if (form.applicantId) {
        const u = userById(form.applicantId);
        if (u && String(u.deptId ?? u.sysDeptId) !== String(deptId)) {
          form.applicantId = "";
          onApplicantChange("");
        }
      }
    } else {
      form.deptName = "";
      remoteSearchApplicantForm("");
    }
  }
@@ -431,17 +491,13 @@
        proxy?.$modal?.msgError?.("加载报销详情失败");
        return;
      }
      Object.assign(form, {
        ...JSON.parse(JSON.stringify(editRow)),
        reimbursementId: editRow.reimbursementId ?? editRow.id,
        attachmentList: JSON.parse(JSON.stringify(editRow.attachmentList || editRow.invoiceAttachments || [])),
        approvalFlowNodes: JSON.parse(JSON.stringify(editRow.approvalFlowNodes || [])),
        expenseDetails: JSON.parse(JSON.stringify(editRow.expenseDetails || [])),
      });
      Object.assign(form, buildCostReimbursementSaveDto(editRow));
      formDialog.title = "编辑费用报销";
      await remoteSearchApplicantForm("");
      const u = userById(editRow.applicantId);
      applicantFormOptions.value = u
        ? [u]
        : [{ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.employeeNo }];
      if (!u) {
        applicantFormOptions.value.unshift({ userId: editRow.applicantId, nickName: editRow.employeeName, userName: editRow.applicantCode });
      }
    } else {
      form.approvalFlowNodes = buildAutoApprovalFlow(0, "other");
      remoteSearchApplicantForm("");
@@ -532,15 +588,17 @@
  }
  function handleExport() {
    const data = allRows.value;
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = `费用报销导出_${dayjs().format("YYYYMMDDHHmmss")}.json`;
    a.click();
    URL.revokeObjectURL(url);
    proxy?.$modal?.msgSuccess?.(`已导出 ${data.length} æ¡`);
    const params = buildFinReimbursementListParams({
      page: { current: 1, size: 99999 }, // Just so the function doesn't crash, backend export ignores pagination anyway
      searchForm,
      reimbursementType: FIN_REIMBURSEMENT_TYPE.COST,
    });
    // Remove pagination parameters since export doesn't need them
    delete params.current;
    delete params.size;
    delete params["page.current"];
    delete params["page.size"];
    proxy.download("/finReimbursement/export", params, `费用报销导出_${dayjs().format("YYYYMMDDHHmmss")}.xlsx`);
  }
  function handleImportClick() {
@@ -572,6 +630,7 @@
  onMounted(async () => {
    loadUserPool();
    loadDeptOptions();
    await fetchList();
    const editPayload = consumeReimburseEditFromApprove();
    if (editPayload?.reimbursementId != null) {
@@ -603,6 +662,7 @@
    approveOpinion,
    applicantFormSearchLoading,
    applicantFormOptions,
    deptOptions,
    flowUserOptions,
    detailTotalAmount,
    approvalRuleHint,
@@ -612,6 +672,7 @@
    remoteSearchApplicantForm,
    userSelectLabel,
    onApplicantChange,
    onDeptChange,
    onExpenseCategoryChange,
    applyTemplate,
    onDetailAmountChange,
src/views/officeProcessAutomation/ReimburseManage/shared/finReimbursementMappers.js
@@ -173,18 +173,30 @@
  return out;
}
/** æ˜¯å¦å­˜åœ¨åˆ—表筛选条件(仅申请人) */
export function hasActiveReimbursementSearch(searchForm = {}) {
  return Boolean((searchForm?.applicantKeyword || "").trim());
export function expenseTypeToCategory(typeLabel) {
  const hit = EXPENSE_CATEGORY_OPTIONS.find((x) => x.label === typeLabel || x.value === typeLabel);
  return hit?.value || typeLabel || "";
}
/** æœåŠ¡ç«¯æœªç”Ÿæ•ˆæ—¶ï¼ŒæŒ‰ç”³è¯·äººåšå‰ç«¯å…œåº•ç­›é€‰ */
/** æ˜¯å¦å­˜åœ¨åˆ—表筛选条件 */
export function hasActiveReimbursementSearch(searchForm = {}) {
  return Boolean(
    (searchForm?.applicantKeyword || "").trim() ||
    searchForm?.deptId ||
    searchForm?.expenseCategory ||
    (searchForm?.applyTimeRange && searchForm.applyTimeRange.length === 2)
  );
}
/** æœåŠ¡ç«¯æœªç”Ÿæ•ˆæ—¶ï¼ŒæŒ‰æ¡ä»¶åšå‰ç«¯å…œåº•ç­›é€‰ */
export function filterReimbursementRowsBySearch(rows, searchForm = {}) {
  const list = Array.isArray(rows) ? rows : [];
  const kw = (searchForm?.applicantKeyword || "").trim().toLowerCase();
  if (!kw) return list;
  let result = list;
  return list.filter((row) => {
  const kw = (searchForm?.applicantKeyword || "").trim().toLowerCase();
  if (kw) {
    result = result.filter((row) => {
    const parts = [
      row.applicantName,
      row.employeeName,
@@ -196,6 +208,26 @@
      .map((v) => String(v).toLowerCase());
    return parts.some((p) => p.includes(kw));
  });
  }
  if (searchForm?.deptId) {
    result = result.filter(row => String(row.deptId) === String(searchForm.deptId));
  }
  if (searchForm?.expenseCategory) {
    result = result.filter(row => row.expenseCategory === searchForm.expenseCategory);
  }
  if (searchForm?.applyTimeRange && searchForm.applyTimeRange.length === 2) {
    const start = searchForm.applyTimeRange[0];
    const end = searchForm.applyTimeRange[1];
    result = result.filter(row => {
      const t = (row.applyTime || row.createTime || "").substring(0, 10);
      return t >= start && t <= end;
    });
  }
  return result;
}
/** æ‰å¹³åŒ–为 Spring GET å¯ç»‘定的 query(finReimbursementDto.xxx,勿用方括号) */
@@ -216,6 +248,10 @@
}) {
  const dto = {
    reimbursementType,
    deptId: searchForm.deptId || undefined,
    expenseType: searchForm.expenseCategory ? expenseCategoryToType(searchForm.expenseCategory) : undefined,
    createTimeStart: searchForm.applyTimeRange?.[0] || undefined,
    createTimeEnd: searchForm.applyTimeRange?.[1] || undefined,
    ...pickApplicantQuery(searchForm),
    ...(extraDto && typeof extraDto === "object" ? extraDto : {}),
  };
@@ -227,6 +263,12 @@
    "page.size": page.size,
    ...dto,
  };
  if (searchForm.applyTimeRange && searchForm.applyTimeRange.length === 2) {
    params["params[beginTime]"] = searchForm.applyTimeRange[0] + " 00:00:00";
    params["params[endTime]"] = searchForm.applyTimeRange[1] + " 23:59:59";
  }
  appendDotNotationQuery(params, "finReimbursementDto", dto);
  return params;
}
@@ -319,7 +361,8 @@
    applicantName: row.applicantName || "",
    employeeNo: row.applicantCode || "",
    employeeName: row.applicantName || "",
    applicantDeptName: row.applicantDeptName || "",
    applicantDeptName: row.deptName || "",
    deptId: row.deptId,
    reimburseReason: row.reason || "",
    travelStartTime: formatReimbursementDateTime(travel.startTime),
    travelEndTime: formatReimbursementDateTime(travel.endTime),
@@ -375,12 +418,14 @@
    applicantName: row.applicantName || "",
    employeeNo: row.applicantCode || "",
    employeeName: row.applicantName || "",
    applicantDeptName: row.applicantDeptName || "",
    applicantDeptName: row.deptName || "",
    deptId: row.deptId,
    reimburseReason: row.reason || "",
    expenseCategory: row.expenseType || "",
    expenseCategory: expenseTypeToCategory(row.expenseType) || "",
    applyAmount: row.applyAmount,
    applyTime: formatReimbursementDateTime(row.createTime),
    payee: row.payeeName || "",
    payer: row.payer || "",
    payeeAccount: row.payeeAccount || "",
    bankBranch: row.payeeBank || "",
    billStatus: row.billStatus,
@@ -846,6 +891,7 @@
    applyAmount,
    detailTotalAmount: detailTotal,
    payeeName: form.payee || "",
    payer: form.payer || "",
    payeeAccount: form.payeeAccount || "",
    payeeBank: form.bankBranch || form.payeeBank || "",
    billStatus: "IN_APPROVAL",
src/views/projectManagement/Management/components/ArchiveDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,175 @@
<template>
  <el-dialog
    title="文档归档"
    v-model="visible"
    width="650px"
    destroy-on-close
    append-to-body
  >
    <div class="archive-container" v-loading="loading">
      <el-empty v-if="!loading && (!archiveGroups || archiveGroups.length === 0)" description="暂无归档附件" />
      <el-collapse v-else v-model="activeNames">
        <el-collapse-item
          v-for="(group, index) in archiveGroups"
          :key="index"
          :title="`${group.moduleName} (${group.files ? group.files.length : 0})`"
          :name="index"
        >
          <div class="attachment-list">
            <div
              v-for="(file, fileIdx) in group.files"
              :key="fileIdx"
              class="attachment-item"
            >
              <el-icon class="file-icon"><Document /></el-icon>
              <a
                :href="getRealUrl(file.url)"
                target="_blank"
                class="file-link"
                :title="file.name"
              >
                {{ file.name }}
              </a>
              <el-button link type="primary" class="download-btn" @click="handleDownload(file.url, file.name)">下载</el-button>
            </div>
          </div>
        </el-collapse-item>
      </el-collapse>
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="visible = false">关闭</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue'
import { getProjectAttachments } from '@/api/projectManagement/project'
import { Document } from '@element-plus/icons-vue'
const props = defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },
  projectId: {
    type: [Number, String],
    default: null
  }
})
const emit = defineEmits(['update:modelValue'])
const visible = ref(false)
const loading = ref(false)
const archiveGroups = ref([])
const activeNames = ref([])
watch(() => props.modelValue, (val) => {
  visible.value = val
  if (val && props.projectId) {
    fetchAttachments()
  } else {
    archiveGroups.value = []
    activeNames.value = []
  }
})
watch(() => visible.value, (val) => {
  emit('update:modelValue', val)
})
const fetchAttachments = async () => {
  loading.value = true
  try {
    const res = await getProjectAttachments(props.projectId)
    if (res.code === 200) {
      archiveGroups.value = res.data || []
      // é»˜è®¤å±•开所有面板
      activeNames.value = archiveGroups.value.map((_, index) => index)
    }
  } catch (error) {
    console.error('获取附件失败', error)
  } finally {
    loading.value = false
  }
}
// èŽ·å–çœŸå®žåœ°å€ï¼ˆå¤„ç†å¯èƒ½çš„ç›¸å¯¹è·¯å¾„å‰ç¼€ï¼‰
const getRealUrl = (url) => {
  if (!url) return '#'
  if (url.startsWith('http://') || url.startsWith('https://')) {
    return url
  }
  return import.meta.env.VITE_APP_BASE_API + url
}
// æå–文件名(兼容旧逻辑)
const getFileName = (url) => {
  if (!url) return '未知文件'
  const parts = url.split('/')
  const name = parts[parts.length - 1]
  // å¤„理可能的时间戳前缀
  const nameParts = name.split('_')
  return nameParts.length > 1 ? nameParts.slice(1).join('_') : name
}
const handleDownload = (url, name) => {
  const realUrl = getRealUrl(url)
  const a = document.createElement('a')
  a.style.display = 'none'
  a.href = realUrl
  a.download = name || getFileName(url)
  document.body.appendChild(a)
  a.click()
  document.body.removeChild(a)
}
</script>
<style scoped>
.archive-container {
  min-height: 200px;
}
.attachment-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 5px 0;
}
.attachment-item {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  background-color: #f8f9fa;
  border-radius: 4px;
  transition: background-color 0.3s;
}
.attachment-item:hover {
  background-color: #f0f2f5;
}
.file-icon {
  font-size: 16px;
  color: #909399;
  margin-right: 8px;
}
.file-link {
  flex: 1;
  color: #606266;
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  margin-right: 15px;
}
.file-link:hover {
  color: #409EFF;
  text-decoration: underline;
}
.download-btn {
  margin-left: auto;
}
</style>
src/views/projectManagement/Management/components/ContractListDialog.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
<template>
  <el-dialog
    title="销售合同列表"
    v-model="visible"
    width="800px"
    append-to-body
    destroy-on-close
    @open="handleOpen"
  >
    <div v-loading="loading">
      <el-table :data="tableData" border style="width: 100%">
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="salesContractNo" label="销售合同编号" align="center" show-overflow-tooltip />
        <el-table-column prop="customerName" label="客户名称" align="center" show-overflow-tooltip />
      </el-table>
      <pagination
        v-show="total > 0"
        :total="total"
        v-model:page="queryParams.current"
        v-model:limit="queryParams.size"
        @pagination="getList"
      />
    </div>
    <template #footer>
      <div class="dialog-footer">
        <el-button @click="visible = false">关 é—­</el-button>
      </div>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, watch, reactive } from 'vue'
import { ledgerListPage } from '@/api/salesManagement/salesLedger'
const props = defineProps({
  modelValue: {
    type: Boolean,
    default: false
  },
  projectId: {
    type: [Number, String],
    default: null
  }
})
const emit = defineEmits(['update:modelValue'])
const visible = ref(false)
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const queryParams = reactive({
  current: 1,
  size: 10,
  projectId: undefined
})
watch(() => props.modelValue, (val) => {
  visible.value = val
})
watch(visible, (val) => {
  emit('update:modelValue', val)
})
function handleOpen() {
  queryParams.current = 1
  queryParams.projectId = props.projectId
  getList()
}
function getList() {
  if (!queryParams.projectId) {
    tableData.value = []
    total.value = 0
    return
  }
  loading.value = true
  ledgerListPage(queryParams).then(res => {
    const records = res?.data?.records || res?.records || res?.rows || []
    // å¦‚果一条记录也没有且在第一页以后,可能需要重置分页
    // å‰ç«¯è¦æ±‚显示“销售合同编号”和“客户名称”,这些字段在 salesLedgerListPage è¿”回的数据中存在
    tableData.value = records
    total.value = res?.data?.total || res?.total || 0
  }).catch(() => {
    tableData.value = []
    total.value = 0
  }).finally(() => {
    loading.value = false
  })
}
</script>
<style scoped>
.dialog-footer {
  text-align: right;
}
</style>
src/views/projectManagement/Management/index.vue
@@ -56,15 +56,21 @@
        <template #projectStage="{ row }">
          <dict-tag :options="plan_status" :value="row.projectStage" />
        </template>
        <template #salesContractCount="{ row }">
          <el-button link type="primary" @click="handleOpenContractDialog(row)">{{ row.salesContractCount || 0 }}</el-button>
        </template>
        <template #action="{ row }">
          <el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
          <el-button link type="primary" :loading="progressBtnLoadingId===row.id" @click="handleProgressReport(row)">进度汇报</el-button>
          <el-button link type="primary" @click="handleArchive(row)">文档归档</el-button>
          <el-button link type="primary" @click="handleDetail(row)">详情</el-button>
        </template>
      </PIMTable>
    </div>
    <FormDia ref="formDiaRef" @completed="getList" />
    <ArchiveDialog v-model="archiveVisible" :project-id="archiveProjectId" />
    <ContractListDialog v-model="contractVisible" :project-id="contractProjectId" />
    <ProgressReportDialog
      v-model="progressReportVisible"
      :project-id="progressProjectId"
@@ -82,6 +88,8 @@
import SearchPanel from '@/components/SearchPanel/index.vue'
import PIMTable from '@/components/PIMTable/PIMTable.vue'
import FormDia from './components/formDia.vue'
import ArchiveDialog from './components/ArchiveDialog.vue'
import ContractListDialog from './components/ContractListDialog.vue'
import ProgressReportDialog from '@/components/ProjectManagement/ProgressReportDialog.vue'
import {
  listProject,
@@ -111,6 +119,10 @@
const progressPlanNodes = ref([])
const progressDefaultPlanNodeId = ref(undefined)
const progressBtnLoadingId = ref(null)
const archiveVisible = ref(false)
const archiveProjectId = ref(null)
const contractVisible = ref(false)
const contractProjectId = ref(null)
const submitLoading = ref(false)
const auditLoading = ref(false)
const reverseAuditLoading = ref(false)
@@ -154,6 +166,7 @@
  { label: '立项日期', prop: 'setupDate', align: 'center', width: '120' },
  { label: '项目来源', prop: 'projectSource', align: 'center' },
  { label: '项目分类', prop: 'projectClassification', align: 'center' },
  { label: '销售合同总数', prop: 'salesContractCount', align: 'center', dataType: 'slot', slot: 'salesContractCount' },
  { label: '操作', prop: 'action', align: 'center', width: '250', dataType: 'slot', slot: 'action', fixed: 'right' }
]
@@ -188,6 +201,7 @@
        projectType: r.planName ?? r.projectType,
        projectSource: r.source ?? r.projectSource,
        projectClassification: r.departmentName ?? r.projectClassification,
        salesContractCount: r.salesContractCount,
        raw: r
      }))
      pagination.value.total = response?.total || response?.data?.total || 0
@@ -407,6 +421,18 @@
  formDiaRef.value?.openDialog({ operationType: 'edit', row })
}
function handleArchive(row) {
  if (!row?.id) return
  archiveProjectId.value = row.id
  archiveVisible.value = true
}
function handleOpenContractDialog(row) {
  if (!row?.id) return
  contractProjectId.value = row.id
  contractVisible.value = true
}
onMounted(() => {
  getList()
})
src/views/projectManagement/config/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
<template>
  <div class="app-container">
    <el-card class="box-card" shadow="never">
      <template #header>
        <div class="card-header">
          <span>业务参数设置</span>
        </div>
      </template>
      <el-form
        ref="configFormRef"
        :model="form"
        :rules="rules"
        label-width="240px"
        style="max-width: 600px; margin-top: 20px;"
      >
        <el-form-item label="合同原件约定回传提前提醒天数" prop="contractReturnReminderDays">
          <el-input-number
            v-model="form.contractReturnReminderDays"
            :min="1"
            :max="365"
            :step="1"
            placeholder="请输入天数"
            style="width: 200px"
          />
          <span style="margin-left: 10px; color: #999;">天</span>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">保存设置</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
    </el-card>
  </div>
</template>
<script setup name="ProjectConfig">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getConfig, updateConfig } from '@/api/projectManagement/config'
const configFormRef = ref(null)
const submitLoading = ref(false)
const originalData = ref({})
const form = reactive({
  id: undefined,
  contractReturnReminderDays: 3
})
const rules = {
  contractReturnReminderDays: [
    { required: true, message: '提前提醒天数不能为空', trigger: 'blur' }
  ]
}
const loadData = async () => {
  try {
    const res = await getConfig()
    if (res.code === 200 && res.data) {
      Object.assign(form, res.data)
      originalData.value = JSON.parse(JSON.stringify(res.data))
    }
  } catch (error) {
    console.error('获取配置失败:', error)
  }
}
const handleSubmit = async () => {
  if (!configFormRef.value) return
  await configFormRef.value.validate(async (valid) => {
    if (valid) {
      submitLoading.value = true
      try {
        const res = await updateConfig(form)
        if (res.code === 200) {
          ElMessage.success('配置更新成功')
          loadData()
        }
      } catch (error) {
        console.error('更新配置失败:', error)
      } finally {
        submitLoading.value = false
      }
    }
  })
}
const handleReset = () => {
  if (originalData.value.id) {
    Object.assign(form, originalData.value)
  } else {
    form.contractReturnReminderDays = 3
  }
}
onMounted(() => {
  loadData()
})
</script>
<style scoped>
.app-container {
  padding: 20px;
}
.box-card {
  min-height: calc(100vh - 124px);
}
</style>
src/views/qualityManagement/finalInspection/components/formDia.vue
@@ -169,13 +169,57 @@
          </el-col>
        </el-row>
      </el-form>
      <el-row class="mb8" v-if="!isViewMode">
        <el-button type="primary" plain icon="Plus" @click="handleAddRow">新增一行</el-button>
      </el-row>
      <PIMTable rowKey="id"
                :column="tableColumn"
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400">
        <template #parameterItemSlot="{ row }">
          <span v-if="isViewMode">{{ row.parameterItem }}</span>
          <el-input v-else v-model="row.parameterItem" placeholder="请输入" />
        </template>
        <template #parameterTypeSlot="{ row }">
          <span v-if="isViewMode">{{ row.parameterType === 1 ? '数字' : row.parameterType === 0 ? '文字' : '' }}</span>
          <el-select v-else v-model="row.parameterType" @change="handleTypeChange(row)">
            <el-option label="文字" :value="0" />
            <el-option label="数字" :value="1" />
          </el-select>
        </template>
        <template #maxValueSlot="{ row }">
          <span v-if="isViewMode">{{ row.maxValue }}</span>
          <el-input-number v-else v-model="row.maxValue" :controls="false" :disabled="row.parameterType !== 1" style="width: 100%" />
        </template>
        <template #minValueSlot="{ row }">
          <span v-if="isViewMode">{{ row.minValue }}</span>
          <el-input-number v-else v-model="row.minValue" :controls="false" :disabled="row.parameterType !== 1" style="width: 100%" />
        </template>
        <template #unitSlot="{ row }">
          <span v-if="isViewMode">{{ row.unit }}</span>
          <el-input v-else v-model="row.unit" />
        </template>
        <template #standardValueSlot="{ row }">
          <span v-if="isViewMode">{{ row.standardValue }}</span>
          <el-input v-else v-model="row.standardValue" :disabled="row.parameterType !== 0" />
        </template>
        <template #controlValueSlot="{ row }">
          <span v-if="isViewMode">{{ row.controlValue }}</span>
          <el-input v-else v-model="row.controlValue" />
        </template>
        <template #operationSlot="{ row }">
          <el-button v-if="!isViewMode" type="danger" link @click="removeRow(row)">删除</el-button>
        </template>
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
          <el-input-number v-if="row.parameterType === 1"
                           v-model="row.testValue"
                           :controls="false"
                           clearable
                           :disabled="isViewMode"
                           style="width: 100%" />
          <el-input v-else
                    v-model="row.testValue"
                    clearable
                    :disabled="isViewMode" />
        </template>
@@ -203,6 +247,7 @@
    computed,
    getCurrentInstance,
    nextTick,
    watch
  } from "vue";
  import { getOptions } from "@/api/procurementManagement/procurementLedger.js";
  import { modelList, productTreeList } from "@/api/basicData/product.js";
@@ -267,36 +312,121 @@
  });
  const supplierList = ref([]);
  const productOptions = ref([]);
  const tableColumn = ref([
  const tableColumn = computed(() => {
    const cols = [
    {
      label: "指标",
      prop: "parameterItem",
        dataType: "slot",
        slot: "parameterItemSlot"
      },
      {
        label: "参数类型",
        prop: "parameterType",
        dataType: "slot",
        slot: "parameterTypeSlot",
        width: "120"
      },
      {
        label: "最大值",
        prop: "maxValue",
        dataType: "slot",
        slot: "maxValueSlot",
        width: "100"
      },
      {
        label: "最小值",
        prop: "minValue",
        dataType: "slot",
        slot: "minValueSlot",
        width: "100"
    },
    {
      label: "单位",
      prop: "unit",
        dataType: "slot",
        slot: "unitSlot",
        width: "100"
    },
    {
      label: "标准值",
      prop: "standardValue",
        dataType: "slot",
        slot: "standardValueSlot"
    },
    {
      label: "内控值",
      prop: "controlValue",
        dataType: "slot",
        slot: "controlValueSlot"
    },
    {
      label: "检验值",
      prop: "testValue",
      dataType: "slot",
      slot: "slot",
    },
  ]);
      }
    ];
    if (!isViewMode.value) {
      cols.push({
        label: "操作",
        prop: "operation",
        dataType: "slot",
        slot: "operationSlot",
        width: "80",
        fixed: "right"
      });
    }
    return cols;
  });
  const tableData = ref([]);
  const tableLoading = ref(false);
  const userList = ref([]);
  const currentProductId = ref(0);
  const testStandardOptions = ref([]); // æŒ‡æ ‡é€‰æ‹©ä¸‹æ‹‰æ¡†æ•°æ®
  const modelOptions = ref([]);
  watch(() => tableData.value, (newVal) => {
    if (isViewMode.value) return;
    if (!newVal || newVal.length === 0) return;
    let passCount = 0;
    let failCount = 0;
    newVal.forEach(item => {
      let isPass = false;
      const val = item.testValue;
      if (val === null || val === undefined || val === '') {
        isPass = false;
      } else if (item.parameterType === 1) {
        const numVal = Number(val);
        const min = Number(item.minValue);
        const max = Number(item.maxValue);
        if (!isNaN(numVal) && numVal >= min && numVal <= max) {
          isPass = true;
        }
      } else {
        if (String(val) === String(item.standardValue)) {
          isPass = true;
        }
      }
      if (isPass) {
        passCount++;
      } else {
        failCount++;
      }
    });
    if (failCount === 0 && passCount > 0) {
      form.value.checkResult = "合格";
    } else if (passCount === 0 && failCount > 0) {
      form.value.checkResult = "不合格";
    } else {
      form.value.checkResult = "部分合格";
    }
  }, { deep: true });
  // æ‰“开弹框
  const openDialog = async (type, row) => {
@@ -533,6 +663,10 @@
          ...item,
          id: null,
        }));
        if (tableData.value.some(item => item.parameterType === null || item.parameterType === undefined)) {
          proxy.$modal.msgWarning("参数类型未配置,请配置指标维护模板");
        }
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
@@ -547,6 +681,36 @@
      tableData.value = res.data;
    });
  };
  const handleAddRow = () => {
    tableData.value.push({
      parameterItem: '',
      parameterType: 0,
      maxValue: undefined,
      minValue: undefined,
      unit: '',
      standardValue: '',
      controlValue: '',
      testValue: ''
    });
  };
  const removeRow = (row) => {
    const index = tableData.value.indexOf(row);
    if (index !== -1) {
      tableData.value.splice(index, 1);
    }
  };
  const handleTypeChange = (row) => {
    if (row.parameterType === 0) {
      row.maxValue = undefined;
      row.minValue = undefined;
    } else {
      row.standardValue = '';
    }
    row.testValue = '';
  };
  // å…³é—­å¼¹æ¡†
  const closeDia = () => {
    proxy.resetForm("formRef");
src/views/qualityManagement/finalInspection/index.vue
@@ -128,6 +128,7 @@
  import { ElMessageBox } from "element-plus";
  import {
    downloadQualityInspect,
    downloadQualityInspectPdf,
    qualityInspectDel,
    qualityInspectListPage,
    qualityInspectUpdate,
@@ -315,9 +316,17 @@
        },
        {
          name: "下载",
          type: "text",
          clickFun: row => {
          type: "dropdown",
          dropdownItems: [
            { label: "Word格式", command: "word" },
            { label: "PDF格式", command: "pdf" }
          ],
          commandFun: (command, row) => {
            if (command === 'word') {
            downLoadFile(row);
            } else if (command === 'pdf') {
              downLoadPdfFile(row);
            }
          },
        },
      ],
@@ -500,7 +509,23 @@
      const link = document.createElement("a");
      link.href = downloadUrl;
      link.download = "原材料检验报告.docx";
      link.download = "出厂检验报告.docx";
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      window.URL.revokeObjectURL(downloadUrl);
    });
  };
  const downLoadPdfFile = row => {
    downloadQualityInspectPdf({ id: row.id }).then(blobData => {
      const blob = new Blob([blobData], { type: "application/pdf" });
      const downloadUrl = window.URL.createObjectURL(blob);
      const link = document.createElement("a");
      link.href = downloadUrl;
      link.download = "出厂检验报告.pdf";
      document.body.appendChild(link);
      link.click();
src/views/qualityManagement/metricMaintenance/ParamFormDialog.vue
@@ -20,9 +20,23 @@
      <el-form-item label="单位" prop="unit">
        <el-input v-model="form.unit" placeholder="请输入单位" />
      </el-form-item>
      <el-form-item label="标准值" prop="standardValue">
      <el-form-item label="参数类型" prop="parameterType">
        <el-radio-group v-model="form.parameterType">
          <el-radio :label="0">文字</el-radio>
          <el-radio :label="1">数字</el-radio>
        </el-radio-group>
      </el-form-item>
      <el-form-item label="标准值" prop="standardValue" v-if="form.parameterType === 0">
        <el-input v-model="form.standardValue" placeholder="请输入标准值" />
      </el-form-item>
      <template v-if="form.parameterType === 1">
        <el-form-item label="最大值" prop="maxValue">
          <el-input-number v-model="form.maxValue" :controls="false" placeholder="请输入最大值" style="width: 100%" />
        </el-form-item>
        <el-form-item label="最小值" prop="minValue">
          <el-input-number v-model="form.minValue" :controls="false" placeholder="请输入最小值" style="width: 100%" />
        </el-form-item>
      </template>
      <el-form-item label="内控值" prop="controlValue">
        <el-input v-model="form.controlValue" placeholder="请输入内控值" />
      </el-form-item>
@@ -52,10 +66,44 @@
const formRef = ref(null)
const rules = {
  parameterItem: [{ required: true, message: '请输入参数项', trigger: 'blur' }],
  unit: [{ required: true, message: '请输入单位', trigger: 'blur' }]
const validateMaxValue = (rule, value, callback) => {
  if (value === undefined || value === null || value === '') {
    callback(new Error('请输入最大值'))
  } else if (props.form.minValue !== undefined && props.form.minValue !== null && props.form.minValue !== '' && Number(value) < Number(props.form.minValue)) {
    callback(new Error('最大值不能小于最小值'))
  } else {
    if (props.form.minValue !== undefined && props.form.minValue !== null && props.form.minValue !== '') {
      formRef.value?.clearValidate('minValue')
}
    callback()
  }
}
const validateMinValue = (rule, value, callback) => {
  if (value === undefined || value === null || value === '') {
    callback(new Error('请输入最小值'))
  } else if (props.form.maxValue !== undefined && props.form.maxValue !== null && props.form.maxValue !== '' && Number(value) > Number(props.form.maxValue)) {
    callback(new Error('最小值不能大于最大值'))
  } else {
    if (props.form.maxValue !== undefined && props.form.maxValue !== null && props.form.maxValue !== '') {
      formRef.value?.clearValidate('maxValue')
    }
    callback()
  }
}
const rules = computed(() => {
  const baseRules = {
    parameterItem: [{ required: true, message: '请输入参数项', trigger: 'blur' }],
    unit: [{ required: true, message: '请输入单位', trigger: 'blur' }],
    parameterType: [{ required: true, message: '请选择参数类型', trigger: 'change' }]
  };
  if (props.form.parameterType === 1) {
    baseRules.maxValue = [{ required: true, validator: validateMaxValue, trigger: 'blur' }];
    baseRules.minValue = [{ required: true, validator: validateMinValue, trigger: 'blur' }];
  }
  return baseRules;
})
const computedTitle = computed(() => (props.operationType === 'edit' ? '编辑标准参数' : '新增标准参数'))
src/views/qualityManagement/metricMaintenance/index.vue
@@ -122,7 +122,14 @@
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="parameterItem" label="参数项" min-width="120" />
        <el-table-column prop="unit" label="单位" width="80" />
        <el-table-column prop="parameterType" label="参数类型" width="100">
          <template #default="{ row }">
            {{ row.parameterType === 1 ? '数字' : (row.parameterType === 0 ? '文字' : '') }}
          </template>
        </el-table-column>
        <el-table-column prop="standardValue" label="标准值" min-width="120" />
        <el-table-column prop="maxValue" label="最大值" min-width="100" />
        <el-table-column prop="minValue" label="最小值" min-width="100" />
        <el-table-column prop="controlValue" label="内控值" min-width="120" />
        <el-table-column prop="defaultValue" label="默认值" min-width="120" />
        <el-table-column label="操作" width="140" fixed="right" align="center">
@@ -266,7 +273,10 @@
  id: undefined,
  parameterItem: '',
  unit: '',
  parameterType: 0,
  standardValue: '',
  maxValue: null,
  minValue: null,
  controlValue: '',
  defaultValue: ''
})
@@ -514,7 +524,10 @@
      id: undefined,
      parameterItem: '',
      unit: '',
      parameterType: 0,
      standardValue: '',
      maxValue: null,
      minValue: null,
      controlValue: '',
      defaultValue: ''
    })
src/views/qualityManagement/processInspection/components/formDia.vue
@@ -292,6 +292,19 @@
      prop: "parameterItem",
    },
    {
      label: "参数类型",
      prop: "parameterType",
      formatter: (row) => (row.parameterType === 1 ? "数字" : row.parameterType === 0 ? "文字" : ""),
    },
    {
      label: "最大值",
      prop: "maxValue",
    },
    {
      label: "最小值",
      prop: "minValue",
    },
    {
      label: "单位",
      prop: "unit",
    },
@@ -581,6 +594,10 @@
          ...item,
          id: null,
        }));
        if (tableData.value.some(item => item.parameterType === null || item.parameterType === undefined)) {
          proxy.$modal.msgWarning("参数类型未配置,请配置指标维护模板");
        }
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
src/views/qualityManagement/rawMaterialInspection/components/formDia.vue
@@ -191,6 +191,9 @@
                :tableData="tableData"
                :tableLoading="tableLoading"
                height="400">
        <template #typeSlot="{ row }">
          {{ row.parameterType === 1 ? '数字' : (row.parameterType === 0 ? '文字' : '') }}
        </template>
        <template #slot="{ row }">
          <el-input v-model="row.testValue"
                    clearable
@@ -290,8 +293,22 @@
      prop: "unit",
    },
    {
      label: "参数类型",
      prop: "parameterType",
      dataType: "slot",
      slot: "typeSlot",
    },
    {
      label: "标准值",
      prop: "standardValue",
    },
    {
      label: "最大值",
      prop: "maxValue",
    },
    {
      label: "最小值",
      prop: "minValue",
    },
    {
      label: "内控值",
@@ -546,6 +563,10 @@
          ...item,
          id: null,
        }));
        if (tableData.value.some(item => item.parameterType === null || item.parameterType === undefined)) {
          proxy.$modal.msgWarning("参数类型未配置,请配置指标维护模板");
        }
      })
      .catch(error => {
        console.error("获取标准参数失败:", error);
src/views/salesManagement/biddingManagement/index.vue
@@ -278,6 +278,7 @@
const formRef = ref();
const form = reactive({
  id: undefined,
  projectId: "",
  projectName: "",
  bidDocName: "",
@@ -418,6 +419,7 @@
  Object.keys(form).forEach((key) => {
    form[key] = "";
  });
  form.id = undefined;
  form.bidDocFee = undefined;
  form.bidDeposit = undefined;
};
@@ -458,12 +460,12 @@
  dialogVisible.value = false;
};
// ä¸­æ ‡åŽå‘起销售合同录入:跳转合同管理页,携带项目与招标单位信息
// ä¸­æ ‡åŽå‘起销售台账录入:跳转销售台账,携带项目信息
const handleStartContract = (row) => {
  proxy.$tab.openPage("发起销售合同录入", "/salesManagement/contractManagement", {
  proxy.$tab.openPage("销售台账", "/salesManagement/salesLedger", {
    action: "add",
    projectId: row.projectId,
    projectName: row.projectName,
    signingUnit: row.tenderUnit,
  });
};
src/views/salesManagement/deliveryLedger/index.vue
@@ -32,6 +32,36 @@
            @change="handleQuery"
          />
        </el-form-item>
        <el-form-item label="项目名称:" prop="projectId">
          <el-select
            v-model="searchForm.projectId"
            placeholder="请选择项目"
            clearable
            filterable
            style="width: 200px"
            @change="handleQuery"
          >
            <el-option
              v-for="item in projectOptions"
              :key="item.id"
              :label="item.no + ' - ' + item.title"
              :value="item.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="发货日期:" prop="shippingDateRange">
          <el-date-picker
            v-model="searchForm.shippingDateRange"
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            value-format="YYYY-MM-DD"
            style="width: 240px"
            @change="handleQuery"
            clearable
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleQuery"> æœç´¢</el-button>
        </el-form-item>
@@ -64,6 +94,12 @@
        <el-table-column
          label="发货订单号"
          prop="shippingNo"
          show-overflow-tooltip
        />
        <el-table-column
          label="项目名称"
          prop="projectName"
          min-width="150"
          show-overflow-tooltip
        />
        <el-table-column
@@ -124,14 +160,22 @@
          show-overflow-tooltip
          width="130"
        />
        <el-table-column label="退货单" align="center" width="140">
          <template #default="scope">
            <el-button v-if="scope.row.returnNoList && scope.row.returnNoList.length > 0" link type="primary" @click="openReturnNosDialog(scope.row)">
              æŸ¥çœ‹ {{ scope.row.returnNoList.length }} ä¸ªé€€è´§å•
            </el-button>
            <span v-else>--</span>
          </template>
        </el-table-column>
        <el-table-column fixed="right" label="操作" width="220" align="center">
          <template #default="scope">
            <!--            <el-button-->
            <!--                link-->
            <!--                type="primary"-->
            <!--                :disabled="!isApproved(scope.row.status)"-->
            <!--                @click="openForm('edit', scope.row)">发货-->
            <!--            </el-button>-->
            <el-button
              link
              type="primary"
              :disabled="scope.row.status === '审核通过' || scope.row.status === '已发货'"
              @click="openForm('edit', scope.row)">补充发货信息
            </el-button>
            <el-button
              link
              type="primary"
@@ -139,13 +183,13 @@
              @click="openDetail(scope.row)"
              >详情
            </el-button>
            <!--            <el-button-->
            <!--              link-->
            <!--              type="danger"-->
            <!--              :disabled="isApproving(scope.row.status)"-->
            <!--              @click="handleDeleteSingle(scope.row)"-->
            <!--              >删除-->
            <!--            </el-button>-->
            <el-button
              link
              type="danger"
              :disabled="scope.row.status === '已发货' || scope.row.status === '审核通过'"
              @click="handleDeleteSingle(scope.row)"
              >删除
            </el-button>
          </template>
        </el-table-column>
      </el-table>
@@ -239,6 +283,56 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="发货单据:" prop="shippingDocument">
              <el-input
                v-model="form.shippingDocument"
                placeholder="请输入发货单据"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="8">
            <el-form-item label="是否对账:" prop="isReconciled">
              <el-switch v-model="form.isReconciled" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="是否结算:" prop="isSettled">
              <el-switch v-model="form.isSettled" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="是否已开票:" prop="isInvoiced">
              <el-switch v-model="form.isInvoiced" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12" v-if="form.isReconciled === 1">
            <el-form-item label="对账单归档情况:" prop="statementArchivingStatus">
              <el-input
                type="textarea"
                v-model="form.statementArchivingStatus"
                placeholder="请输入对账单归档情况"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.isSettled === 1">
            <el-form-item label="结算单归档情况:" prop="settlementArchivingStatus">
              <el-input
                type="textarea"
                v-model="form.settlementArchivingStatus"
                placeholder="请输入结算单归档情况"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
@@ -293,6 +387,24 @@
          <el-descriptions-item label="出库单号" :span="2">{{
            detailRow.outboundBatches || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="发货单据">{{
            detailRow.shippingDocument || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="是否对账">{{
            detailRow.isReconciled === 1 ? '是' : '否'
          }}</el-descriptions-item>
          <el-descriptions-item label="是否结算">{{
            detailRow.isSettled === 1 ? '是' : '否'
          }}</el-descriptions-item>
          <el-descriptions-item label="是否已开票">{{
            detailRow.isInvoiced === 1 ? '是' : '否'
          }}</el-descriptions-item>
          <el-descriptions-item label="对账单归档情况" :span="2">{{
            detailRow.statementArchivingStatus || "--"
          }}</el-descriptions-item>
          <el-descriptions-item label="结算单归档情况" :span="2">{{
            detailRow.settlementArchivingStatus || "--"
          }}</el-descriptions-item>
        </el-descriptions>
        <el-table
          :data="getDeliveryProductInfoList()"
@@ -334,6 +446,19 @@
        </div>
      </template>
    </el-dialog>
    <!-- é€€è´§å•号弹框 -->
    <el-dialog v-model="returnNosDialogVisible" title="退货单号列表" width="400px" @close="closeReturnNosDialog">
      <el-table :data="currentReturnNos" border size="small">
        <el-table-column label="序号" type="index" width="60" align="center" />
        <el-table-column label="退货单号" prop="returnNo" align="center" />
      </el-table>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="closeReturnNosDialog">关闭</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
@@ -349,6 +474,7 @@
  getDeliveryDetail,
} from "@/api/salesManagement/deliveryLedger.js";
import { delLedgerFile } from "@/api/salesManagement/salesLedger.js";
import { listProject } from "@/api/projectManagement/project.js";
import ImageUpload from "@/components/AttachmentUpload/image/index.vue";
import ImagePreview from "@/components/AttachmentPreview/image/index.vue";
@@ -357,6 +483,7 @@
const selectedRows = ref([]);
const tableLoading = ref(false);
const salesOrderOptions = ref([]);
const projectOptions = ref([]);
const page = reactive({
  current: 1,
  size: 100,
@@ -368,14 +495,30 @@
const detailRow = ref(null);
const detailProductList = ref([]);
// é€€è´§å•号弹框
const returnNosDialogVisible = ref(false);
const currentReturnNos = ref([]);
const openReturnNosDialog = (row) => {
  currentReturnNos.value = (row.returnNoList || []).map(no => ({ returnNo: no }));
  returnNosDialogVisible.value = true;
};
const closeReturnNosDialog = () => {
  returnNosDialogVisible.value = false;
  currentReturnNos.value = [];
};
// ç”¨æˆ·ä¿¡æ¯è¡¨å•弹框数据
const operationType = ref("");
const dialogFormVisible = ref(false);
const data = reactive({
  searchForm: {
    salesContractNo: "", // é”€å”®è®¢å•号
    shippingCarNumber: "", // è½¦ç‰Œå·
    expressNumber: "", // å¿«é€’单号
    salesContractNo: undefined,
    shippingCarNumber: undefined,
    expressNumber: undefined,
    projectId: undefined,
    shippingDateRange: []
  },
  form: {
    id: null,
@@ -433,7 +576,13 @@
const getList = () => {
  tableLoading.value = true;
  deliveryLedgerListPage({ ...searchForm.value, ...page })
  const params = { ...searchForm.value };
  if (params.shippingDateRange && params.shippingDateRange.length === 2) {
    params.beginShippingDate = params.shippingDateRange[0];
    params.endShippingDate = params.shippingDateRange[1];
  }
  delete params.shippingDateRange;
  deliveryLedgerListPage({ ...params, ...page })
    .then((res) => {
      tableLoading.value = false;
      tableData.value = res.data.records || [];
@@ -461,9 +610,8 @@
// æ‰“开弹框
const openForm = async (type, row) => {
  // å‘货:仅“审核通过”允许编辑
  if (type === "edit" && row && !isApproved(row.status)) {
    proxy.$modal.msgWarning("只有审核通过的数据才可以发货");
  if (type === "edit" && row && (row.status === "审核通过" || row.status === "已发货")) {
    proxy.$modal.msgWarning("该状态的数据不可以编辑发货信息");
    return;
  }
@@ -479,6 +627,12 @@
      shippingCarNumber: row.shippingCarNumber ?? "",
      expressCompany: row.expressCompany ?? "",
      expressNumber: row.expressNumber ?? "",
      shippingDocument: row.shippingDocument ?? "",
      isReconciled: row.isReconciled ?? 0,
      statementArchivingStatus: row.statementArchivingStatus ?? "",
      isSettled: row.isSettled ?? 0,
      settlementArchivingStatus: row.settlementArchivingStatus ?? "",
      isInvoiced: row.isInvoiced ?? 0,
    };
    deliveryFileList.value = row.storageBlobVOs || [];
  }
@@ -616,7 +770,13 @@
    type: "warning",
  })
    .then(() => {
      proxy.download("/shippingInfo/export", {}, "发货台账.xlsx");
      const params = { ...searchForm.value };
      if (params.shippingDateRange && params.shippingDateRange.length === 2) {
        params.beginShippingDate = params.shippingDateRange[0];
        params.endShippingDate = params.shippingDateRange[1];
      }
      delete params.shippingDateRange;
      proxy.download("/shippingInfo/export", params, "发货台账.xlsx");
    })
    .catch(() => {
      proxy.$modal.msg("已取消");
@@ -866,6 +1026,7 @@
};
onMounted(() => {
  listProject({}).then(res => projectOptions.value = res.rows || res.data.records || res.data);
  getList();
});
</script>
src/views/salesManagement/lawsuitManagement/index.vue
@@ -253,6 +253,7 @@
const fileList = ref([]);
const form = reactive({
  id: undefined,
  caseNo: "",
  projectId: "",
  projectName: "",
@@ -299,9 +300,13 @@
const initProjects = async () => {
  try {
    const res = await listProject({ pageSize: 1000 });
    const rows = res?.rows || res?.data?.rows || [];
    projectOptions.value = rows.map((p) => ({ label: p.projectName, value: p.id }));
    const res = await listProject({ pageSize: 1000, current: 1, size: 1000 });
    const rows = res?.records || res?.rows || res?.data?.records || res?.data?.rows || res?.data || [];
    projectOptions.value = rows.map((p) => ({
      label: (p.no ? p.no + ' - ' : '') + (p.title || ''),
      value: p.id,
      title: p.title
    }));
  } catch (e) {
    console.error("加载项目列表失败", e);
  }
@@ -310,7 +315,7 @@
const initContracts = async () => {
  try {
    const res = await contractListNoPage({});
    const list = Array.isArray(res) ? res : res?.data ?? res?.records ?? [];
    const list = Array.isArray(res) ? res : (res?.data?.records || res?.data || res?.records || []);
    contractOptions.value = list.map((item) => ({
      label: item.contractNo + (item.signingUnit ? `(${item.signingUnit})` : ""),
      value: item.id,
@@ -323,7 +328,7 @@
const handleProjectChange = (val) => {
  const project = projectOptions.value.find((item) => item.value === val);
  form.projectName = project ? project.label : "";
  form.projectName = project ? (project.title || project.label) : "";
};
const handleContractChange = (val) => {
@@ -390,6 +395,7 @@
    form[key] = "";
  });
  form.claimAmount = undefined;
  form.id = undefined;
};
const openForm = async (type, row) => {
src/views/salesManagement/receiptPaymentLedger/index.vue
@@ -1,272 +1,610 @@
<template>
  <div class="app-container">
    <div class="search_form"
         style="margin-bottom: 20px;">
      <div>
        <span class="search_title">客户名称:</span>
        <el-input v-model="searchForm.searchText"
                  style="width: 240px"
                  placeholder="输入客户名称搜索"
                  @change="handleQuery"
    <div class="search_form">
      <el-form :model="searchForm" :inline="true">
        <el-form-item label="项目名称:">
          <el-input
              v-model="searchForm.projectName"
              placeholder="请输入项目名称"
                  clearable
                  prefix-icon="Search" />
        <el-button type="primary"
                   @click="handleQuery"
                   style="margin-left: 10px">搜索</el-button>
              prefix-icon="Search"
              style="width: 200px"
              @change="handleSearch"
          />
        </el-form-item>
        <el-form-item label="客户名称:">
          <el-input
              v-model="searchForm.customerName"
              placeholder="请输入客户名称"
              clearable
              prefix-icon="Search"
              style="width: 200px"
              @change="handleSearch"
          />
        </el-form-item>
        <el-form-item label="合同名称:">
          <el-input
              v-model="searchForm.contractName"
              placeholder="请输入合同名称"
              clearable
              prefix-icon="Search"
              style="width: 200px"
              @change="handleSearch"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      </div>
    </div>
    <el-row :gutter="20">
      <el-col :span="12">
        <div class="table_list"
             style="width: 100%">
          <el-table :data="tableData"
                    border
                    v-loading="tableLoading"
                    :row-key="(row) => row.customerId"
                    show-summary
                    :summary-method="summarizeMainTable"
                    @row-click="rowClickMethod"
                    highlight-current-row
                    height="calc(100vh - 18.5em)">
            <el-table-column align="center"
                             label="序号"
                             type="index"
                             width="60" />
            <el-table-column label="客户名称"
                             prop="customerName"
                             show-overflow-tooltip
                             width="200" />
            <el-table-column label="合同金额(元)"
                             prop="contractAmounts"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="回款金额(元)"
                             prop="receiptPaymentAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="退货金额(元)"
                             prop="returnAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="应收金额(元)"
                             prop="receiptableAmount"
                             show-overflow-tooltip
                             width="200">
              <template #default="{ row, column }">
                <el-text type="danger">
                  {{ formattedNumber(row, column, row.receiptableAmount) }}
                </el-text>
  <div class="table_list">
  <div class="app-container layout-fullscreen">
    <!-- Search Form -->
    <div class="summary-strip">
      <div class="summary-stats">
        <span>当前项目:<b>{{ summaryData.totalProjects || 0 }}</b> ä¸ª</span>
        <span>关联客户:<b>{{ summaryData.totalCustomers || 0 }}</b> å®¶</span>
        <span>合同总数:<b>{{ summaryData.totalContracts || 0 }}</b> ä»½</span>
        <span>签约总额:<b>{{ fmtMoney(summaryData.totalAmount || 0) }}</b></span>
        <span class="warn-summary">⚠ ç­¾è®¢è¶…30天未完成回款:<span>{{
            summaryData.totalOverdueCount || 0
          }}</span> ä»½</span>
      </div>
    </div>
    <!-- Layout Columns -->
    <div class="layout-columns">
      <!-- Column 1: Projects -->
      <div class="column-panel" v-loading="prjLoading">
        <div class="panel-header">
          <span>1. é¡¹ç›®æ±‡æ€» ({{ prjTotal }})</span>
        </div>
        <div class="panel-body">
          <div v-if="projects.length === 0" class="empty-state">无匹配项目</div>
          <div v-for="p in projects" :key="p.projectId"
               class="item-card" :class="{ active: p.projectId === selectedProjectId }"
               @click="handleSelectProject(p)">
            <div class="card-title-row">
              <div class="card-main-title">{{ p.projectName }}</div>
            </div>
            <div class="card-code">{{ p.projectCode || '暂无项目编号' }}</div>
            <div class="meta-row" style="margin-top: 8px;">
              <span>总额: <strong>{{ fmtMoney(p.contractAmounts) }}</strong></span>
              <span>开票: <strong>{{ fmtMoney(p.invoiceAmount) }}</strong></span>
            </div>
            <div class="meta-row">
              <span>回款: <strong>{{ fmtMoney(p.receiptPaymentAmount) }}</strong></span>
              <span>应收: <strong>{{ fmtMoney(p.receiptableAmount) }}</strong></span>
            </div>
            <div class="meta-row">
              <span>退货: <strong>{{ fmtMoney(p.returnAmount) }}</strong></span>
              <span>进度: <strong :class="{'text-danger': p.overdueCount > 0}">{{
                  calcRate(p.receiptPaymentAmount, p.contractAmounts)
                }}%</strong></span>
            </div>
            <div class="meta-row">
              <span>客户: <strong>{{ p.customerCount || 0 }} å®¶</strong></span>
              <span>合同: <strong>{{ p.contractCount || 0 }} ä»½</strong></span>
            </div>
            <div v-if="p.overdueCount > 0" style="margin-top: 6px;">
              <el-tag type="danger" size="small" effect="light">⚠ {{ p.overdueCount }} ä»½è¶…期合同</el-tag>
            </div>
          </div>
        </div>
        <div class="panel-pagination">
          <el-pagination background small layout="total, prev, pager, next" :total="prjTotal" :page-size="prjPage.size"
                         v-model:current-page="prjPage.current" @current-change="handlePrjPageChange"/>
        </div>
      </div>
      <!-- Column 2: Customers -->
      <div class="column-panel" v-loading="custLoading">
        <div class="panel-header">
          <span>2. å®¢æˆ·æ±‡æ€» ({{ custTotal }})</span>
        </div>
        <div class="panel-body">
          <div v-if="!selectedProjectId" class="empty-state">请先在左侧选择项目</div>
          <div v-else-if="customers.length === 0" class="empty-state">该项目下无匹配的客户信息</div>
          <div v-for="c in customers" :key="c.customerId"
               class="item-card" :class="{ active: c.customerId === selectedCustomerId }"
               @click="handleSelectCustomer(c)">
            <div class="card-title-row">
              <div class="card-main-title">{{ c.customerName || '未命名客户' }}</div>
            </div>
            <div class="meta-row" style="margin-top: 8px;">
              <span>签约: <strong>{{ fmtMoney(c.contractAmounts) }}</strong></span>
              <span>进度: <strong :class="{'text-danger': c.overdueCount > 0}">{{
                  calcRate(c.receiptPaymentAmount, c.contractAmounts)
                }}%</strong></span>
            </div>
            <div class="meta-row">
              <span>退货: <strong>{{ fmtMoney(c.returnAmount) }}</strong></span>
              <span>应收: <strong>{{ fmtMoney(c.receiptableAmount) }}</strong></span>
            </div>
            <div class="meta-row">
              <span>已回款: <strong>{{ fmtMoney(c.receiptPaymentAmount) }}</strong></span>
              <span>合同数: <strong>{{ c.contractCount || 0 }} ä»½</strong></span>
            </div>
            <div style="margin-top: 6px;">
              <el-tag v-if="c.overdueCount > 0" type="danger" size="small" effect="light">⚠ {{ c.overdueCount }} ä»½è¶…期</el-tag>
              <el-tag v-else type="success" size="small" effect="light">正常履约</el-tag>
            </div>
          </div>
        </div>
        <div class="panel-pagination">
          <el-pagination background small layout="total, prev, pager, next" :total="custTotal"
                         :page-size="custPage.size" v-model:current-page="custPage.current"
                         @current-change="handleCustPageChange"/>
        </div>
      </div>
      <!-- Column 3: Contracts -->
      <div class="column-panel" v-loading="contractLoading">
        <div class="panel-header">
          <span>3. åˆåŒæ˜Žç»† ({{ contractTotal }})</span>
        </div>
        <div class="panel-body" style="padding: 0;">
          <div v-if="!selectedCustomerId" class="empty-state" style="padding-top: 40px;">请先在中间栏选择客户</div>
          <div v-else-if="contracts.length === 0" class="empty-state" style="padding-top: 40px;">
            è¯¥å®¢æˆ·ä¸‹æ— åŒ¹é…çš„合同明细
          </div>
          <el-table v-else :data="contracts" size="small" style="width: 100%; height: 100%;" stripe
                    :row-class-name="tableRowClassName">
            <el-table-column label="合同信息" min-width="160" show-overflow-tooltip>
              <template #default="{ row }">
                <div style="font-weight: 600; color: #0f172a;">{{
                    row.contractName || row.salesContractNo || '未命名'
                  }}
                </div>
                <div style="font-size: 11px; color: #64748b;">合同编号:{{ row.salesContractNo }}</div>
              </template>
            </el-table-column>
            <el-table-column label="金额与回款" min-width="150">
              <template #default="{ row }">
                <div style="font-size: 12px; color: #64748b;">总额: <span
                    style="color: #0f172a; font-weight: 500;">{{ fmtMoney(row.contractAmount) }}</span></div>
                <div :class="row.isPaymentTimeout ? 'text-danger' : ''" style="font-size: 12px; font-weight: 500;">
                  å·²å›ž: {{ fmtMoney(row.receiptPaymentAmount) }}
                  ({{ calcRate(row.receiptPaymentAmount, row.contractAmount) }}%)
                </div>
              </template>
            </el-table-column>
            <el-table-column label="应收与退货" min-width="130">
              <template #default="{ row }">
                <div style="font-size: 12px; color: #64748b;">应收: <span
                    style="color: #0f172a;">{{ fmtMoney(row.receiptableAmount) }}</span></div>
                <div style="font-size: 12px; color: #64748b;">退货: <span
                    style="color: #0f172a;">{{ fmtMoney(row.returnAmount) }}</span></div>
              </template>
            </el-table-column>
            <el-table-column label="状态" width="100" align="center">
              <template #default="{ row }">
                <el-tag v-if="row.isPaymentTimeout" type="danger" size="small" effect="dark">超期未回</el-tag>
                <el-tag
                    v-else-if="(row.receiptPaymentAmount || 0) >= (row.contractAmount || 0) && (row.contractAmount || 0) > 0"
                    type="success" size="small" effect="dark">已结清
                </el-tag>
                <el-tag v-else type="primary" size="small" effect="plain">履行中</el-tag>
              </template>
            </el-table-column>
          </el-table>
          <pagination v-show="total > 0"
                      :total="total"
                      layout="total, sizes, prev, pager, next, jumper"
                      :page="page.current"
                      :limit="page.size"
                      @pagination="paginationChange" />
        </div>
      </el-col>
      <el-col :span="12">
        <div class="table_list"
             style="width: 100%">
          <el-table :data="receiptRecord"
                    border
                    :row-key="(row) => row.id"
                    show-summary
                    :summary-method="summarizeMainTable1"
                    height="calc(100vh - 18.5em)">
            <el-table-column align="center"
                             label="序号"
                             type="index"
                             width="60" />
            <el-table-column label="合同签订日期"
                             prop="executionDate"
                             show-overflow-tooltip
                             width="110" />
            <el-table-column label="销售合同号"
                             prop="salesContractNo"
                             show-overflow-tooltip
                             width="200" />
            <el-table-column label="合同金额(元)"
                             prop="contractAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="回款金额(元)"
                             prop="receiptPaymentAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="退货金额(元)"
                             prop="returnAmount"
                             show-overflow-tooltip
                             :formatter="formattedNumber"
                             width="200" />
            <el-table-column label="应收金额(元)"
                             prop="receiptableAmount"
                             show-overflow-tooltip
                             width="200">
              <template #default="{ row, column }">
                <el-text type="danger">
                  {{ formattedNumber(row, column, row.receiptableAmount) }}
                </el-text>
              </template>
            </el-table-column>
          </el-table>
          <pagination v-show="recordTotal > 0"
                      :total="recordTotal"
                      layout="total, sizes, prev, pager, next, jumper"
                      :page="recordPage.current"
                      :limit="recordPage.size"
                      @pagination="recordPaginationChange" />
        <div class="panel-pagination">
          <el-pagination background small layout="total, prev, pager, next" :total="contractTotal"
                         :page-size="contractPage.size" v-model:current-page="contractPage.current"
                         @current-change="handleContractPageChange"/>
        </div>
      </el-col>
    </el-row>
      </div>
    </div>
  </div>
  </div>
</template>
<script setup>
  import { onMounted, ref, reactive, toRefs, getCurrentInstance } from "vue";
import {onMounted, ref, reactive, toRefs} from "vue";
  import {
  projectTransactions,
    customewTransactions,
    customewTransactionsDetails,
  transactionsSummary
  } from "@/api/salesManagement/indicatorStats.js";
  import Pagination from "../../../components/PIMTable/Pagination.vue";
  const { proxy } = getCurrentInstance();
  const tableData = ref([]);
  const receiptRecord = ref([]);
  const tableLoading = ref(false);
  const page = reactive({
    current: 1,
    size: 100,
  });
  const recordPage = reactive({
    current: 1,
    size: 100,
  });
  const total = ref(0);
  const recordTotal = ref(0);
const summaryData = ref({});
  const data = reactive({
    searchForm: {
      searchText: "",
      invoiceDate: "",
    projectName: "",
    customerName: "",
    contractName: "",
    },
  });
  const customerId = ref("");
  const { searchForm } = toRefs(data);
  const originReceiptRecord = ref([]);
  // æŸ¥è¯¢åˆ—表
  /** æœç´¢æŒ‰é’®æ“ä½œ */
  const handleQuery = () => {
    page.current = 1;
    getList();
  };
  const paginationChange = obj => {
    page.current = obj.page;
    page.size = obj.limit;
    getList();
  };
  const getList = () => {
    tableLoading.value = true;
    customewTransactions({ ...searchForm.value, ...page }).then(res => {
      tableLoading.value = false;
      tableData.value = res.data.records;
      total.value = res.data.total;
      if (tableData.value.length > 0) {
        recordPage.current = 1;
        customerId.value = tableData.value[0].customerId;
        receiptPaymentList(customerId.value);
      }
// Column 1: Projects
const projects = ref([]);
const prjLoading = ref(false);
const prjTotal = ref(0);
const prjPage = reactive({current: 1, size: 20});
const selectedProjectId = ref(null);
// Column 2: Customers
const customers = ref([]);
const custLoading = ref(false);
const custTotal = ref(0);
const custPage = reactive({current: 1, size: 20});
const selectedCustomerId = ref(null);
// Column 3: Contracts
const contracts = ref([]);
const contractLoading = ref(false);
const contractTotal = ref(0);
const contractPage = reactive({current: 1, size: 20});
const getSummary = () => {
  transactionsSummary().then(res => {
    summaryData.value = res.data || {};
    });
  };
  const formattedNumber = (row, column, cellValue) => {
    return cellValue ? parseFloat(cellValue).toFixed(2) : "0.00";
  };
  // ä¸»è¡¨åˆè®¡æ–¹æ³•
  const summarizeMainTable = param => {
    return proxy.summarizeTable(
      param,
      [
        "contractAmounts",
        "receiptPaymentAmount",
        "returnAmount",
        "receiptableAmount",
      ],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
    );
  };
  // å­è¡¨åˆè®¡æ–¹æ³•
  const summarizeMainTable1 = param => {
    var summarizeTable = proxy.summarizeTable(
      param,
      [
        "contractAmount",
        "receiptPaymentAmount",
        "returnAmount",
        "receiptableAmount",
      ],
      {
        ticketsNum: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
        futureTickets: { noDecimal: true }, // ä¸ä¿ç•™å°æ•°
      }
    );
    return summarizeTable;
const handleSearch = () => {
  prjPage.current = 1;
  loadProjects();
  getSummary();
  };
  const receiptPaymentList = id => {
    const param = {
      customerId: id,
      current: recordPage.current,
      size: recordPage.size,
const handleReset = () => {
  searchForm.value = {
    projectName: "",
    customerName: "",
    contractName: "",
    };
    customewTransactionsDetails(param).then(res => {
      if (Array.isArray(res.data)) {
        originReceiptRecord.value = res.data;
        recordTotal.value = res.data.length;
        handlePagination({ page: 1, limit: recordPage.size });
  handleSearch();
};
const loadProjects = () => {
  prjLoading.value = true;
  projectTransactions({...searchForm.value, ...prjPage}).then(res => {
    prjLoading.value = false;
    projects.value = res.data.records || [];
    prjTotal.value = res.data.total || 0;
    if (projects.value.length > 0) {
      handleSelectProject(projects.value[0]);
      } else {
        receiptRecord.value = res.data.records;
        recordTotal.value = res.data.total;
      selectedProjectId.value = null;
      customers.value = [];
      custTotal.value = 0;
      contracts.value = [];
      contractTotal.value = 0;
      }
  }).catch(() => {
    prjLoading.value = false;
    });
  };
  // æ±‡æ¬¾è®°å½•列表分页
  const recordPaginationChange = pagination => {
    recordPage.current = pagination.page;
    recordPage.size = pagination.limit;
    receiptPaymentList(customerId.value);
const handleSelectProject = (prj) => {
  selectedProjectId.value = prj.projectId;
  custPage.current = 1;
  loadCustomers();
  };
  const rowClickMethod = row => {
    customerId.value = row.customerId;
    receiptPaymentList(customerId.value);
const loadCustomers = () => {
  if (!selectedProjectId.value) return;
  custLoading.value = true;
  const params = {
    projectId: selectedProjectId.value,
    customerName: searchForm.value.customerName,
    contractName: searchForm.value.contractName,
    ...custPage
  };
  customewTransactions(params).then(res => {
    custLoading.value = false;
    const records = res.data.records || res.data || [];
    customers.value = records;
    custTotal.value = res.data.total || records.length || 0;
    if (customers.value.length > 0) {
      handleSelectCustomer(customers.value[0]);
    } else {
      selectedCustomerId.value = null;
      contracts.value = [];
      contractTotal.value = 0;
    }
  }).catch(() => {
    custLoading.value = false;
  });
  };
  const handlePagination = ({ page, limit }) => {
    recordPage.current = page;
    recordPage.size = limit;
const handleSelectCustomer = (cust) => {
  selectedCustomerId.value = cust.customerId;
  contractPage.current = 1;
  loadContracts();
};
    const start = (page - 1) * limit;
    const end = start + limit;
const loadContracts = () => {
  if (!selectedCustomerId.value) return;
  contractLoading.value = true;
  const params = {
    projectId: selectedProjectId.value,
    customerId: selectedCustomerId.value,
    contractName: searchForm.value.contractName,
    ...contractPage
  };
  customewTransactionsDetails(params).then(res => {
    contractLoading.value = false;
    const records = Array.isArray(res.data) ? res.data : (res.data.records || []);
    contracts.value = records;
    contractTotal.value = res.data.total || records.length || 0;
  }).catch(() => {
    contractLoading.value = false;
  });
};
    receiptRecord.value = originReceiptRecord.value.slice(start, end);
const handlePrjPageChange = (val) => {
  prjPage.current = val;
  loadProjects();
};
const handleCustPageChange = (val) => {
  custPage.current = val;
  loadCustomers();
};
const handleContractPageChange = (val) => {
  contractPage.current = val;
  loadContracts();
};
const fmtMoney = (num) => {
  if (num === null || num === undefined || isNaN(num)) return 'Â¥ 0.00';
  return 'Â¥ ' + parseFloat(num).toLocaleString('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2});
};
const calcRate = (paid, amount) => {
  if (!amount || amount == 0) return '0.0';
  const val = ((paid || 0) / amount) * 100;
  return val.toFixed(1);
};
const getDaysFromSign = (dateStr) => {
  if (!dateStr) return 0;
  const sign = new Date(dateStr);
  const now = new Date();
  const diffTime = now - sign;
  return Math.max(0, Math.floor(diffTime / (1000 * 60 * 60 * 24)));
};
const tableRowClassName = ({row}) => {
  if (row.isPaymentTimeout) {
    return 'warning-row';
  }
  return '';
  };
  onMounted(() => {
    getList();
  loadProjects();
  getSummary();
  });
</script>
<style scoped lang="scss">
  .table_list {
    width: 50%;
<style scoped>
/* CSS Variables */
.layout-fullscreen {
  --bg: #f8fafc;
  --card: #ffffff;
  --text: #0f172a;
  --muted: #64748b;
  --border: #e2e8f0;
  --primary: #2563eb;
  --primary-50: #eff6ff;
  --success: #16a34a;
  --success-50: #dcfce7;
  --warning: #d97706;
  --danger: #dc2626;
  --danger-50: #fef2f2;
  --danger-border: #fecaca;
  height: calc(100vh - 84px); /* Fallback height, allows container to take viewport height */
  display: flex;
  flex-direction: column;
  background-color: var(--bg);
  padding: 16px;
  overflow: hidden; /* Hide body scroll, let columns scroll */
}
/* é¡¶éƒ¨æœç´¢ */
.search_form {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 16px 20px 0 20px;
  margin-bottom: 12px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.02);
}
/* åŠ¨æ€ç»Ÿè®¡æ±‡æ€»æ  */
.summary-strip {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 20px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
  flex-shrink: 0;
}
.summary-stats {
  display: flex;
  gap: 20px;
  align-items: center;
  flex-wrap: wrap;
}
.summary-stats b {
  color: var(--primary);
  font-size: 14px;
}
.warn-summary {
  background: var(--danger-50);
  color: var(--danger);
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: 600;
  border: 1px solid var(--danger-border);
}
/* ä¸‰æ è”动 */
.layout-columns {
  flex: 1;
  display: grid;
  grid-template-columns: 310px 330px 1fr;
  gap: 16px;
  margin-top: 16px;
  overflow: hidden;
}
@media (max-width: 1400px) {
  .layout-columns {
    grid-template-columns: 280px 300px 1fr;
  }
}
@media (max-width: 1200px) {
  .layout-columns {
    display: flex;
    flex-direction: column;
    overflow-y: auto;
  }
  .layout-fullscreen {
    overflow: auto;
  }
  .column-panel {
    min-height: 400px;
  }
}
.column-panel {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
}
.panel-header {
  padding: 12px 16px;
  background: #f8fafc;
  border-bottom: 1px solid var(--border);
  font-size: 14px;
  font-weight: 600;
  flex-shrink: 0;
  color: var(--text);
}
.panel-body {
  flex: 1;
  overflow-y: auto;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.panel-pagination {
  padding: 8px;
  background: #ffffff;
  border-top: 1px solid var(--border);
  display: flex;
  justify-content: center;
  align-items: center;
  flex-shrink: 0;
}
/* åˆ—表卡片 (项目 & å®¢æˆ·) */
.item-card {
  background: #ffffff;
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 12px 14px;
  cursor: pointer;
  transition: all 0.15s;
}
.item-card:hover {
  border-color: #cbd5e1;
  background: #f8fafc;
}
.item-card.active {
  border-color: var(--primary);
  background: var(--primary-50);
  box-shadow: 0 0 0 1px var(--primary);
}
.card-title-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 6px;
}
.card-main-title {
  font-size: 14px;
  font-weight: 600;
  color: var(--text);
  line-height: 1.4;
}
.card-code {
  font-size: 11px;
  color: var(--muted);
  background: #f1f5f9;
  padding: 2px 6px;
  border-radius: 4px;
  display: inline-block;
}
.meta-row {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--muted);
  margin-top: 4px;
}
.meta-row strong {
  color: var(--text);
  font-weight: 600;
}
.text-danger {
  color: var(--danger) !important;
  font-weight: 700;
}
:deep(.warning-row) {
  background-color: var(--danger-50) !important;
}
.empty-state {
  text-align: center;
  color: var(--muted);
  padding: 40px 16px;
  font-size: 13px;
  }
</style>
src/views/salesManagement/salesLedger/index.vue
@@ -188,6 +188,7 @@
          </template>
        </el-table-column>
        <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column
          label="销售合同号"
          prop="salesContractNo"
@@ -216,7 +217,14 @@
          label="付款方式"
          prop="paymentMethod"
          show-overflow-tooltip
        >
          <template #default="scope">
            <dict-tag
              :options="checkout_payment"
              :value="scope.row.paymentMethod"
        />
          </template>
        </el-table-column>
        <el-table-column
          label="合同金额(元)"
          prop="contractAmount"
@@ -322,6 +330,76 @@
            </el-button>
          </el-col>
        </el-row>
        <el-divider content-position="left">关联项目</el-divider>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="客户名称:" prop="customerId">
              <el-select
                v-model="form.customerId"
                placeholder="请选择"
                clearable
                :disabled="operationType === 'view'"
                filterable
              >
                <el-option
                  v-for="item in customerOption"
                  :key="item.id"
                  :label="item.customerName"
                  :value="item.id"
                >
                  {{
                    item.customerName + "——" + item.taxpayerIdentificationNumber
                  }}
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectId">
              <el-select
                v-model="form.projectId"
                placeholder="请选择项目"
                clearable
                filterable
                :disabled="operationType === 'view'"
                @focus="previousProjectId = form.projectId"
                @change="handleProjectChange"
              >
                <el-option
                  v-for="item in projectOptions"
                  :key="item.id"
                  :label="item.no + ' - ' + item.title"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="关联合同:" prop="associatedContractIds">
              <el-select
                v-model="form.associatedContractIds"
                multiple
                placeholder="请选择关联合同"
                clearable
                filterable
                :disabled="operationType === 'view'"
                style="width: 100%"
              >
                <el-option
                  v-for="item in contractOptions"
                  :key="item.id"
                  :label="item.contractNo"
                  :value="item.id"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-divider content-position="left">台账信息</el-divider>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="销售合同号:" prop="salesContractNo">
@@ -372,44 +450,11 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="客户名称:" prop="customerId">
              <el-select
                v-model="form.customerId"
                placeholder="请选择"
                clearable
                :disabled="operationType === 'view'"
                filterable
              >
                <el-option
                  v-for="item in customerOption"
                  :key="item.id"
                  :label="item.customerName"
                  :value="item.id"
                >
                  {{
                    item.customerName + "——" + item.taxpayerIdentificationNumber
                  }}
                </el-option>
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="项目名称:" prop="projectName">
              <el-input
                v-model="form.projectName"
                placeholder="请输入"
                clearable
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="签订日期:" prop="executionDate">
              <el-date-picker
                style="width: 100%"
                v-model="form.executionDate"
                @change="(val) => { if(val) form.year = parseInt(val.substring(0, 4)) }"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                type="date"
@@ -421,12 +466,19 @@
          </el-col>
          <el-col :span="12">
            <el-form-item label="付款方式">
              <el-input
              <el-select
                v-model="form.paymentMethod"
                placeholder="请输入"
                placeholder="请选择付款方式"
                clearable
                :disabled="operationType === 'view'"
              >
                <el-option
                  v-for="item in checkout_payment"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
              />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
@@ -483,13 +535,160 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="签单单位:" prop="signingUnitId">
              <el-tree-select
                v-model="form.signingUnitId"
                :data="deptOptions"
                :props="{ value: 'id', label: 'label', children: 'children' }"
                value-key="id"
                placeholder="请选择签单单位"
                check-strictly
                clearable
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="办事处:" prop="officeId">
              <el-tree-select
                v-model="form.officeId"
                :data="deptOptions"
                :props="{ value: 'id', label: 'label', children: 'children' }"
                value-key="id"
                placeholder="请选择办事处"
                check-strictly
                clearable
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="组负责人:" prop="groupLeaderId">
              <el-select
                v-model="form.groupLeaderId"
                filterable
                placeholder="请选择组负责人"
                clearable
                :disabled="operationType === 'view'"
              >
                <el-option
                  v-for="item in userList"
                  :key="item.userId"
                  :label="item.nickName"
                  :value="item.userId"
                />
              </el-select>
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="年份:">
              <el-input
                v-model="form.year"
                placeholder="自动取签订日期"
                disabled
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="是否开发票:">
              <el-switch
                v-model="form.isInvoice"
                :active-value="1"
                :inactive-value="0"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="是否中标:">
              <el-switch
                v-model="form.isBid"
                :active-value="1"
                :inactive-value="0"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12" v-if="form.isBid === 1">
            <el-form-item label="中标价:">
              <el-input-number
                v-model="form.bidPrice"
                :precision="2"
                :step="1000"
                :min="0"
                style="width: 100%"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="form.isBid !== 1">
            <el-form-item label="未中标原因:">
              <el-input
                v-model="form.unbidReason"
                placeholder="请输入未中标原因"
                clearable
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12">
            <el-form-item label="合同原件约定回传日期:">
              <el-date-picker
                style="width: 100%"
                v-model="form.contractOriginalReturnDate"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                type="date"
                placeholder="请选择日期"
                clearable
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:" prop="remarks">
              <el-input
                v-model="form.remarks"
                placeholder="请输入"
                clearable
                type="textarea"
                :rows="2"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="operationType !== 'view'" :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:" prop="salesLedgerFiles">
              <FileUpload
                v-model:file-list="fileList"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-divider content-position="left">产品信息</el-divider>
        <el-row>
          <el-form-item label="产品信息:" prop="entryDate">
          <el-form-item label="" prop="entryDate">
            <el-button
              v-if="operationType !== 'view'"
              type="primary"
              @click="openProductForm('add')"
              >添加
              >添加明细
            </el-button>
            <el-button
              v-if="operationType !== 'view'"
@@ -566,30 +765,6 @@
            </template>
          </el-table-column>
        </el-table>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="备注:" prop="remarks">
              <el-input
                v-model="form.remarks"
                placeholder="请输入"
                clearable
                type="textarea"
                :rows="2"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row v-if="operationType !== 'view'" :gutter="30">
          <el-col :span="24">
            <el-form-item label="附件材料:" prop="salesLedgerFiles">
              <FileUpload
                v-model:file-list="fileList"
                :disabled="operationType === 'view'"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
    </FormDialog>
    <!-- ä»ŽæŠ¥ä»·å•导入(仅审批通过) -->
@@ -1105,6 +1280,56 @@
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="发货单据:" prop="shippingDocument">
              <el-input
                v-model="deliveryForm.shippingDocument"
                placeholder="请输入发货单据"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="8">
            <el-form-item label="是否对账:" prop="isReconciled">
              <el-switch v-model="deliveryForm.isReconciled" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="是否结算:" prop="isSettled">
              <el-switch v-model="deliveryForm.isSettled" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="是否已开票:" prop="isInvoiced">
              <el-switch v-model="deliveryForm.isInvoiced" :active-value="1" :inactive-value="0" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="12" v-if="deliveryForm.isReconciled === 1">
            <el-form-item label="对账单归档情况:" prop="statementArchivingStatus">
              <el-input
                type="textarea"
                v-model="deliveryForm.statementArchivingStatus"
                placeholder="请输入对账单归档情况"
                clearable
              />
            </el-form-item>
          </el-col>
          <el-col :span="12" v-if="deliveryForm.isSettled === 1">
            <el-form-item label="结算单归档情况:" prop="settlementArchivingStatus">
              <el-input
                type="textarea"
                v-model="deliveryForm.settlementArchivingStatus"
                placeholder="请输入结算单归档情况"
                clearable
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="30">
          <el-col :span="24">
            <el-form-item label="库存:" prop="batchNo">
              <el-table
                :data="deliveryForm.batchNoList"
@@ -1176,9 +1401,11 @@
import { addShippingInfo } from "@/api/salesManagement/deliveryLedger.js";
import { ElMessageBox, ElMessage } from "element-plus";
import useUserStore from "@/store/modules/user";
import { userListNoPage } from "@/api/system/user.js";
import { userListNoPage, deptTreeSelect } from "@/api/system/user.js";
import { listProject, getProject } from "@/api/projectManagement/project.js";
import FormDialog from "@/components/Dialog/FormDialog.vue";
import { getQuotationList } from "@/api/salesManagement/salesQuotation.js";
import { contractListNoPage } from "@/api/salesManagement/contractManagement.js";
import {
  ledgerListPage,
  productList,
@@ -1208,12 +1435,15 @@
const route = useRoute();
const userStore = useUserStore();
const { proxy } = getCurrentInstance();
const { tax_rate } = proxy.useDict("tax_rate");
const { tax_rate, checkout_payment } = proxy.useDict("tax_rate", "checkout_payment");
const tableData = ref([]);
const productData = ref([]);
const selectedRows = ref([]);
const productSelectedRows = ref([]);
const userList = ref([]);
const deptOptions = ref([]);
const projectOptions = ref([]);
const contractOptions = ref([]);
const customerOption = ref([]);
const productOptions = ref([]);
const modelOptions = ref([]);
@@ -1248,6 +1478,18 @@
    maintenanceTime: "",
    productData: [],
    executionDate: "",
    projectId: undefined,
    associatedContractIds: [],
    year: "",
    signingUnitId: undefined,
    officeId: undefined,
    groupLeaderId: undefined,
    isInvoice: 0,
    bidPrice: undefined,
    isBid: 0,
    unbidReason: "",
    contractOriginalReturnDate: "",
    hasProductionRecord: false,
    createTime: "",
  },
@@ -1323,6 +1565,51 @@
  total: 0,
});
const selectedQuotation = ref(null);
const previousProjectId = ref(null);
const handleProjectChange = (val) => {
  if (productData.value && productData.value.length > 0) {
    ElMessageBox.confirm("当前已录入产品明细,切换项目将被覆盖,是否继续?", "提示", {
      type: "warning",
    })
      .then(() => {
        previousProjectId.value = val;
        fetchAndPopulateProject(val);
      })
      .catch(() => {
        form.value.projectId = previousProjectId.value;
      });
  } else {
    previousProjectId.value = val;
    fetchAndPopulateProject(val);
  }
};
const fetchAndPopulateProject = (val) => {
  let proj = projectOptions.value.find((p) => p.id === val);
  form.value.projectName = proj ? proj.title : "";
  if (!val) {
    productData.value = [];
    return;
  }
  getProject(val).then((res) => {
    const projectInfo = res.data;
    if (projectInfo.info && projectInfo.info.clientId) {
      form.value.customerId = projectInfo.info.clientId;
    }
    if (projectInfo.salesLedgerProductList) {
      productData.value = projectInfo.salesLedgerProductList.map((item) => ({
        ...item,
        id: undefined,
        salesLedgerId: undefined
      }));
    } else {
      productData.value = [];
    }
  });
};
// å‘货相关
const deliveryFormVisible = ref(false);
@@ -1423,6 +1710,12 @@
    expressCompany: "",
    expressNumber: "",
    type: "货车", // è´§è½¦, å¿«é€’
    shippingDocument: "",
    isReconciled: 0,
    statementArchivingStatus: "",
    isSettled: 0,
    settlementArchivingStatus: "",
    isInvoiced: 0,
  },
  deliveryRules: {
    shippingCarNumber: [
@@ -1750,6 +2043,9 @@
  form.value = {};
  productData.value = [];
  selectedQuotation.value = null;
  deptTreeSelect().then(res => deptOptions.value = res.data);
  listProject({}).then(res => projectOptions.value = res.rows || res.data.records || res.data);
  contractListNoPage({}).then(res => contractOptions.value = res.data);
  let userLists = await userListNoPage();
  userList.value = userLists.data;
  listCustomer({ current: -1, size: -1, type: 0 }).then((res) => {
@@ -2993,13 +3289,22 @@
  let res = await userStore.getInfo();
  currentFactoryName.value = res.user.currentFactoryName;
};
onMounted(() => {
onMounted(async () => {
  searchForm.salesContractNo = route.query.salesContractNo;
  getList();
  userListNoPage().then((res) => {
    userList.value = res.data;
  });
  getCurrentFactoryName();
  if (route.query.action === "add") {
    await openForm("add");
    if (route.query.projectId) {
      const pId = Number(route.query.projectId);
      form.value.projectId = pId;
      fetchAndPopulateProject(pId);
    }
  }
});
</script>
src/views/salesManagement/salesQuotation/index.vue
@@ -3,7 +3,7 @@
    <el-card class="box-card">
      <!-- æœç´¢åŒºåŸŸ -->
      <el-row :gutter="20" class="search-row">
        <el-col :span="8">
        <el-col :span="6">
          <el-input
            v-model="searchForm.quotationNo"
            placeholder="请输入报价单号"
@@ -15,7 +15,7 @@
            </template>
          </el-input>
        </el-col>
        <el-col :span="8">
        <el-col :span="6">
          <el-select v-model="searchForm.customerId" placeholder="请选择客户" clearable>
                        <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.id">
                            {{
@@ -32,7 +32,12 @@
<!--            <el-option label="已过期" value="已过期"></el-option>-->
<!--          </el-select>-->
<!--        </el-col>-->
        <el-col :span="8">
        <el-col :span="6">
          <el-select v-model="searchForm.projectId" placeholder="请选择项目名称" clearable filterable>
            <el-option v-for="item in projectOptions" :key="item.id" :label="item.no + ' - ' + item.title" :value="item.id" />
          </el-select>
        </el-col>
        <el-col :span="6">
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="resetSearch">重置</el-button>
          <el-button style="float: right;" type="primary" @click="handleAdd">
@@ -52,6 +57,7 @@
      >
                <el-table-column align="center" label="序号" type="index" width="60" />
        <el-table-column prop="quotationNo" label="报价单号" />
        <el-table-column prop="projectName" label="项目名称" />
        <el-table-column prop="customer" label="客户名称" />
        <el-table-column prop="salesperson" label="业务员" width="100" />
        <el-table-column prop="quotationDate" label="报价日期" width="120" />
@@ -149,6 +155,13 @@
              <el-col :span="12">
                <el-form-item label="付款方式" prop="paymentMethod">
                  <el-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable />
                </el-form-item>
              </el-col>
              <el-col :span="12">
                <el-form-item label="项目名称" prop="projectId">
                  <el-select v-model="form.projectId" placeholder="请选择项目名称" style="width: 100%" clearable filterable>
                    <el-option v-for="item in projectOptions" :key="item.id" :label="item.no + ' - ' + item.title" :value="item.id" />
                  </el-select>
                </el-form-item>
              </el-col>
            </el-row>
@@ -258,6 +271,7 @@
    <el-dialog v-model="viewDialogVisible" title="报价详情" width="800px">
      <el-descriptions :column="2" border>
        <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item>
        <el-descriptions-item label="项目名称">{{ currentQuotation.projectName }}</el-descriptions-item>
        <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item>
        <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item>
        <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item>
@@ -303,19 +317,22 @@
import {modelList, productTreeList} from "@/api/basicData/product.js";
import {listCustomer} from "@/api/basicData/customer.js";
import { userListNoPage } from "@/api/system/user.js";
import { listProject } from "@/api/projectManagement/project.js";
// å“åº”式数据
const loading = ref(false)
const searchForm = reactive({
  quotationNo: '',
  customerId: '',
  status: ''
  status: '',
  projectId: undefined
})
const quotationList = ref([])
const userList = ref([])
const productOptions = ref([]);
const modelOptions  = ref([]);
const projectOptions = ref([]);
const pagination = reactive({
  total: 3,
  currentPage: 1,
@@ -328,6 +345,7 @@
const form = reactive({
  quotationNo: '',
  customerId: undefined,
  projectId: undefined,
  customer: '',
  salesperson: '',
  quotationDate: '',
@@ -396,6 +414,7 @@
  searchForm.quotationNo = ''
  searchForm.customer = ''
  searchForm.status = ''
  searchForm.projectId = undefined
  // é‡ç½®åˆ°ç¬¬ä¸€é¡µå¹¶é‡æ–°æŸ¥è¯¢
  pagination.currentPage = 1
  handleSearch()
@@ -407,7 +426,15 @@
  resetForm()
  dialogVisible.value = true
  getProductOptions();
  fetchCustomerOptions()
  fetchCustomerOptions();
  fetchProjectOptions();
}
const fetchProjectOptions = () => {
  if (projectOptions.value.length > 0) return
  listProject({current: -1,size:-1}).then((res) => {
    projectOptions.value = res.data.records;
  });
}
const fetchCustomerOptions = () => {
@@ -530,7 +557,8 @@
      unitPrice: product.unitPrice || 0,
      amount: product.amount || 0
    })) : [],
    totalAmount: row.totalAmount || 0
    totalAmount: row.totalAmount || 0,
    projectName: row.projectName || ''
  }
  viewDialogVisible.value = true
}
@@ -543,11 +571,13 @@
  // å…ˆåŠ è½½äº§å“æ ‘æ•°æ®ï¼Œå¦åˆ™ el-tree-select æ— æ³•反显产品名称
  await getProductOptions()
  await fetchCustomerOptions()
  await fetchProjectOptions()
  // åªå¤åˆ¶éœ€è¦çš„字段,避免将组件引用放入响应式对象
  form.quotationNo = row.quotationNo || ''
  form.customer = row.customer || ''
  form.customerId = row.customerId || undefined
  form.projectId = row.projectId || undefined
  form.salesperson = row.salesperson || ''
  form.quotationDate = row.quotationDate || ''
  form.validDate = row.validDate || ''
@@ -629,6 +659,8 @@
const resetForm = () => {
  form.customer = ''
  form.customerId = undefined
  form.projectId = undefined
  form.salesperson = ''
  form.quotationDate = ''
  form.validDate = ''
@@ -790,6 +822,7 @@
  getUserList()
  handleSearch()
  fetchCustomerOptions()
  fetchProjectOptions()
})
</script>