gaoluyang
18 小时以前 e0cb2008ffb01348b54a7370180a100f3c975877
Merge branch 'dev_New' into dev_天津军泰伟业
已添加6个文件
已修改20个文件
已删除1个文件
4826 ■■■■■ 文件已修改
src/api/customerService/index.js 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/viewIndex.js 58 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/Echarts/echarts.vue 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/customerService/afterSalesHandling/index.vue 231 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/equipmentManagement/attendanceManagement/index.vue 403 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/index.vue 661 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/receiptManagement/Record.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/stockReport/index.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/transportTaskManagement/index.vue 692 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleFuelManagement/index.vue 556 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/inventoryManagement/vehicleManagement/index.vue 581 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/personnelManagement/attendanceCheckin/index.vue 469 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/procurementManagement/procurementLedger/index.vue 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/qualityManagement/afterSalesTraceability/index.vue 595 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/financialAnalysis/components/center-bottom.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-center.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/center-top.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/left-top.vue 12 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/productionAnalysis/components/right-top.vue 60 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue 105 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/left-bottom.vue 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/reportAnalysis/qualityAnalysis/index.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/menu/index.vue 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/customerService/index.js
@@ -42,6 +42,40 @@
  })
}
// å”®åŽå¤„理-附件列表
export function afterSalesServiceFileListPage(query) {
  return request({
    url: '/afterSalesService/file/listPage',
    method: 'get',
    params: query,
  })
}
// å”®åŽå¤„理-附件新增
export function afterSalesServiceFileAdd(data) {
  return request({
    url: '/afterSalesService/file/add',
    method: 'post',
    data,
  })
}
// å”®åŽå¤„理-附件删除
export function afterSalesServiceFileDel(ids) {
  return request({
    url: '/afterSalesService/file/del',
    method: 'delete',
    data: ids,
  })
}
// å”®åŽå¤„理-维修记录列表
export function afterSalesServiceRepairListPage(query) {
  return request({
    url: '/afterSalesService/repair/listPage',
    method: 'get',
    params: query,
  })
}
// ä¸´æœŸå”®åŽç®¡ç†-分页查询
export function expiryAfterSalesListPage(query) {
  return request({
src/api/viewIndex.js
@@ -1,12 +1,21 @@
// é¦–页接口
import request from "@/utils/request";
//  å·¥å•执行效率分析
export const workOrderEfficiencyAnalysis = (query) => {
//  å·¥åºæ•°æ®ç”Ÿäº§ç»Ÿè®¡æ˜Žç»†
export const processDataProductionStatistics = (params) => {
  return request({
    url: "/home/workOrderEfficiencyAnalysis",
    url: "/home/processDataProductionStatistics",
    method: "get",
    params: query,
    params,
  });
};
//  è´¨é‡ç»Ÿè®¡
export const qualityInspectionStatistics = (params) => {
  return request({
    url: "/home/qualityInspectionStatistics",
    method: "get",
    params,
  });
};
@@ -91,13 +100,24 @@
    method: "get",
  });
};
// è´¨æ£€åˆ†æž
export const qualityStatistics = () => {
// è´¨æ£€åˆ†æžï¼ˆå¯ä¼  dateType: 1周 2月 3季度)
export const qualityStatistics = (params) => {
  return request({
    url: "/home/qualityStatistics",
    method: "get",
    params,
  });
};
// å·¥å•执行效率分析(dateType: 1周 2月 3季度)
export const workOrderEfficiencyAnalysis = (params) => {
  return request({
    url: "/home/workOrderEfficiencyAnalysis",
    method: "get",
    params,
  });
};
// ç”Ÿäº§æ ¸ç®—分析
export const productionAccountingAnalysis = (query) => {
  return request({
@@ -135,6 +155,14 @@
export const getProgressStatistics = () => {
  return request({
    url: "/home/progressStatistics",
    method: "get",
  });
};
// è®¢å•数量统计(生产订单数、已完成订单数、待生产订单数)
export const orderCount = () => {
  return request({
    url: "/home/orderCount",
    method: "get",
  });
};
@@ -207,6 +235,15 @@
  });
};
// å·¥åºäº§å‡ºåˆ†æžï¼ˆdateType: 1周 2月 3季度)
export const processOutputAnalysis = (params) => {
  return request({
    url: "/home/processOutputAnalysis",
    method: "get",
    params,
  });
};
// åŽŸææ–™é‡‡è´­é‡‘é¢å æ¯”
export const rawMaterialPurchaseAmountRatio = () => {
  return request({
@@ -241,6 +278,15 @@
  });
};
// æŠ•入产出分析
export const inputOutputAnalysis = (params) => {
  return request({
    url: "/home/inputOutputAnalysis",
    method: "get",
    params,
  });
};
// äº§å“å‘¨è½¬å¤©æ•°
export const productTurnoverDays = () => {
  return request({
src/components/Echarts/echarts.vue
@@ -9,7 +9,7 @@
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import * as echarts from 'echarts'
const emit = defineEmits(['finished'])
const emit = defineEmits(['finished', 'click'])
// Props
const props = defineProps({
@@ -128,6 +128,9 @@
  chartInstance = echarts.init(chartRef.value)
  finishedHandler = () => emit('finished')
  chartInstance.on('finished', finishedHandler)
  chartInstance.on('click', (params) => {
    emit('click', params)
  })
  renderChart()
  // setOption åŽè¡¥ä¸€æ¬¡ resize,确保首屏尺寸正确
  nextTick(() => {
src/views/customerService/afterSalesHandling/index.vue
@@ -46,15 +46,64 @@
            ></PIMTable>
        </div>
        <form-dia ref="formDia" @close="handleQuery"></form-dia>
        <FileListDialog
            ref="fileListRef"
            v-model="fileListDialogVisible"
            title="售后附件"
            :show-upload-button="true"
            :show-delete-button="true"
            :upload-method="handleFileUpload"
            :delete-method="handleFileDelete"
        />
        <el-dialog
            v-model="repairDialogVisible"
            title="维修记录"
            width="700px"
            destroy-on-close
            @close="repairRecordList = []"
        >
            <el-table
                :data="repairRecordList"
                border
                v-loading="repairRecordLoading"
                max-height="400"
            >
                <el-table-column type="index" label="序号" width="55" align="center" />
                <el-table-column label="维修日期" prop="maintenanceTime" min-width="120" show-overflow-tooltip>
                    <template #default="{ row }">
                        {{ row.maintenanceTime || row.repairTime || '-' }}
                    </template>
                </el-table-column>
                <el-table-column label="维修人" prop="maintenanceName" min-width="100" show-overflow-tooltip>
                    <template #default="{ row }">
                        {{ row.maintenanceName || row.repairName || '-' }}
                    </template>
                </el-table-column>
                <el-table-column label="维修结果" prop="maintenanceResult" min-width="180" show-overflow-tooltip />
            </el-table>
            <template #footer>
                <el-button @click="repairDialogVisible = false">关闭</el-button>
            </template>
        </el-dialog>
    </div>
</template>
<script setup>
import {Search} from "@element-plus/icons-vue";
import {onMounted, ref, getCurrentInstance, nextTick} from "vue";
import { onMounted, ref, reactive, toRefs, getCurrentInstance, nextTick } from "vue";
import FormDia from "@/views/customerService/afterSalesHandling/components/formDia.vue";
import FileListDialog from "@/components/Dialog/FileListDialog.vue";
import {ElMessageBox} from "element-plus";
import {afterSalesServiceDelete, afterSalesServiceListPage} from "@/api/customerService/index.js";
import request from "@/utils/request";
import { getToken } from "@/utils/auth";
import {
    afterSalesServiceDelete,
    afterSalesServiceListPage,
    afterSalesServiceFileListPage,
    afterSalesServiceFileAdd,
    afterSalesServiceFileDel,
    afterSalesServiceRepairListPage,
} from "@/api/customerService/index.js";
import useUserStore from "@/store/modules/user.js";
const { proxy } = getCurrentInstance();
const userStore = useUserStore()
@@ -134,7 +183,7 @@
        label: "操作",
        align: "center",
        fixed: 'right',
        width: 120,
        width: 240,
        operation: [
            {
                name: "处理",
@@ -151,6 +200,22 @@
                type: "text",
                clickFun: (row) => {
                    openForm("view", row);
                },
            },
            // TODO ä¸ºå†™æŠ¥å‘Šæ·»åŠ çš„
            {
                name: "附件",
                type: "text",
                clickFun: (row) => {
                    openFilesFormDia(row);
                },
            },
            // TODO ä¸ºå†™æŠ¥å‘Šæ·»åŠ çš„
            {
                name: "维修记录",
                type: "text",
                clickFun: (row) => {
                    openRepairDialog(row);
                },
            },
        ],
@@ -170,6 +235,166 @@
    selectedRows.value = selection;
};
const formDia = ref()
const fileListRef = ref(null)
const fileListDialogVisible = ref(false)
const currentFileRow = ref(null)
const repairDialogVisible = ref(false)
const repairRecordList = ref([])
const repairRecordLoading = ref(false)
// æ‰“开维修记录弹框
const openRepairDialog = async (row) => {
    repairDialogVisible.value = true
    repairRecordLoading.value = true
    repairRecordList.value = []
    try {
        const res = await afterSalesServiceRepairListPage({
            afterSalesServiceId: row.id,
            current: 1,
            size: 100,
        })
        if (res.code === 200 && res.data?.records) {
            repairRecordList.value = res.data.records
        }
    } catch (error) {
        proxy.$modal.msgError("获取维修记录失败")
    } finally {
        repairRecordLoading.value = false
    }
}
// æ‰“开附件弹框-----  TODO:接口是没有对接的,需要新增接口,为写报告添加的
const openFilesFormDia = async (row) => {
    currentFileRow.value = row
    try {
        const res = await afterSalesServiceFileListPage({
            afterSalesServiceId: row.id,
            current: 1,
            size: 100,
        })
        if (res.code === 200 && fileListRef.value) {
            const fileList = (res.data?.records || []).map((item) => ({
                name: item.name || item.fileName,
                url: item.url || item.fileUrl,
                id: item.id,
                ...item,
            }))
            fileListRef.value.open(fileList)
            fileListDialogVisible.value = true
        } else {
            fileListRef.value?.open([])
            fileListDialogVisible.value = true
        }
    } catch (error) {
        proxy.$modal.msgError("获取附件列表失败")
        fileListRef.value?.open([])
        fileListDialogVisible.value = true
    }
}
// ä¸Šä¼ é™„ä»¶
const handleFileUpload = async () => {
    if (!currentFileRow.value) {
        proxy.$modal.msgWarning("请先选择数据")
        return
    }
    return new Promise((resolve) => {
        const input = document.createElement("input")
        input.type = "file"
        input.style.display = "none"
        input.onchange = async (e) => {
            const file = e.target.files[0]
            if (!file) {
                resolve(null)
                return
            }
            try {
                const formData = new FormData()
                formData.append("file", file)
                const uploadRes = await request({
                    url: "/file/upload",
                    method: "post",
                    data: formData,
                    headers: {
                        "Content-Type": "multipart/form-data",
                        Authorization: `Bearer ${getToken()}`,
                    },
                })
                if (uploadRes.code === 200) {
                    const fileData = {
                        afterSalesServiceId: currentFileRow.value.id,
                        name: uploadRes.data?.originalName || file.name,
                        url: uploadRes.data?.tempPath || uploadRes.data?.url,
                    }
                    const saveRes = await afterSalesServiceFileAdd(fileData)
                    if (saveRes.code === 200) {
                        proxy.$modal.msgSuccess("文件上传成功")
                        const listRes = await afterSalesServiceFileListPage({
                            afterSalesServiceId: currentFileRow.value.id,
                            current: 1,
                            size: 100,
                        })
                        if (listRes.code === 200 && fileListRef.value) {
                            const fileList = (listRes.data?.records || []).map((item) => ({
                                name: item.name || item.fileName,
                                url: item.url || item.fileUrl,
                                id: item.id,
                                ...item,
                            }))
                            fileListRef.value.setList(fileList)
                        }
                        resolve({ name: fileData.name, url: fileData.url, id: saveRes.data?.id })
                    } else {
                        proxy.$modal.msgError(saveRes.msg || "文件保存失败")
                        resolve(null)
                    }
                } else {
                    proxy.$modal.msgError(uploadRes.msg || "文件上传失败")
                    resolve(null)
                }
            } catch (err) {
                proxy.$modal.msgError("文件上传失败")
                resolve(null)
            } finally {
                document.body.removeChild(input)
            }
        }
        document.body.appendChild(input)
        input.click()
    })
}
// åˆ é™¤é™„ä»¶
const handleFileDelete = async (row) => {
    try {
        const res = await afterSalesServiceFileDel([row.id])
        if (res.code === 200) {
            proxy.$modal.msgSuccess("删除成功")
            if (currentFileRow.value && fileListRef.value) {
                const listRes = await afterSalesServiceFileListPage({
                    afterSalesServiceId: currentFileRow.value.id,
                    current: 1,
                    size: 100,
                })
                if (listRes.code === 200) {
                    const fileList = (listRes.data?.records || []).map((item) => ({
                        name: item.name || item.fileName,
                        url: item.url || item.fileUrl,
                        id: item.id,
                        ...item,
                    }))
                    fileListRef.value.setList(fileList)
                }
            }
        } else {
            proxy.$modal.msgError(res.msg || "删除失败")
            return false
        }
    } catch (error) {
        proxy.$modal.msgError("删除失败")
        return false
    }
}
// æŸ¥è¯¢åˆ—表
/** æœç´¢æŒ‰é’®æ“ä½œ */
src/views/equipmentManagement/attendanceManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,403 @@
<template>
  <div class="app-container">
    <!-- æœåŠ¡è¯„ä»·æ¦‚è§ˆï¼šæ¨¡æ‹Ÿå‘˜å·¥ä¸šç»©è¯„åˆ† -->
    <el-row :gutter="16" class="mb16">
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">本月平均评分</div>
          <div class="kpi-value">
            {{ overallAvgScore.toFixed(1) }}
            <span class="kpi-unit">分</span>
          </div>
          <el-rate v-model="overallAvgScore" disabled show-score score-template="{value} / 5" />
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">已评价维修工单</div>
          <div class="kpi-value">
            {{ ratedCount }}
            <span class="kpi-unit">单</span>
          </div>
        </el-card>
      </el-col>
      <el-col :span="8">
        <el-card shadow="never">
          <div class="kpi-title">待评价维修工单</div>
          <div class="kpi-value kpi-warning">
            {{ pendingCount }}
            <span class="kpi-unit">单</span>
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æŸ¥è¯¢æ¡ä»¶ï¼šç®¡ç†å‘˜æŒ‰å·¥ç¨‹å¸ˆ / å®¢æˆ· / æ—¶é—´è¿½æº¯è¯„ä»· -->
    <div class="search_form">
      <div>
        <span class="search_title">维修工程师:</span>
        <el-input
          v-model="searchForm.engineerName"
          placeholder="请输入工程师姓名"
          style="width: 180px"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">客户名称:</span>
        <el-input
          v-model="searchForm.customerName"
          placeholder="请输入客户名称"
          style="width: 180px"
          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
        />
        <span class="search_title ml10">评价状态:</span>
        <el-select
          v-model="searchForm.status"
          placeholder="请选择"
          style="width: 140px"
          clearable
        >
          <el-option label="待评价" value="pending" />
          <el-option label="已评价" value="rated" />
        </el-select>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          å¯¼å‡ºè¯„价统计
        </el-button>
      </div>
    </div>
    <!-- ç»´ä¿®è¯„价列表:模拟“维修完成后触发评价”场景 -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column prop="orderNo" label="维修工单号" width="160" show-overflow-tooltip />
        <el-table-column prop="deviceName" label="设备名称" width="160" show-overflow-tooltip />
        <el-table-column prop="customerName" label="客户名称" width="180" show-overflow-tooltip />
        <el-table-column prop="engineerName" label="维修工程师" width="120" />
        <el-table-column prop="completeTime" label="维修完成时间" width="180" />
        <el-table-column prop="score" label="星级评分" width="140" align="center">
          <template #default="scope">
            <el-rate v-if="scope.row.score" v-model="scope.row.score" disabled />
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column prop="status" label="评价状态" width="100" align="center">
          <template #default="scope">
            <el-tag
              :type="scope.row.status === 'rated' ? 'success' : 'warning'"
              size="small"
            >
              {{ scope.row.status === 'rated' ? '已评价' : '待评价' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="feedback" label="客户反馈" show-overflow-tooltip />
        <el-table-column label="操作" width="160" align="center" fixed="right">
          <template #default="scope">
            <el-button
              v-if="scope.row.status === 'pending'"
              type="primary"
              link
              size="small"
              @click="openEvaluate(scope.row)"
            >
              åŽ»è¯„ä»·
            </el-button>
            <el-button
              v-else
              type="primary"
              link
              size="small"
              @click="openEvaluate(scope.row)"
            >
              æŸ¥çœ‹ / ä¿®æ”¹è¯„ä»·
            </el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- è¯„价弹框:模拟“维修完成后弹出客户端评价” -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="520px"
      destroy-on-close
    >
      <div class="dialog-order-info" v-if="currentOrder">
        <div>维修工单:{{ currentOrder.orderNo }}</div>
        <div>设备名称:{{ currentOrder.deviceName }}</div>
        <div>维修工程师:{{ currentOrder.engineerName }}</div>
      </div>
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="100px"
      >
        <el-form-item label="星级评分:" prop="score">
          <el-rate v-model="form.score" :max="5" />
        </el-form-item>
        <el-form-item label="文字反馈:" prop="feedback">
          <el-input
            v-model="form.feedback"
            type="textarea"
            :rows="4"
            placeholder="请填写对本次维修服务的评价,如响应速度、专业程度、沟通体验等"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="dialogVisible = false">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">提 äº¤ è¯„ ä»·</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed } from "vue";
import { ElMessage } from "element-plus";
// æ¨¡æ‹Ÿç»´ä¿®å·¥å• + å®¢æˆ·è¯„价数据
const rawOrders = ref([
  {
    id: 1,
    orderNo: "WX-2024-1201-001",
    deviceName: "空压机 A1 å·",
    customerName: "华南电子科技有限公司",
    engineerName: "王师傅",
    completeTime: "2024-12-01 10:30:00",
    completeDate: "2024-12-01",
    status: "rated",
    score: 5,
    feedback: "维修非常专业,响应速度快,现场解释也很清晰,满意。",
  },
  {
    id: 2,
    orderNo: "WX-2024-1201-002",
    deviceName: "注塑机 B3 å·",
    customerName: "华东精密制造有限公司",
    engineerName: "李师傅",
    completeTime: "2024-12-01 15:20:00",
    completeDate: "2024-12-01",
    status: "rated",
    score: 4,
    feedback: "整体还不错,就是到场时间稍微长了一点,希望后面能再快一些。",
  },
  {
    id: 3,
    orderNo: "WX-2024-1202-003",
    deviceName: "焊接机器人 C2 å·",
    customerName: "西南新能源科技股份",
    engineerName: "张师傅",
    completeTime: "2024-12-02 11:05:00",
    completeDate: "2024-12-02",
    status: "pending",
    score: null,
    feedback: "",
  },
  {
    id: 4,
    orderNo: "WX-2024-1203-005",
    deviceName: "测试台 D1 å·",
    customerName: "北方汽车零部件有限公司",
    engineerName: "王师傅",
    completeTime: "2024-12-03 09:50:00",
    completeDate: "2024-12-03",
    status: "pending",
    score: null,
    feedback: "",
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  engineerName: "",
  customerName: "",
  dateRange: [],
  status: "",
});
// åˆ—表数据
const tableData = ref([...rawOrders.value]);
// ç»Ÿè®¡ï¼šæ•´ä½“评分、已评价 / å¾…评价数量
const ratedOrders = computed(() =>
  rawOrders.value.filter((o) => o.status === "rated" && o.score)
);
const overallAvgScore = computed(() => {
  if (!ratedOrders.value.length) return 0;
  const sum = ratedOrders.value.reduce((acc, cur) => acc + (cur.score || 0), 0);
  return sum / ratedOrders.value.length;
});
const ratedCount = computed(() => ratedOrders.value.length);
const pendingCount = computed(
  () => rawOrders.value.filter((o) => o.status === "pending").length
);
// æŸ¥è¯¢ / é‡ç½®
const recomputeTable = () => {
  const list = rawOrders.value.filter((item) => {
    if (
      searchForm.engineerName &&
      !item.engineerName.includes(searchForm.engineerName.trim())
    ) {
      return false;
    }
    if (
      searchForm.customerName &&
      !item.customerName.includes(searchForm.customerName.trim())
    ) {
      return false;
    }
    if (searchForm.status && item.status !== searchForm.status) {
      return false;
    }
    if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
      const [start, end] = searchForm.dateRange;
      if (item.completeDate < start || item.completeDate > end) {
        return false;
      }
    }
    return true;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.engineerName = "";
  searchForm.customerName = "";
  searchForm.dateRange = [];
  searchForm.status = "";
  recomputeTable();
};
// å¯¼å‡ºï¼ˆæ¼”示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,评价导出功能未对接实际接口");
};
// è¯„价弹框
const dialogVisible = ref(false);
const dialogTitle = ref("维修服务评价");
const currentOrder = ref(null);
const formRef = ref(null);
const form = reactive({
  score: 0,
  feedback: "",
});
const rules = {
  score: [{ required: true, message: "请选择星级评分", trigger: "change" }],
  feedback: [{ required: true, message: "请填写文字反馈", trigger: "blur" }],
};
// æ‰“开评价:模拟“维修完成确认后弹出评价弹框”
const openEvaluate = (row) => {
  currentOrder.value = row;
  dialogTitle.value =
    row.status === "pending" ? "维修服务评价" : "查看 / ä¿®æ”¹è¯„ä»·";
  form.score = row.score || 0;
  form.feedback = row.feedback || "";
  dialogVisible.value = true;
};
// æäº¤è¯„价:同步到本地“员工业绩统计”
const handleSubmit = () => {
  if (!formRef.value) return;
  formRef.value.validate((valid) => {
    if (!valid || !currentOrder.value) return;
    const target = rawOrders.value.find((o) => o.id === currentOrder.value.id);
    if (target) {
      target.score = form.score;
      target.feedback = form.feedback;
      target.status = "rated";
    }
    ElMessage.success("评价提交成功,已同步至员工业绩统计");
    dialogVisible.value = false;
    recomputeTable();
  });
};
// åˆå§‹åŒ–列表
recomputeTable();
</script>
<style scoped lang="scss">
.mb16 {
  margin-bottom: 16px;
}
.kpi-title {
  font-size: 13px;
  color: #909399;
}
.kpi-value {
  margin-top: 6px;
  font-size: 24px;
  font-weight: 600;
  color: #303133;
}
.kpi-unit {
  font-size: 12px;
  margin-left: 4px;
  color: #909399;
}
.kpi-warning {
  color: #e6a23c;
}
.dialog-order-info {
  margin-bottom: 12px;
  font-size: 13px;
  color: #606266;
  line-height: 1.8;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/index.vue
@@ -5,16 +5,29 @@
            <!-- å·¦ï¼šä¼ä¸šä¿¡æ¯+三大数据卡片(上下排列) -->
            <div class="top-left">
                <div class="company-info">
                    <div class="section-title">登陆信息</div>
                    <div style="display: flex;align-items: center;gap: 20px">
                        <img :src="userStore.avatar" class="avatar" alt=""/>
                        <div class="company-card">
                            <div class="company-name">{{userStore.name}}</div>
                            <div class="company-meta">{{userStore.roleName}}</div>
          <!-- é¡¶éƒ¨é—®å€™æ¡ -->
          <div class="welcome-banner">
            <div class="welcome-title">
              <span class="welcome-user">{{ userStore.roleName || '系统管理员' }}</span>
              <span> æ‚¨å¥½ï¼ç¥æ‚¨å¼€å¿ƒæ¯ä¸€å¤©</span>
                        </div>
                        <div style="display: flex;align-items: center;gap: 8px">
                            <el-icon color="#5053B5" size="22"><Clock /></el-icon>
                            <span>登陆日期:{{userStore.currentLoginTime}}</span>
            <div class="welcome-time">登录于: {{ userStore.currentLoginTime }}</div>
          </div>
          <!-- ç”¨æˆ·ä¿¡æ¯å¡ç‰‡ -->
          <div class="user-card">
            <img :src="userStore.avatar" class="avatar" alt="" />
            <div class="user-card-main">
              <div class="user-name">{{ userStore.name }}</div>
              <div class="user-role">{{ userStore.roleName }}</div>
              <div class="user-meta">
                <span>{{ userStore.phoneNumber || '123456789' }}</span>
                <span class="sep">|</span>
                <span>{{ userStore.deptName || '组织架构' }}</span>
                <span class="sep">|</span>
                <span>{{ userStore.postName || '岗位名' }}</span>
              </div>
            </div>
                        </div>
                    </div>
                </div>
@@ -60,7 +73,6 @@
                        </div>
                    </div>
                </div>
            </div>
            <!-- å³ï¼šå¾…办事项 -->
            <div class="todo-panel">
                <div class="section-title">待办事项</div>
@@ -81,7 +93,81 @@
                </div>
            </div>
        </div>
    <div class="dashboard-row">
      <div class="main-panel process-panel">
        <div class="process-panel__header">
          <div class="section-title">工序数据生产统计明细</div>
          <div style="display: flex; gap: 10px; align-items: center;">
            <el-button type="primary" size="small" plain icon="Filter" @click="openProcessDialog">选择工序</el-button>
            <el-button type="info" size="small" plain icon="Refresh" @click="resetProcessFilter">重置</el-button>
            <el-radio-group v-model="processRange" size="small" @change="refreshProcessStats">
              <el-radio-button :value="1">日</el-radio-button>
              <el-radio-button :value="2">周</el-radio-button>
              <el-radio-button :value="3">月</el-radio-button>
            </el-radio-group>
          </div>
        </div>
        
        <div class="process-panel__body">
          <div class="process-panel__chart">
            <Echarts :chartStyle="{ width: '100%', height: '100%' }" :grid="processGrid" :series="processSeries"
              :tooltip="processTooltip" :xAxis="processXAxis" :yAxis="processYAxis" style="height: 100%"
              @click="handleChartClick" />
          </div>
          <div class="process-panel__aside">
            <div class="process-legend">
              <div class="process-legend__item">
                <span class="dot dot-blue"></span><span>投入量</span>
              </div>
              <div class="process-legend__item">
                <span class="dot dot-yellow"></span><span>报废量</span>
              </div>
              <div class="process-legend__item">
                <span class="dot dot-teal"></span><span>产出量</span>
              </div>
            </div>
            <div class="process-card process-card--name">{{ processAside.processName }}</div>
            <div class="process-card">
              <div class="process-card__label">累计总投入</div>
              <div class="process-card__value">{{ formatAmount(processAside.totalInput) }}<span class="unit">元</span>
              </div>
            </div>
            <div class="process-card">
              <div class="process-card__label">累计总报废</div>
              <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }}<span class="unit">元</span>
              </div>
            </div>
            <div class="process-card">
              <div class="process-card__label">累计总产出</div>
              <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }}<span class="unit">元</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <!-- å·¥åºé€‰æ‹©å¼¹çª— -->
    <el-dialog v-model="processDialogVisible" title="选择工序" width="500px" append-to-body>
      <div class="process-selection-wrapper">
        <el-checkbox-group v-model="tempProcessIds">
          <div class="process-grid">
            <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border>
              {{ item.name }}
            </el-checkbox>
          </div>
        </el-checkbox-group>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="processDialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleProcessDialogConfirm">确认</el-button>
        </span>
      </template>
    </el-dialog>
        <!-- ä¸­éƒ¨æ¨ªå‘两栏 -->
        <div class="dashboard-row">
            <div class="main-panel">
@@ -98,10 +184,10 @@
                        </div>
                    </div>
                </div>
                <div style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px">
        <div
          style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px">
                    <div>
                        <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie"
                                         :series="materialPieSeries"
            <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie" :series="materialPieSeries"
                                         :tooltip="pieTooltip"></Echarts>
                    </div>
                    <ul class="contract-list">
@@ -124,48 +210,42 @@
<!--                        <el-radio-button label="按季度" :value="3" />-->
<!--                    </el-radio-group>-->
                </div>
                <Echarts ref="chart"
                                 :color="barColors2"
                                 :chartStyle="chartStyle"
                                 :grid="grid"
                                 :series="barSeries"
                                 :tooltip="tooltip"
                                 :xAxis="xAxis"
                                 :yAxis="yAxis"
                                 style="height: 260px"></Echarts>
        <Echarts ref="chart" :color="barColors2" :chartStyle="chartStyle" :grid="grid" :series="barSeries"
          :tooltip="tooltip" :xAxis="xAxis" :yAxis="yAxis" style="height: 260px"></Echarts>
            </div>
        </div>
        
        <!-- åº•部横向两栏 -->
        <div class="dashboard-row">
<!--            <div class="main-panel">-->
<!--                <div class="section-title">质量统计</div>-->
<!--                <div class="quality-cards">-->
<!--                    <div class="quality-card one">原材料已检测数 <span>{{qualityStatisticsObject.supplierNum}}ä»¶</span></div>-->
<!--                    <div class="quality-card two">过程检验数量 <span>{{qualityStatisticsObject.processNum}}ä»¶</span></div>-->
<!--                    <div class="quality-card three">出厂已检数量 <span>{{qualityStatisticsObject.factoryNum}}ä»¶</span></div>-->
<!--                </div>-->
<!--                <Echarts ref="chart"-->
<!--                                 :chartStyle="chartStyle"-->
<!--                                 :grid="grid"-->
<!--                                 :legend="barLegend"-->
<!--                                 :series="barSeries1"-->
<!--                                 :tooltip="tooltip"-->
<!--                                 :xAxis="xAxis1"-->
<!--                                 :yAxis="yAxis1"-->
<!--                                 style="height: 260px"></Echarts>-->
<!--            </div>-->
      <div class="main-panel">
        <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;">
          <div class="section-title" style="margin-bottom: 0;">质量统计</div>
          <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo">
            <el-radio-button :value="1">周</el-radio-button>
            <el-radio-button :value="2">月</el-radio-button>
            <el-radio-button :value="3">季度</el-radio-button>
          </el-radio-group>
        </div>
        <div class="quality-cards">
          <div class="quality-card one">原材料已检测数 <span>{{ qualityStatisticsObject.supplierNum }}ä»¶</span></div>
          <div class="quality-card two">过程检验数量 <span>{{ qualityStatisticsObject.processNum }}ä»¶</span></div>
          <div class="quality-card three">出厂已检数量 <span>{{ qualityStatisticsObject.factoryNum }}ä»¶</span></div>
        </div>
        <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="barLegend" :series="barSeries1"
          :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1" style="height: 260px"></Echarts>
      </div>
            <div class="main-panel">
                <div class="section-title">回款与开票分析</div>
                <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
                                 :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;"></Echarts>
        <Echarts ref="invoiceChart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries"
          :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;" />
            </div>
        </div>
    </div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed, reactive } from 'vue'
import Echarts from "@/components/Echarts/echarts.vue";
import * as echarts from 'echarts';
import useUserStore from "@/store/modules/user.js";
@@ -173,11 +253,20 @@
    analysisCustomerContractAmounts, getAmountHalfYear,
    getBusiness,
    homeTodos,
    qualityStatistics,
    statisticsReceivablePayable
  processDataProductionStatistics,
  statisticsReceivablePayable,
  qualityInspectionStatistics
} from "@/api/viewIndex.js";
import { list } from '@/api/productionManagement/productionProcess';
const userStore = useUserStore()
const processOptions = ref([])
const selectedProcessIds = ref([])
const tempProcessIds = ref([])
const processDialogVisible = ref(false)
const activeProcessIndex = ref(0)
const businessInfo = ref({
    inventoryNum: 0,
@@ -208,6 +297,7 @@
        }
    },
])
const barSeries1 = ref([
    {
        name: '原材料不合格数',
@@ -340,6 +430,7 @@
// å¾…办事项
const todoList = ref([])
const radio1 = ref(1)
const qualityRange = ref(1)
// å›¾è¡¨å¼•用
const barChart = ref(null)
@@ -358,6 +449,7 @@
    statisticsReceivable()
    qualityStatisticsInfo()
    getAmountHalfYearNum()
  getProcessList()
})
// æ•°æ®ç»Ÿè®¡
const getBusinessData = () => {
@@ -384,8 +476,37 @@
        todoList.value = res.data
    })
}
// èŽ·å–å·¥åºåˆ—è¡¨
const getProcessList = () => {
  list().then(res => {
    processOptions.value = res.data
  })
}
const openProcessDialog = () => {
  tempProcessIds.value = [...selectedProcessIds.value]
  processDialogVisible.value = true
}
const handleProcessDialogConfirm = () => {
  selectedProcessIds.value = [...tempProcessIds.value]
  processDialogVisible.value = false
  refreshProcessStats()
}
const resetProcessFilter = () => {
  selectedProcessIds.value = []
  tempProcessIds.value = []
  refreshProcessStats()
}
const handleChartClick = (params) => {
  if (params && params.dataIndex !== undefined) {
    activeProcessIndex.value = params.dataIndex
  }
}
// åº”付应收统计
const statisticsReceivable = (type) => {
const statisticsReceivable = () => {
    statisticsReceivablePayable({type: radio1.value}).then((res) => {
        barSeries.value[0].data = [
            // { value: res.data.prepayMoney, itemStyle: { color: barColors2[0] } },
@@ -397,7 +518,11 @@
}
// è´¨æ£€ç»Ÿè®¡
const qualityStatisticsInfo = () => {
    qualityStatistics().then((res) => {
  qualityInspectionStatistics({ type: qualityRange.value }).then((res) => {
    xAxis1.value[0].data = []
    barSeries1.value[0].data = []
    barSeries1.value[1].data = []
    barSeries1.value[2].data = []
        res.data.item.forEach(item => {
            xAxis1.value[0].data.push(item.date)
            barSeries1.value[0].data.push(item.supplierNum)
@@ -484,6 +609,121 @@
        }
    ]
}
// å·¥åºæ•°æ®ç”Ÿäº§ç»Ÿè®¡æ˜Žç»†ï¼ˆå‡æ•°æ® + å›¾è¡¨ï¼‰
const processRange = ref(1)
const processChartData = ref([])
const processXAxis = ref([
  {
    nameTextStyle: { color: 'rgba(0,0,0,0.35)', fontSize: 12 },
    axisLabel: { color: 'rgba(0,0,0,0.35)' },
    splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)', type: 'dashed' } },
  },
])
const processYAxis = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLine: { show: false },
    axisLabel: { color: 'rgba(0,0,0,0.45)' },
    data: [],
  },
])
const processGrid = reactive({ left: 0, right: 100, top: 30, bottom: 20, containLabel: true })
const processTooltip = reactive({
  trigger: 'axis',
  axisPointer: { type: 'shadow' },
  formatter: (params) => {
    const name = params?.[0]?.name ?? ''
    const list = Array.isArray(params) ? params : []
    const lines = list
      .map((p) => {
        const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>`
        return `${colorBox}${p.seriesName} <b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>`
      })
      .join('<br/>')
    return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>`
  },
})
const processSeries = computed(() => {
  const input = processChartData.value.map((i) => i.input)
  const scrap = processChartData.value.map((i) => i.scrap)
  const output = processChartData.value.map((i) => i.output)
  return [
    {
      name: '投入量',
      type: 'bar',
      stack: 'total',
      barWidth: 22,
      itemStyle: { color: '#1E5BFF', borderRadius: [6, 0, 0, 6] },
      data: input,
    },
    {
      name: '报废量',
      type: 'bar',
      stack: 'total',
      barWidth: 22,
      itemStyle: { color: '#F7B500' },
      data: scrap,
    },
    {
      name: '产出量',
      type: 'bar',
      stack: 'total',
      barWidth: 22,
      itemStyle: { color: '#19C6C6', borderRadius: [0, 6, 6, 0] },
      data: output,
    },
  ]
})
const processAside = computed(() => {
  const list = processChartData.value
  const item = list[activeProcessIndex.value] || {}
  return {
    processName: item.name || '暂无数据',
    totalInput: item.input || 0,
    totalScrap: item.scrap || 0,
    totalOutput: item.output || 0,
  }
})
const formatAmount = (n) => {
  const num = Number(n || 0)
  return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}
const refreshProcessStats = () => {
  processDataProductionStatistics({
    type: processRange.value,
    processIds: selectedProcessIds.value.length > 0 ? selectedProcessIds.value.join(',') : null
  }).then(res => {
    processChartData.value = res.data.map(item => ({
      name: item.processName,
      input: item.totalInput,
      scrap: item.totalScrap,
      output: item.totalOutput
    }))
    processYAxis.value[0].data = processChartData.value.map((i) => i.name)
    activeProcessIndex.value = 0
  })
}
onMounted(() => {
  getBusinessData()
  analysisCustomer()
  todoInfoS()
  statisticsReceivable()
  qualityStatisticsInfo()
  getAmountHalfYearNum()
  refreshProcessStats()
})
</script>
<style scoped>
@@ -493,62 +733,101 @@
    padding: 20px;
    box-sizing: border-box;
}
.dashboard-top {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
}
.company-info {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 20px;
    min-width: 0;
    background-color: #EFF2FB; /* ä½¿ç”¨æŒ‡å®šçš„背景颜色 */
    background-image: url("../assets/images/denglu.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
    border-radius: 12px;
    height: 138px;
}
.avatar {
    width: 60px;
    height: 60px;
    border-radius: 50%;
    object-fit: contain;
    background: #fff;
    border: 1px solid #eee;
}
.company-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    position: relative;
    padding-right: 15px;
  align-items: flex-start;
  justify-content: space-evenly;
}
.company-card::after {
    content: '';
    position: absolute;
    right: 0;
    top: 0;
    bottom: 0;
    width: 1px;
    background-color: #C9C5C5;
    border-radius: 2px;
.company-info {
  padding: 0;
  overflow: hidden;
  border-radius: 12px;
  background: #fff;
  height: 100%;
}
.company-name {
    font-weight: 400;
.welcome-banner {
  padding: 10px 10px;
  background: linear-gradient(135deg, rgba(229, 240, 255, 0.9), rgba(214, 232, 255, 0.7), rgba(207, 236, 255, 0.9));
}
.welcome-title {
  font-size: 18px;
  font-weight: 700;
  color: #222;
  line-height: 1.3;
}
.welcome-user {
  margin-right: 6px;
}
.welcome-time {
  margin-top: 10px;
    font-size: 16px;
    color: #161A9A;
  color: rgba(0, 0, 0, 0.55);
}
.company-meta {
    font-weight: 400;
.user-card {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 18px 22px;
}
.user-card-main {
  display: flex;
  flex-direction: column;
  gap: 5px;
  min-width: 0;
}
.user-name {
  font-size: 16px;
  font-weight: bold;
  color: #111;
  letter-spacing: 1px;
}
.user-role {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  padding: 5px 10px;
  background: rgba(245, 246, 248, 1);
  color: #333;
  width: fit-content;
  font-weight: 600;
}
.user-meta {
    font-size: 12px;
    color: #818185;
  color: rgba(0, 0, 0, 0.55);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.user-meta .sep {
  margin: 0 10px;
  color: rgba(0, 0, 0, 0.25);
}
.avatar {
  width: 90px;
  height: 90px;
  border-radius: 50%;
  object-fit: cover;
  flex: 0 0 auto;
}
.data-cards {
  width: 50%;
    display: flex;
    gap: 16px;
    justify-content: flex-start;
@@ -556,17 +835,20 @@
    border-radius: 12px;
    padding: 20px;
}
.data-title {
    font-weight: 700;
    font-size: 26px;
    color: #FFFFFF;
}
.data-num {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-top: 20px;
}
.data-card {
    background: #fff;
    border-radius: 12px;
@@ -578,55 +860,66 @@
    width: 32%;
    height: 140px;
}
.data-card.sales {
    background-image: url("../assets/images/xioashoushuju.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.data-card.purchase {
    background-image: url("../assets/images/caigou.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.data-card.inventory {
    background-image: url("../assets/images/kucun.png");
    background-size: cover;
    background-position: center;
    background-repeat: no-repeat;
}
.data-desc {
    font-weight: 500;
    font-size: 13px;
    color: #FFFFFF;
}
.data-value {
    font-size: 18px;
    font-weight: 500;
    margin: 10px 0;
    color: #FFFFFF;
}
.top-left {
    display: flex;
    flex-direction: column;
    gap: 20px;
    width: 50%;
  height: 180px;
  width: 20%;
}
.todo-panel {
    background: #fff;
    border-radius: 12px;
    padding: 20px;
    width: 50%;
  height: 180px;
  width: 30%;
}
.todo-list {
  height: 100px;
    list-style: none;
    padding: 0;
    margin: 0;
    font-size: 15px;
    overflow-y: auto;
    height: 260px;
}
.todo-list li {
    border-radius: 8px;
    margin-bottom: 12px;
@@ -637,42 +930,56 @@
    align-items: center;
    background: rgba(225,227,250,0.62);
}
.todo-title {
    font-weight: 400;
    font-size: 12px;
    color: #000000;
    position: relative;
}
.todo-title::before {
    content: ''; /* å¿…需,表示这里有一个内容 */
  content: '';
  /* å¿…需,表示这里有一个内容 */
    position: absolute;
    left: -10px; /* å®šä½åˆ°å·¦ä¾§ */
    top: 50%; /* åž‚直居中 */
    transform: translateY(-50%); /* å¾®è°ƒåž‚直居中 */
    width: 6px; /* åœ†çš„直径 */
    height: 6px; /* åœ†çš„直径 */
  left: -10px;
  /* å®šä½åˆ°å·¦ä¾§ */
  top: 50%;
  /* åž‚直居中 */
  transform: translateY(-50%);
  /* å¾®è°ƒåž‚直居中 */
  width: 6px;
  /* åœ†çš„直径 */
  height: 6px;
  /* åœ†çš„直径 */
    background: #498CEB;
    border-radius: 50%; /* è®©å…¶å˜æˆåœ†å½¢ */
  border-radius: 50%;
  /* è®©å…¶å˜æˆåœ†å½¢ */
}
.todo-division {
    font-weight: 400;
    font-size: 12px;
    color: #000000;
}
.todo-time {
    font-weight: 400;
    font-size: 12px;
    color: #000000;
}
.todo-meta {
    color: #888;
    font-size: 13px;
}
.dashboard-row {
    display: flex;
    gap: 20px;
    margin-bottom: 20px;
}
.main-panel {
    background: #fff;
    border-radius: 12px;
@@ -682,6 +989,7 @@
    display: flex;
    flex-direction: column;
}
.section-title {
    position: relative;
    font-size: 18px;
@@ -701,6 +1009,7 @@
    background-color: #002FA7;
    border-radius: 2px;
}
.contract-info {
    display: flex;
    align-items: center;
@@ -711,32 +1020,41 @@
    border-radius: 10px;
    padding: 10px 30px;
}
.contract-summary {
    display: flex;
    align-items: center;
    gap: 30px;
}
.contract-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.contract-name {
    font-weight: 400;
    font-size: 14px;
    color: #050505;
}
.contract-meta {
    display: flex;
    align-items: center;
    width: 100%;
    gap: 80px;
}
.main-amount {
    font-size: 24px;
    color: rgba(51,50,50,0.85);
}
.up { color: #e57373; }
.up {
  color: #e57373;
}
.contract-list {
    margin-top: 16px;
    font-size: 14px;
@@ -747,10 +1065,12 @@
    overflow-y: auto;
    width: 460px;
}
.line {
    position: relative;
    width: 230px;
}
.line::after {
    content: '';
    position: absolute;
@@ -761,14 +1081,17 @@
    background-color: #C9C5C5;
    border-radius: 2px;
}
.contract-list li {
    margin-top: 10px;
}
.quality-cards {
    display: flex;
    gap: 12px;
    margin-bottom: 12px;
}
.quality-card {
    border-radius: 8px;
    padding: 15px 10px 10px 50px;
@@ -781,24 +1104,160 @@
    background-position: center;
    background-repeat: no-repeat;
}
.quality-card.one {
    background-image: url("../assets/images/yuancailiao.png");
}
.quality-card.two {
    background-image: url("../assets/images/guocheng.png");
}
.quality-card.three {
    background-image: url("../assets/images/chuchang.png");
    
}
.quality-card span {
    color: #4fc3f7;
    font-weight: bold;
    margin-left: 6px;
}
.chart {
    width: 100%;
    height: 220px;
    margin-top: 10px;
}
.process-panel {
  padding-bottom: 10px;
}
.process-panel__header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.process-panel__body {
  display: flex;
  gap: 24px;
  align-items: stretch;
  margin-top: 10px;
}
.process-panel__chart {
  flex: 1;
  min-width: 0;
  padding: 6px 0;
}
.process-panel__aside {
  width: 260px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.process-legend {
  display: flex;
  flex-direction: column;
  gap: 10px;
  align-items: flex-start;
  padding: 8px 6px;
}
.process-legend__item {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: rgba(0, 0, 0, 0.55);
}
.dot {
  width: 10px;
  height: 10px;
  border-radius: 2px;
  display: inline-block;
}
.dot-blue {
  background: #1E5BFF;
}
.dot-yellow {
  background: #F7B500;
}
.dot-teal {
  background: #19C6C6;
}
.process-card {
  background: rgba(245, 247, 250, 0.9);
  border-radius: 10px;
  padding: 16px 16px;
}
.process-card--name {
  background: rgba(235, 242, 255, 1);
  color: #1E5BFF;
  font-weight: 800;
  font-size: 14px;
}
.process-card__label {
  font-size: 13px;
  color: rgba(0, 0, 0, 0.55);
  margin-bottom: 10px;
}
.process-card__value {
  font-size: 24px;
  font-weight: 800;
  color: rgba(0, 0, 0, 0.8);
}
.process-card__value .unit {
  font-size: 12px;
  font-weight: 600;
  color: rgba(0, 0, 0, 0.45);
  margin-left: 6px;
}
@media (max-width: 1200px) {
  .process-panel__body {
    flex-direction: column;
  }
  .process-panel__aside {
    width: 100%;
    flex-direction: row;
    flex-wrap: wrap;
  }
  .process-card {
    flex: 1;
    min-width: 220px;
  }
}
.process-selection-wrapper {
  max-height: 400px;
  overflow-y: auto;
  padding: 10px;
}
.process-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
  gap: 12px;
}
:deep(.el-checkbox.is-bordered) {
  margin-left: 0 !important;
  width: 100%;
}
</style>
src/views/inventoryManagement/receiptManagement/Record.vue
@@ -129,7 +129,7 @@
const stockRecordTypeOptions = ref([]);
const page = reactive({
  current: 1,
  size: 100,
  size: 10,
});
const total = ref(0);
@@ -167,6 +167,7 @@
  getStockInRecordListPage(params)
      .then(res => {
        tableData.value = res.data.records;
        total.value = res.data.total || 0;
      }).finally(() => {
    tableLoading.value = false;
  })
src/views/inventoryManagement/stockReport/index.vue
@@ -48,7 +48,7 @@
          style="width: 240px;"
        />
        
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
        <el-button type="primary" @click="onSearch" style="margin-left: 10px">
          æŸ¥è¯¢
        </el-button>
        <el-button @click="handleReset">重置</el-button>
@@ -230,21 +230,29 @@
             show-overflow-tooltip
           />
        </el-table>
        <pagination
          :total="total"
          layout="total, sizes, prev, pager, next, jumper"
          :page="page.current"
          :limit="page.size"
          @pagination="paginationChange"
        />
      </el-card>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted, nextTick } from 'vue'
import { ref, reactive, onMounted, nextTick, getCurrentInstance } from 'vue'
import { ElMessage } from 'element-plus'
import * as echarts from 'echarts'
import pagination from '@/components/PIMTable/Pagination.vue'
import {
  getStockInventoryInAndOutReportList,
  getStockInventoryReportList
} from "@/api/inventoryManagement/stockInventory.js";
import {
  findAllQualifiedStockInRecordTypeOptions,
  findAllQualifiedStockInRecordTypeOptions,findAllUnQualifiedStockInRecordTypeOptions,
} from "@/api/basicData/enum.js";
@@ -267,6 +275,13 @@
  tableData: []
})
const page = reactive({
  current: 1,
  size: 10,
})
const total = ref(0)
const stockRecordTypeOptions = ref([])
const getRecordType = (recordType) => {
@@ -278,6 +293,10 @@
  findAllQualifiedStockInRecordTypeOptions()
      .then(res => {
        stockRecordTypeOptions.value = res.data;
        findAllUnQualifiedStockInRecordTypeOptions()
          .then(res => {
          stockRecordTypeOptions.value = [...stockRecordTypeOptions.value,...res.data];
      })
      })
}
@@ -293,6 +312,7 @@
// æŠ¥è¡¨ç±»åž‹æ”¹å˜
const handleReportTypeChange = () => {
  page.current = 1
  reportData.value = {
    summary: null,
    chartData: null,
@@ -308,7 +328,12 @@
  
  tableLoading.value = true
  try {
    const params = getQueryParams()
    const baseParams = getQueryParams()
    const params = {
      ...baseParams,
      current: page.current,
      size: page.size,
    }
    let response
    if (searchForm.reportType === 'inout') {
@@ -317,7 +342,8 @@
      response = await getStockInventoryReportList(params)
    }
    if (response.code === 200) {
      reportData.value.tableData = response.data.records
      reportData.value.tableData = response.data.records || []
      total.value = response.data.total || 0
      // reportData.value.summary = response.data.summary
      // reportData.value.chartData = response.data.chartData
      // nextTick(() => {
@@ -330,6 +356,19 @@
  } finally {
    tableLoading.value = false
  }
}
// æŸ¥è¯¢æŒ‰é’®ï¼šé‡ç½®åˆ°ç¬¬ä¸€é¡µå¹¶æŸ¥è¯¢
const onSearch = () => {
  page.current = 1
  handleQuery()
}
// åˆ†é¡µå˜åŒ–
const paginationChange = (obj) => {
  page.current = obj.page
  page.size = obj.limit
  handleQuery()
}
// // ç”Ÿæˆå‡æ•°æ®
// const generateMockData = () => {
@@ -550,6 +589,8 @@
  ]
  fetchStockRecordTypeOptions()
  // åˆå§‹åŒ–加载一次数据
  handleQuery()
})
</script>
src/views/inventoryManagement/transportTaskManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,692 @@
<template>
  <div class="app-container">
    <!-- ç»Ÿè®¡æ¦‚览 -->
    <el-row :gutter="16" style="margin-bottom: 16px">
      <el-col :span="6">
        <el-card shadow="never">
          <div>总任务数</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ totalTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>进行中任务</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ runningTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>已完成任务</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ finishedTasks }}
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card shadow="never">
          <div>完成率</div>
          <div style="font-size: 22px; font-weight: 600; margin-top: 4px">
            {{ completionRate }}%
          </div>
        </el-card>
      </el-col>
    </el-row>
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <div class="search_form">
      <div>
        <span class="search_title">任务编号:</span>
        <el-input
          v-model="searchForm.taskNo"
          style="width: 200px"
          placeholder="请输入任务编号"
          clearable
          @keyup.enter.native="handleQuery"
        />
        <span class="search_title ml10">车辆编号:</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"
        />
        <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>
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å»ºè¿è¾“任务
        </el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        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="taskNo"
          label="任务编号"
          width="150"
          show-overflow-tooltip
        />
        <el-table-column
          prop="outboundOrderNo"
          label="出库订单号"
          width="180"
          show-overflow-tooltip
        />
        <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="driverName"
          label="司机"
          width="100"
          show-overflow-tooltip
        />
        <el-table-column
          prop="loadAddress"
          label="装货地点"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="deliveryAddress"
          label="送货地点"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="loadTime"
          label="装货时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="deliveryTime"
          label="送货时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column
          prop="signTime"
          label="签收时间"
          width="160"
          show-overflow-tooltip
        />
        <el-table-column label="状态" width="110" align="center">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column label="进度" width="150" align="center">
          <template #default="scope">
            <el-progress
              :percentage="scope.row.progress"
              :status="scope.row.status === '已完成' ? 'success' : undefined"
              :stroke-width="12"
              :show-text="false"
            />
            <div style="font-size: 12px; margin-top: 4px">
              {{ scope.row.progress }}%
            </div>
          </template>
        </el-table-column>
        <el-table-column label="操作" fixed="right" width="160" 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>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- æ–°å¢ž/编辑弹窗 -->
    <el-dialog
      v-model="dialogVisible"
      :title="dialogTitle"
      width="780px"
      destroy-on-close
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
        label-position="right"
      >
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="任务编号:" prop="taskNo">
              <el-input
                v-model="form.taskNo"
                placeholder="请输入任务编号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="出库订单号:" prop="outboundOrderNo">
              <el-input
                v-model="form.outboundOrderNo"
                placeholder="请输入关联出库订单号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <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-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="driverName">
              <el-input
                v-model="form.driverName"
                placeholder="请输入司机姓名"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="司机电话:" prop="driverPhone">
              <el-input
                v-model="form.driverPhone"
                placeholder="请输入司机联系电话"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="装货地点:" prop="loadAddress">
              <el-input
                v-model="form.loadAddress"
                placeholder="请输入装货地点"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="送货地点:" prop="deliveryAddress">
              <el-input
                v-model="form.deliveryAddress"
                placeholder="请输入送货地点"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="8">
            <el-form-item label="装货时间:" prop="loadTime">
              <el-date-picker
                v-model="form.loadTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择装货时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="送货时间:" prop="deliveryTime">
              <el-date-picker
                v-model="form.deliveryTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择送货时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
          <el-col :span="8">
            <el-form-item label="签收时间:" prop="signTime">
              <el-date-picker
                v-model="form.signTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择签收时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="状态:" prop="status">
              <el-select v-model="form.status" placeholder="请选择任务状态">
                <el-option
                  v-for="item in statusOptions"
                  :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="planDate">
              <el-date-picker
                v-model="form.planDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择计划日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹Ÿè¿è¾“任务数据
const rawTasks = ref([
  {
    id: 1,
    taskNo: "T2024-1201-001",
    outboundOrderNo: "OUT-2024-1201-1001",
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    driverName: "张师傅",
    driverPhone: "13800000001",
    loadAddress: "深圳仓库A区",
    deliveryAddress: "广州客户一部",
    planDate: "2024-12-01",
    loadTime: "2024-12-01 09:00:00",
    deliveryTime: "2024-12-01 14:30:00",
    signTime: "2024-12-01 15:00:00",
    status: "已完成",
  },
  {
    id: 2,
    taskNo: "T2024-1201-002",
    outboundOrderNo: "OUT-2024-1201-1002",
    vehicleCode: "CL-202402",
    plateNumber: "粤B67890",
    driverName: "李师傅",
    driverPhone: "13800000002",
    loadAddress: "深圳仓库B区",
    deliveryAddress: "东莞客户二部",
    planDate: "2024-12-01",
    loadTime: "2024-12-01 10:00:00",
    deliveryTime: "2024-12-01 13:00:00",
    signTime: "",
    status: "运输中",
  },
  {
    id: 3,
    taskNo: "T2024-1202-001",
    outboundOrderNo: "OUT-2024-1202-1003",
    vehicleCode: "CL-202401",
    plateNumber: "粤A12345",
    driverName: "张师傅",
    driverPhone: "13800000001",
    loadAddress: "深圳仓库A区",
    deliveryAddress: "佛山客户三部",
    planDate: "2024-12-02",
    loadTime: "2024-12-02 08:30:00",
    deliveryTime: "",
    signTime: "",
    status: "待发车",
  },
  {
    id: 4,
    taskNo: "T2024-1203-001",
    outboundOrderNo: "OUT-2024-1203-1004",
    vehicleCode: "CL-202403",
    plateNumber: "粤C11223",
    driverName: "王师傅",
    driverPhone: "13800000003",
    loadAddress: "深圳仓库C区",
    deliveryAddress: "惠州客户四部",
    planDate: "2024-12-03",
    loadTime: "",
    deliveryTime: "",
    signTime: "",
    status: "未开始",
  },
]);
// çŠ¶æ€æžšä¸¾
const statusOptions = [
  { label: "未开始", value: "未开始" },
  { label: "待发车", value: "待发车" },
  { label: "运输中", value: "运输中" },
  { label: "待签收", value: "待签收" },
  { label: "已完成", value: "已完成" },
];
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  taskNo: "",
  vehicleCode: "",
  dateRange: [],
  status: "",
});
// è¡¨æ ¼æ•°æ®ï¼ˆå¸¦è¿›åº¦ç­‰è®¡ç®—字段)
const tableData = ref([]);
// ç»Ÿè®¡
const totalTasks = computed(() => rawTasks.value.length);
const finishedTasks = computed(
  () => rawTasks.value.filter((t) => t.status === "已完成").length
);
const runningTasks = computed(
  () =>
    rawTasks.value.filter((t) =>
      ["待发车", "运输中", "待签收"].includes(t.status)
    ).length
);
const completionRate = computed(() => {
  if (!totalTasks.value) return 0;
  return Math.round((finishedTasks.value / totalTasks.value) * 100);
});
// è®¡ç®—单条任务进度
const computeProgress = (task) => {
  if (task.status === "已完成" || task.signTime) return 100;
  if (task.status === "待签收" || task.deliveryTime) return 80;
  if (task.status === "运输中") return 60;
  if (task.status === "待发车" || task.loadTime) return 30;
  if (task.status === "未开始") return 0;
  return 0;
};
// çŠ¶æ€ tag æ ·å¼
const statusTagType = (status) => {
  if (status === "已完成") return "success";
  if (status === "运输中") return "warning";
  if (status === "待签收" || status === "待发车") return "info";
  return "default";
};
// é‡ç®—表格数据
const recomputeTable = () => {
  const filtered = rawTasks.value
    .filter((t) => {
      if (searchForm.taskNo && !t.taskNo.includes(searchForm.taskNo.trim())) {
        return false;
      }
      if (
        searchForm.vehicleCode &&
        !t.vehicleCode.includes(searchForm.vehicleCode.trim())
      ) {
        return false;
      }
      if (searchForm.status && t.status !== searchForm.status) {
        return false;
      }
      if (Array.isArray(searchForm.dateRange) && searchForm.dateRange.length === 2) {
        const [start, end] = searchForm.dateRange;
        if (!t.planDate || t.planDate < start || t.planDate > end) {
          return false;
        }
      }
      return true;
    })
    .map((t) => ({
      ...t,
      progress: computeProgress(t),
    }));
  tableData.value = filtered;
};
// æŸ¥è¯¢
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.taskNo = "";
  searchForm.vehicleCode = "";
  searchForm.dateRange = [];
  searchForm.status = "";
  recomputeTable();
};
// è¡Œæ ·å¼
const tableRowClassName = ({ row }) => {
  if (row.status === "已完成") {
    return "row-finished";
  }
  if (row.status === "运输中") {
    return "row-running";
  }
  return "";
};
// å¼¹çª— & è¡¨å•
const dialogVisible = ref(false);
const dialogTitle = ref("新建运输任务");
const isEdit = ref(false);
const formRef = ref(null);
const form = reactive({
  id: null,
  taskNo: "",
  outboundOrderNo: "",
  vehicleCode: "",
  plateNumber: "",
  driverName: "",
  driverPhone: "",
  loadAddress: "",
  deliveryAddress: "",
  planDate: "",
  loadTime: "",
  deliveryTime: "",
  signTime: "",
  status: "未开始",
});
const rules = {
  taskNo: [{ required: true, message: "请输入任务编号", trigger: "blur" }],
  outboundOrderNo: [
    { required: true, message: "请输入出库订单号", trigger: "blur" },
  ],
  vehicleCode: [{ required: true, message: "请输入车辆编号", trigger: "blur" }],
  plateNumber: [{ required: true, message: "请输入车牌号码", trigger: "blur" }],
  driverName: [{ required: true, message: "请输入司机姓名", trigger: "blur" }],
  loadAddress: [{ required: true, message: "请输入装货地点", trigger: "blur" }],
  deliveryAddress: [
    { required: true, message: "请输入送货地点", trigger: "blur" },
  ],
  planDate: [{ required: true, message: "请选择计划日期", trigger: "change" }],
};
// æ–°å»º
const openAdd = () => {
  dialogTitle.value = "新建运输任务";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    taskNo: "",
    outboundOrderNo: "",
    vehicleCode: "",
    plateNumber: "",
    driverName: "",
    driverPhone: "",
    loadAddress: "",
    deliveryAddress: "",
    planDate: "",
    loadTime: "",
    deliveryTime: "",
    signTime: "",
    status: "未开始",
  });
  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 (isEdit.value) {
      const index = rawTasks.value.findIndex((t) => t.id === form.id);
      if (index !== -1) {
        rawTasks.value[index] = { ...form };
      }
      ElMessage.success("运输任务已更新");
    } else {
      const newId = rawTasks.value.length
        ? Math.max(...rawTasks.value.map((t) => t.id)) + 1
        : 1;
      rawTasks.value.push({ ...form, id: newId });
      ElMessage.success("运输任务已新增");
    }
    dialogVisible.value = false;
    recomputeTable();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该运输任务?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawTasks.value = rawTasks.value.filter((t) => t.id !== row.id);
      recomputeTable();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
onMounted(() => {
  recomputeTable();
});
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
::v-deep(.row-finished) {
  background-color: #f6ffed;
}
::v-deep(.row-running) {
  background-color: #fffbe6;
}
</style>
src/views/inventoryManagement/vehicleFuelManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,556 @@
<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>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">
          æ–°å¢žåŠ æ²¹è®°å½•
        </el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        :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"
        >
          <template #default="scope">
            <span>{{ scope.row.amount?.toFixed(2) }}</span>
          </template>
        </el-table-column>
        <el-table-column
          prop="liters"
          label="升数(L)"
          width="90"
          align="right"
        >
          <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>
          </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"
      >
        <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-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="fuelDate">
              <el-date-picker
                v-model="form.fuelDate"
                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="gunNo">
              <el-input
                v-model="form.gunNo"
                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>
          </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>
          </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>
          </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>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹ŸåŠ æ²¹è®°å½•æ•°æ®
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 searchForm = reactive({
  vehicleCode: "",
  dateRange: [],
});
// è¡¨æ ¼æ•°æ®ï¼ˆåŒ…含计算字段)
const tableData = ref([]);
// å¼¹çª— & è¡¨å•
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,
});
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" }],
};
// é‡æ–°è®¡ç®—油耗、平均油耗和异常预警
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;
  });
  const avgMap = {};
  Object.keys(stats).forEach((key) => {
    avgMap[key] = stats[key].totalCons / stats[key].count;
  });
  // 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,
      };
    });
  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,
  });
  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("加油记录已更新");
    } else {
      const newId = rawRecords.value.length
        ? Math.max(...rawRecords.value.map((r) => r.id)) + 1
        : 1;
      rawRecords.value.push({ ...form, id: newId });
      ElMessage.success("加油记录已新增");
    }
    dialogVisible.value = false;
    recomputeTable();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该加油记录?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      rawRecords.value = rawRecords.value.filter((r) => r.id !== row.id);
      recomputeTable();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
onMounted(() => {
  recomputeTable();
});
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/inventoryManagement/vehicleManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,581 @@
<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"
          />
        </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>
      </div>
      <div>
        <el-button type="primary" icon="Plus" @click="openAdd">新增车辆</el-button>
      </div>
    </div>
    <!-- è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 18.5em)"
        :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">
          <template #default="scope">
            <el-tag :type="statusTagType(scope.row.status)">
              {{ scope.row.status }}
            </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 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>
          </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"
      >
        <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-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>
          </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="请选择购置日期"
                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-option
                  v-for="item in statusOptions"
                  :key="item.value"
                  :label="item.label"
                  :value="item.value"
                />
              </el-select>
            </el-form-item>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="行驶证编号:" prop="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>
          </el-col>
        </el-row>
        <el-row :gutter="20">
          <el-col :span="12">
            <el-form-item label="到期日期:" prop="licenseExpireDate">
              <el-date-picker
                v-model="form.licenseExpireDate"
                type="date"
                value-format="YYYY-MM-DD"
                format="YYYY-MM-DD"
                placeholder="请选择到期日期"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button @click="handleCancel">取 æ¶ˆ</el-button>
          <el-button type="primary" @click="handleSubmit">保 å­˜</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
// æ¨¡æ‹Ÿè½¦è¾†åŸºç¡€æ•°æ®
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 vehicleTypeOptions = [
  { label: "厢式货车", value: "厢式货车" },
  { label: "冷藏车", value: "冷藏车" },
  { label: "牵引车", value: "牵引车" },
  { label: "其他", value: "其他" },
];
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 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,
});
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" }],
};
// æŸ¥è¯¢
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;
  });
};
const resetSearch = () => {
  searchForm.plateNumber = "";
  searchForm.vehicleType = "";
  searchForm.department = "";
  searchForm.status = "";
  searchForm.archived = "";
  handleQuery();
};
// æ–°å¢ž
const openAdd = () => {
  dialogTitle.value = "新增车辆";
  isEdit.value = false;
  Object.assign(form, {
    id: null,
    vehicleCode: "",
    plateNumber: "",
    vehicleType: "",
    department: "",
    purchaseDate: "",
    licenseInfo: "",
    status: "在用",
    archived: false,
  });
  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 (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();
  });
};
const handleCancel = () => {
  dialogVisible.value = false;
};
// å½’æ¡£
const archiveRow = (row) => {
  if (row.archived) return;
  ElMessageBox.confirm(
    "是否确认将该车辆归档?归档后仅保留查询,不再参与运输任务分配。",
    "归档提示",
    {
      confirmButtonText: "确认",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      row.archived = true;
      if (row.status === "在用") {
        row.status = "闲置";
      }
      ElMessage.success("车辆已归档");
      handleQuery();
    })
    .catch(() => {});
};
// åˆ é™¤
const removeRow = (row) => {
  ElMessageBox.confirm("是否确认删除该车辆基础信息?", "删除提示", {
    confirmButtonText: "确认",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      allVehicles.value = allVehicles.value.filter((v) => v.id !== row.id);
      handleQuery();
      ElMessage.success("删除成功");
    })
    .catch(() => {});
};
// çŠ¶æ€æ ·å¼
const statusTagType = (status) => {
  if (status === "在用") return "success";
  if (status === "闲置") return "info";
  if (status === "ç»´ä¿®") return "warning";
  return "default";
};
</script>
<style scoped lang="scss">
.dialog-footer {
  text-align: right;
}
</style>
src/views/personnelManagement/attendanceCheckin/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,469 @@
<template>
  <div class="app-container">
    <!-- å‘˜å·¥æ‰“卡区 -->
    <el-card shadow="never" class="mb16">
      <div class="attendance-header">
        <div>
          <div class="title">打卡签到</div>
          <div class="sub-title">支持一键打卡,自动记录上下班时间</div>
        </div>
        <div class="attendance-actions">
          <div class="time-block">
            <div class="label">当前时间</div>
            <div class="value">{{ nowTime }}</div>
          </div>
          <el-button type="primary" size="large" @click="handleCheckInOut">
            {{ checkInOutText }}
          </el-button>
        </div>
      </div>
      <el-descriptions border :column="4" class="mt10">
        <el-descriptions-item label="员工姓名">
          {{ currentUser.name }}
        </el-descriptions-item>
        <el-descriptions-item label="工号">
          {{ currentUser.no }}
        </el-descriptions-item>
        <el-descriptions-item label="所属部门">
          {{ currentUser.dept }}
        </el-descriptions-item>
        <el-descriptions-item label="今日状态">
          <el-tag :type="todayStatusTag" size="small">
            {{ todayStatusText }}
          </el-tag>
        </el-descriptions-item>
        <el-descriptions-item label="上班时间">
          {{ todayRecord?.checkInTime || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="下班时间">
          {{ todayRecord?.checkOutTime || '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="工时(小时)">
          {{ todayRecord?.workHours ?? '-' }}
        </el-descriptions-item>
        <el-descriptions-item label="异常标记">
          <span v-if="todayRecord?.status === 'normal'">-</span>
          <el-tag v-else type="danger" size="small">
            {{ todayRecord?.statusText }}
          </el-tag>
        </el-descriptions-item>
      </el-descriptions>
    </el-card>
    <!-- æŸ¥è¯¢æ¡ä»¶ï¼ˆç®¡ç†å‘˜è€ƒå‹¤æ—¥æŠ¥ï¼‰ -->
    <div class="search_form">
      <div>
        <span class="search_title">部门:</span>
        <el-select
          v-model="searchForm.dept"
          placeholder="请选择部门"
          style="width: 180px"
          clearable
        >
          <el-option
            v-for="item in deptOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
        <span class="search_title ml10">日期:</span>
        <el-date-picker
          v-model="searchForm.date"
          type="date"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          placeholder="请选择日期"
          clearable
        />
        <el-button type="primary" @click="handleQuery" style="margin-left: 10px">
          æœç´¢
        </el-button>
        <el-button @click="resetSearch">重置</el-button>
      </div>
      <div>
        <el-button icon="Download" @click="handleExport">
          å¯¼å‡ºè€ƒå‹¤æ—¥æŠ¥
        </el-button>
      </div>
    </div>
    <!-- è€ƒå‹¤æ—¥æŠ¥è¡¨æ ¼ -->
    <div class="table_list">
      <el-table
        :data="tableData"
        border
        style="width: 100%"
        height="calc(100vh - 24em)"
        :header-cell-style="{ background: '#F0F1F5', color: '#333333' }"
        :row-class-name="rowClassName"
      >
        <el-table-column type="index" label="序号" width="60" align="center" />
        <el-table-column
          prop="date"
          label="日期"
          width="120"
        />
        <el-table-column
          prop="dept"
          label="部门"
          width="140"
        />
        <el-table-column
          prop="name"
          label="姓名"
          width="120"
        />
        <el-table-column
          prop="no"
          label="工号"
          width="120"
        />
        <el-table-column
          prop="checkInTime"
          label="上班时间"
          width="140"
        />
        <el-table-column
          prop="checkOutTime"
          label="下班时间"
          width="140"
        />
        <el-table-column
          prop="workHours"
          label="工时(小时)"
          width="110"
          align="center"
        />
        <el-table-column
          prop="statusText"
          label="考勤状态"
          width="120"
          align="center"
        >
          <template #default="scope">
            <el-tag
              v-if="scope.row.status === 'normal'"
              type="success"
              size="small"
            >
              æ­£å¸¸
            </el-tag>
            <el-tag
              v-else
              type="danger"
              size="small"
            >
              {{ scope.row.statusText }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column
          prop="remark"
          label="备注"
          show-overflow-tooltip
        />
      </el-table>
    </div>
  </div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onBeforeUnmount } from "vue";
import { ElMessage } from "element-plus";
// æ¨¡æ‹Ÿå½“前登录员工
const currentUser = reactive({
  id: 1,
  name: "张三",
  no: "E10001",
  dept: "生产一部",
});
// éƒ¨é—¨é€‰é¡¹
const deptOptions = [
  { label: "生产一部", value: "生产一部" },
  { label: "生产二部", value: "生产二部" },
  { label: "设备维护部", value: "设备维护部" },
  { label: "质检部", value: "质检部" },
];
// æ¨¡æ‹Ÿè€ƒå‹¤åŽŸå§‹æ•°æ®
const rawAttendance = ref([
  {
    id: 1,
    date: "2024-12-01",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:58",
    checkOutTime: "18:10",
    workHours: 9.2,
    status: "normal",
    statusText: "正常",
    remark: "",
  },
  {
    id: 2,
    date: "2024-12-01",
    userId: 2,
    name: "李四",
    no: "E10002",
    dept: "生产一部",
    checkInTime: "09:15",
    checkOutTime: "18:05",
    workHours: 8.8,
    status: "late",
    statusText: "迟到",
    remark: "因交通拥堵迟到",
  },
  {
    id: 3,
    date: "2024-12-01",
    userId: 3,
    name: "王五",
    no: "E20001",
    dept: "设备维护部",
    checkInTime: "08:50",
    checkOutTime: "17:20",
    workHours: 8.5,
    status: "early",
    statusText: "早退",
    remark: "外出处理紧急故障",
  },
  {
    id: 4,
    date: "2024-12-02",
    userId: 1,
    name: "张三",
    no: "E10001",
    dept: "生产一部",
    checkInTime: "08:45",
    checkOutTime: "18:30",
    workHours: 9.7,
    status: "normal",
    statusText: "正常",
    remark: "加班0.5小时",
  },
]);
// æŸ¥è¯¢è¡¨å•
const searchForm = reactive({
  dept: "",
  date: "",
});
// è¡¨æ ¼æ•°æ®
const tableData = ref([]);
// å½“前时间展示
const nowTime = ref("");
let timer = null;
const updateNowTime = () => {
  const now = new Date();
  const Y = now.getFullYear();
  const M = String(now.getMonth() + 1).padStart(2, "0");
  const D = String(now.getDate()).padStart(2, "0");
  const h = String(now.getHours()).padStart(2, "0");
  const m = String(now.getMinutes()).padStart(2, "0");
  const s = String(now.getSeconds()).padStart(2, "0");
  nowTime.value = `${Y}-${M}-${D} ${h}:${m}:${s}`;
};
// ä»Šæ—¥æ—¥æœŸ
const todayStr = computed(() => nowTime.value.slice(0, 10));
// å½“日当前员工考勤记录
const todayRecord = computed(() =>
  rawAttendance.value.find(
    (item) =>
      item.userId === currentUser.id && item.date === todayStr.value
  )
);
// æ‰“卡按钮文本
const checkInOutText = computed(() => {
  if (!todayRecord.value || !todayRecord.value.checkInTime) {
    return "上班打卡";
  }
  if (!todayRecord.value.checkOutTime) {
    return "下班打卡";
  }
  return "今日已打卡完成";
});
// ä»Šæ—¥çŠ¶æ€å±•ç¤º
const todayStatusTag = computed(() => {
  if (!todayRecord.value) return "info";
  if (todayRecord.value.status === "normal") return "success";
  return "danger";
});
const todayStatusText = computed(() => {
  if (!todayRecord.value) return "未打卡";
  return todayRecord.value.statusText || "正常";
});
// è¡Œæ ·å¼ï¼šå¼‚常高亮
const rowClassName = ({ row }) => {
  if (row.status === "late" || row.status === "early") {
    return "row-abnormal";
  }
  return "";
};
// æŸ¥è¯¢
const recomputeTable = () => {
  const list = rawAttendance.value.filter((item) => {
    if (searchForm.dept && item.dept !== searchForm.dept) {
      return false;
    }
    if (searchForm.date && item.date !== searchForm.date) {
      return false;
    }
    return true;
  });
  tableData.value = list;
};
const handleQuery = () => {
  recomputeTable();
};
const resetSearch = () => {
  searchForm.dept = "";
  searchForm.date = "";
  recomputeTable();
};
// å¯¼å‡ºï¼ˆæ¼”示)
const handleExport = () => {
  ElMessage.success("当前为演示页面,导出功能未对接实际接口");
};
// æ‰“卡逻辑(仅前端模拟)
const handleCheckInOut = () => {
  const [dateStr, timeStr] = nowTime.value.split(" ");
  if (!dateStr || !timeStr) return;
  // ä¸Šç­æ‰“卡
  if (!todayRecord.value) {
    const newId = rawAttendance.value.length
      ? Math.max(...rawAttendance.value.map((i) => i.id)) + 1
      : 1;
    const status =
      timeStr > "09:00:00" ? "late" : "normal";
    const statusText = status === "late" ? "迟到" : "正常";
    rawAttendance.value.push({
      id: newId,
      date: dateStr,
      userId: currentUser.id,
      name: currentUser.name,
      no: currentUser.no,
      dept: currentUser.dept,
      checkInTime: timeStr.slice(0, 5),
      checkOutTime: "",
      workHours: null,
      status,
      statusText,
      remark: "",
    });
    ElMessage.success("上班打卡成功");
  } else if (!todayRecord.value.checkOutTime) {
    // ä¸‹ç­æ‰“卡
    todayRecord.value.checkOutTime = timeStr.slice(0, 5);
    // ç®€å•按 9:00-18:00 è®¡ç®—工时
    const start = todayRecord.value.checkInTime || "09:00";
    const [sh, sm] = start.split(":").map((v) => parseInt(v, 10));
    const [eh, em] = todayRecord.value.checkOutTime
      .split(":")
      .map((v) => parseInt(v, 10));
    const diff = (eh * 60 + em - (sh * 60 + sm)) / 60;
    todayRecord.value.workHours = Number(Math.max(diff, 0).toFixed(1));
    // æ—©é€€åˆ¤æ–­ï¼š18:00 å‰ç¦»å¼€è§†ä¸ºæ—©é€€ï¼ˆåªç¤ºæ„ï¼‰
    if (timeStr < "18:00:00") {
      todayRecord.value.status = "early";
      todayRecord.value.statusText = "早退";
    } else if (todayRecord.value.status === "normal") {
      todayRecord.value.statusText = "正常";
    }
    ElMessage.success("下班打卡成功");
  } else {
    ElMessage.info("今日已完成上下班打卡");
  }
  recomputeTable();
};
onMounted(() => {
  updateNowTime();
  timer = setInterval(updateNowTime, 1000);
  // é»˜è®¤å±•示当天数据
  const today = new Date();
  const Y = today.getFullYear();
  const M = String(today.getMonth() + 1).padStart(2, "0");
  const D = String(today.getDate()).padStart(2, "0");
  searchForm.date = `${Y}-${M}-${D}`;
  recomputeTable();
});
onBeforeUnmount(() => {
  if (timer) {
    clearInterval(timer);
  }
});
</script>
<style scoped lang="scss">
.mb16 {
  margin-bottom: 16px;
}
.attendance-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.attendance-header .title {
  font-size: 18px;
  font-weight: 600;
  margin-bottom: 4px;
}
.attendance-header .sub-title {
  font-size: 13px;
  color: #909399;
}
.attendance-actions {
  display: flex;
  align-items: center;
  gap: 16px;
}
.time-block {
  text-align: right;
}
.time-block .label {
  font-size: 12px;
  color: #909399;
}
.time-block .value {
  font-size: 18px;
  font-weight: 600;
  color: #333;
}
::v-deep(.row-abnormal) {
  background-color: #fff5f5;
}
</style>
src/views/procurementManagement/procurementLedger/index.vue
@@ -1648,7 +1648,7 @@
          delProduct(ids).then(res => {
            proxy.$modal.msgSuccess("删除成功");
            closeProductDia();
            getSalesLedgerWithProducts({ id: currentId.value, type: 2 }).then(
            getPurchaseById({ id: currentId.value, type: 2 }).then(
              res => {
                productData.value = res.productData;
              }
@@ -1683,14 +1683,6 @@
  const handleDelete = () => {
    let ids = [];
    if (selectedRows.value.length > 0) {
      // æ£€æŸ¥æ˜¯å¦æœ‰ä»–人维护的数据
      const unauthorizedData = selectedRows.value.filter(
        item => item.recorderName !== userStore.nickName
      );
      if (unauthorizedData.length > 0) {
        proxy.$modal.msgWarning("不可删除他人维护的数据");
        return;
      }
      ids = selectedRows.value.map(item => item.id);
    } else {
      proxy.$modal.msgWarning("请选择数据");
src/views/qualityManagement/afterSalesTraceability/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,595 @@
<template>
  <div class="app-container">
    <!-- æŸ¥è¯¢æ¡ä»¶ -->
    <el-form
      :model="queryParams"
      ref="queryForm"
      :inline="true"
      v-show="showSearch"
      label-width="90px"
    >
      <el-form-item label="产品型号" prop="productModel">
        <el-input
          v-model="queryParams.productModel"
          placeholder="请输入产品型号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="客户名称" prop="customerName">
        <el-input
          v-model="queryParams.customerName"
          placeholder="请输入客户名称"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="反馈时间" prop="feedbackRange">
        <el-date-picker
          v-model="queryParams.feedbackRange"
          type="daterange"
          range-separator="至"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
          value-format="YYYY-MM-DD"
          format="YYYY-MM-DD"
          clearable
        />
      </el-form-item>
      <el-form-item label="处理状态" prop="status">
        <el-select
          v-model="queryParams.status"
          placeholder="请选择处理状态"
          clearable
        >
          <el-option
            v-for="item in statusOptions"
            :key="item.value"
            :label="item.label"
            :value="item.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">
          æœç´¢
        </el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>
    <!-- æ“ä½œåŒº -->
    <el-row :gutter="10" class="mb8">
      <el-col :span="3">
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
        >
          æ–°å¢žå”®åŽè´¨é‡è®°å½•
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
        >
          ä¿®æ”¹
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
        >
          åˆ é™¤
        </el-button>
      </el-col>
      <el-col :span="3">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
        >
          å¯¼å‡º
        </el-button>
      </el-col>
      <right-toolbar
        v-model:showSearch="showSearch"
        @queryTable="getList"
      />
    </el-row>
    <!-- æ•°æ®è¡¨ -->
    <el-table
      v-loading="loading"
      :data="afterSalesList"
      @selection-change="handleSelectionChange"
    >
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="序号" type="index" width="55" align="center" />
      <el-table-column label="销售合同号" prop="contractNo" width="160" />
      <el-table-column label="产品编号" prop="productCode" width="140" />
      <el-table-column label="产品型号" prop="productModel" width="140" />
      <el-table-column label="客户名称" prop="customerName" width="160" />
      <el-table-column label="联系方式" prop="contact" width="140" />
      <el-table-column label="反馈时间" prop="feedbackTime" width="160">
        <template #default="scope">
          <span>{{ scope.row.feedbackTime }}</span>
        </template>
      </el-table-column>
      <el-table-column label="问题描述" prop="problemDesc" show-overflow-tooltip />
      <el-table-column label="维修情况" prop="repairInfo" show-overflow-tooltip />
      <el-table-column label="处理结果" prop="result" show-overflow-tooltip />
      <el-table-column label="处理状态" prop="status" width="120" align="center">
        <template #default="scope">
          <dict-tag
            :options="statusOptions"
            :value="scope.row.status"
          />
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160" fixed="right">
        <template #default="scope">
          <el-button size="small" type="text" icon="Edit" @click="handleUpdate(scope.row)">
            ä¿®æ”¹
          </el-button>
          <el-button size="small" type="text" icon="Delete" @click="handleDelete(scope.row)">
            åˆ é™¤
          </el-button>
        </template>
      </el-table-column>
    </el-table>
    <pagination
      v-show="total > 0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />
    <!-- æ–°å¢ž/修改弹窗 -->
    <el-dialog
      :title="title"
      v-model="open"
      width="900px"
      append-to-body
    >
      <el-form
        ref="formRef"
        :model="form"
        :rules="rules"
        label-width="110px"
      >
        <el-row>
          <el-col :span="12">
            <el-form-item label="销售合同号" prop="contractNo">
              <el-input
                v-model="form.contractNo"
                placeholder="请选择关联销售合同号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="产品编号" prop="productCode">
              <el-input
                v-model="form.productCode"
                placeholder="请选择关联产品编号"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="产品型号" prop="productModel">
              <el-input
                v-model="form.productModel"
                placeholder="请输入产品型号"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="客户名称" prop="customerName">
              <el-input
                v-model="form.customerName"
                placeholder="请输入客户名称"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="联系方式" prop="contact">
              <el-input
                v-model="form.contact"
                placeholder="请输入客户联系方式"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="反馈时间" prop="feedbackTime">
              <el-date-picker
                v-model="form.feedbackTime"
                type="datetime"
                value-format="YYYY-MM-DD HH:mm:ss"
                format="YYYY-MM-DD HH:mm"
                placeholder="请选择反馈时间"
                style="width: 100%"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="24">
            <el-form-item label="客户反馈问题" prop="problemDesc">
              <el-input
                v-model="form.problemDesc"
                type="textarea"
                :rows="3"
                placeholder="请详细记录客户反馈问题、现象描述等信息"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="维修情况" prop="repairInfo">
              <el-input
                v-model="form.repairInfo"
                type="textarea"
                :rows="2"
                placeholder="记录维修过程、使用备件、返修次数等"
              />
            </el-form-item>
          </el-col>
          <el-col :span="12">
            <el-form-item label="处理结果" prop="result">
              <el-input
                v-model="form.result"
                type="textarea"
                :rows="2"
                placeholder="记录最终处理结果,如更换、退货、升级等"
              />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row>
          <el-col :span="12">
            <el-form-item label="处理状态" prop="status">
              <el-select
                v-model="form.status"
                placeholder="请选择处理状态"
              >
                <el-option
                  v-for="item in statusOptions"
                  :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="remark">
              <el-input
                v-model="form.remark"
                type="textarea"
                :rows="2"
                placeholder="可记录后续跟踪意见、复盘结论等"
              />
            </el-form-item>
          </el-col>
        </el-row>
      </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>
  </div>
</template>
<script setup name="AfterSalesTraceability">
import { ref, reactive, onMounted } from "vue";
import { ElMessageBox } from "element-plus";
const { proxy } = getCurrentInstance();
// çŠ¶æ€å­—å…¸
const statusOptions = ref([
  { label: "待处理", value: "0" },
  { label: "处理中", value: "1" },
  { label: "已完成", value: "2" },
  { label: "已关闭", value: "3" },
]);
// æ¨¡æ‹Ÿå”®åŽè´¨é‡æ•°æ®
const afterSalesList = ref([
  {
    id: 1,
    contractNo: "SC-2024-001",
    productCode: "P-10001",
    productModel: "XG-500A",
    customerName: "华南电子科技有限公司",
    contact: "å¼ å·¥ / 13800000001",
    feedbackTime: "2024-12-01 10:23:00",
    problemDesc: "使用三个月后出现间歇性掉电,影响产线稳定运行。",
    repairInfo: "安排工程师上门检修,更换电源模块并加固接线端子。",
    result: "更换电源总成,恢复正常使用,建议客户增加UPS保护。",
    status: "2",
    remark: "列入重点跟踪客户,后续观察一个季度。",
  },
  {
    id: 2,
    contractNo: "SC-2024-015",
    productCode: "P-10045",
    productModel: "XG-500B",
    customerName: "华东精密制造有限公司",
    contact: "李工 / 13800000002",
    feedbackTime: "2024-12-05 15:40:00",
    problemDesc: "部分批次出现外壳刮花,客户投诉外观质量不达标。",
    repairInfo: "与生产现场核查,确认来料搬运及包装环节存在磕碰风险。",
    result: "对问题批次重新返工,补发良品,并优化包装防护方案。",
    status: "1",
    remark: "需跟踪后续批次投诉率变化。",
  },
  {
    id: 3,
    contractNo: "SC-2024-032",
    productCode: "P-10110",
    productModel: "XG-600C",
    customerName: "西南新能源科技股份",
    contact: "王工 / 13800000003",
    feedbackTime: "2024-11-28 09:15:00",
    problemDesc: "现场调试时发现接口不兼容,需要适配客户旧版系统。",
    repairInfo: "远程技术支持+现场工程师联合排查,提供过渡适配方案。",
    result: "通过更换接插件并升级固件版本解决。",
    status: "0",
    remark: "建议下次合同前置沟通接口规格。",
  },
]);
const open = ref(false);
const loading = ref(false);
const showSearch = ref(true);
const ids = ref([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(3);
const title = ref("新增售后质量记录");
const data = reactive({
  form: {},
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    productModel: null,
    customerName: null,
    feedbackRange: [],
    status: null,
  },
  rules: {
    contractNo: [
      { required: true, message: "销售合同号不能为空", trigger: "blur" },
    ],
    productCode: [
      { required: true, message: "产品编号不能为空", trigger: "blur" },
    ],
    productModel: [
      { required: true, message: "产品型号不能为空", trigger: "blur" },
    ],
    customerName: [
      { required: true, message: "客户名称不能为空", trigger: "blur" },
    ],
    contact: [
      { required: true, message: "联系方式不能为空", trigger: "blur" },
    ],
    feedbackTime: [
      { required: true, message: "反馈时间不能为空", trigger: "change" },
    ],
    problemDesc: [
      { required: true, message: "客户反馈问题不能为空", trigger: "blur" },
    ],
    status: [
      { required: true, message: "处理状态不能为空", trigger: "change" },
    ],
  },
});
const { queryParams, form, rules } = toRefs(data);
// æŸ¥è¯¢åˆ—表(仅前端筛选,不调接口)
function getList() {
  loading.value = true;
  const list = afterSalesList.value.filter((item) => {
    if (
      queryParams.value.productModel &&
      !item.productModel
        ?.toLowerCase()
        .includes(queryParams.value.productModel.toLowerCase())
    ) {
      return false;
    }
    if (
      queryParams.value.customerName &&
      !item.customerName
        ?.toLowerCase()
        .includes(queryParams.value.customerName.toLowerCase())
    ) {
      return false;
    }
    if (queryParams.value.status && item.status !== queryParams.value.status) {
      return false;
    }
    if (
      Array.isArray(queryParams.value.feedbackRange) &&
      queryParams.value.feedbackRange.length === 2
    ) {
      const [start, end] = queryParams.value.feedbackRange;
      const dateStr = item.feedbackTime?.slice(0, 10);
      if (dateStr < start || dateStr > end) {
        return false;
      }
    }
    return true;
  });
  total.value = list.length;
  // æ­¤å¤„未做分页,仅模拟全量展示
  afterSalesList.value = list;
  loading.value = false;
}
// å–消
function cancel() {
  open.value = false;
  reset();
}
// è¡¨å•重置
function reset() {
  form.value = {
    id: null,
    contractNo: null,
    productCode: null,
    productModel: null,
    customerName: null,
    contact: null,
    feedbackTime: null,
    problemDesc: null,
    repairInfo: null,
    result: null,
    status: "0",
    remark: null,
  };
  proxy.resetForm("formRef");
}
// æœç´¢
function handleQuery() {
  queryParams.value.pageNum = 1;
  getList();
}
// é‡ç½®
function resetQuery() {
  proxy.resetForm("queryForm");
  queryParams.value.feedbackRange = [];
  handleQuery();
}
// å¤šé€‰
function handleSelectionChange(selection) {
  ids.value = selection.map((item) => item.id);
  single.value = selection.length !== 1;
  multiple.value = !selection.length;
}
// æ–°å¢ž
function handleAdd() {
  reset();
  open.value = true;
  title.value = "新增售后质量记录";
}
// ä¿®æ”¹
function handleUpdate(row) {
  reset();
  const current = row || afterSalesList.value.find((item) => item.id === ids.value[0]);
  if (current) {
    form.value = { ...current };
    open.value = true;
    title.value = "修改售后质量记录";
  }
}
// æäº¤
function submitForm() {
  proxy.$refs["formRef"].validate((valid) => {
    if (valid) {
      if (form.value.id != null) {
        // ä¿®æ”¹ï¼šæ›¿æ¢æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®
        const index = afterSalesList.value.findIndex(
          (item) => item.id === form.value.id
        );
        if (index !== -1) {
          afterSalesList.value.splice(index, 1, { ...form.value });
        }
        proxy.$modal.msgSuccess("修改成功");
      } else {
        // æ–°å¢žï¼šæ’入本地模拟数据
        const newId =
          afterSalesList.value.length > 0
            ? Math.max(...afterSalesList.value.map((i) => i.id)) + 1
            : 1;
        afterSalesList.value.push({ ...form.value, id: newId });
        proxy.$modal.msgSuccess("新增成功");
      }
      open.value = false;
      getList();
    }
  });
}
// åˆ é™¤
function handleDelete(row) {
  const deleteIds = row?.id ? [row.id] : ids.value;
  if (!deleteIds || deleteIds.length === 0) {
    proxy.$modal.msgWarning("请先选择要删除的记录");
    return;
  }
  ElMessageBox.confirm(
    '是否确认删除选中的售后质量记录?',
    "警告",
    {
      confirmButtonText: "确定",
      cancelButtonText: "取消",
      type: "warning",
    }
  )
    .then(() => {
      // å‰ç«¯åˆ é™¤æœ¬åœ°æ¨¡æ‹Ÿæ•°æ®
      afterSalesList.value = afterSalesList.value.filter(
        (item) => !deleteIds.includes(item.id)
      );
      proxy.$modal.msgSuccess("删除成功");
      getList();
    })
    .catch(() => {});
}
// å¯¼å‡º
function handleExport() {
  proxy.$modal.msgSuccess("导出功能为演示功能,当前未对接实际导出接口");
}
onMounted(() => {
  getList();
});
</script>
<style scoped>
.mb8 {
  margin-bottom: 8px;
}
.dialog-footer {
  text-align: right;
}
</style>
src/views/reportAnalysis/financialAnalysis/components/center-bottom.vue
@@ -101,7 +101,7 @@
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 428px;
  height: 432px;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/center-center.vue
@@ -30,7 +30,7 @@
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import Echarts from '@/components/Echarts/echarts.vue'
import { productInOutAnalysis } from '@/api/viewIndex.js'
import { inputOutputAnalysis } from '@/api/viewIndex.js'
const chartStyle = { width: '100%', height: '100%' }
const grid = {
@@ -132,13 +132,13 @@
}
const fetchData = () => {
  productInOutAnalysis({ type: 1 })
  inputOutputAnalysis()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const list = res.data
        xAxis1.value[0].data = list.map((d) => d.date)
        lineSeries.value[0].data = list.map((d) => Number(d.outCount) || 0)
        lineSeries.value[1].data = list.map((d) => Number(d.inCount) || 0)
        lineSeries.value[0].data = list.map((d) => Number(d.outputSum) || 0)
        lineSeries.value[1].data = list.map((d) => Number(d.inputSum) || 0)
      }
    })
    .catch((err) => {
src/views/reportAnalysis/productionAnalysis/components/center-top.vue
@@ -25,7 +25,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { salesPurchaseStorageProductCount } from '@/api/viewIndex.js'
import { orderCount } from '@/api/viewIndex.js'
const statItems = ref([])
@@ -37,7 +37,7 @@
const compareClass = (val) => (val >= 0 ? 'compare-up' : 'compare-down')
const fetchData = () => {
  salesPurchaseStorageProductCount()
  orderCount()
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        statItems.value = res.data.map((item) => ({
@@ -48,7 +48,7 @@
      }
    })
    .catch((err) => {
      console.error('获取销售/采购/储存产品数失败:', err)
      console.error('获取订单数量统计失败:', err)
    })
}
@@ -97,7 +97,7 @@
.card-label {
  font-weight: 400;
  font-size: 19px;
  font-size: 16px;
  color: rgba(208, 231, 255, 0.7);
}
src/views/reportAnalysis/productionAnalysis/components/left-top.vue
@@ -24,7 +24,7 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { productSalesAnalysis } from '@/api/viewIndex.js'
import { processOutputAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from '@/views/reportAnalysis/financialAnalysis/components/DateTypeSwitch.vue'
@@ -73,7 +73,7 @@
    formatter: function (name) {
      const item = pieObjData.value[name]
      if (!item) return name
      return `{title|${name}}{value|${item.value}}{unit|元}{percent|${item.rate}}{unit|%}`
      return `{title|${name}}{value|${item.value}}{unit|ä»¶}{percent|${item.rate}}{unit|%}`
    },
    textStyle: {
      rich: {
@@ -106,12 +106,12 @@
const pieTooltip = {
  trigger: 'item',
  formatter: '{a} <br/>{b} : {c}元 ({d}%)',
  formatter: '{a} <br/>{b} : {c}ä»¶ ({d}%)',
}
const pieSeries = computed(() => [
  {
    name: '产品销售金额分析',
    name: '工序产出分析',
    type: 'pie',
    radius: '60%',
    center: ['25%', '50%'],
@@ -150,7 +150,7 @@
})
const fetchData = () => {
  productSalesAnalysis()
  processOutputAnalysis({ dateType: dateType.value })
    .then((res) => {
      if (res.code === 200 && Array.isArray(res.data)) {
        const items = res.data
@@ -162,7 +162,7 @@
      }
    })
    .catch((err) => {
      console.error('获取产品销售金额分析失败:', err)
      console.error('获取工序产出分析失败:', err)
    })
}
src/views/reportAnalysis/productionAnalysis/components/right-bottom.vue
@@ -28,29 +28,27 @@
import DateTypeSwitch from './DateTypeSwitch.vue'
import Echarts from '@/components/Echarts/echarts.vue'
const dateType = ref(1) // 1=周 2=月 3=季度
const dateType = ref(1)
const chartStyle = {
  width: '100%',
  height: '140%',
}
const grid = { left: '10%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  textStyle: { color: '#B8C8E0' },
  data: ['产量', '工资', '合格率'],
  data: ['完成数量', '工资金额', '合格率'],
}
// æŸ±çŠ¶å›¾ï¼šäº§é‡ã€å·¥èµ„ï¼›æŠ˜çº¿å›¾ï¼šåˆæ ¼çŽ‡ï¼ˆç»¿è‰²ï¼‰
const chartSeries = ref([
  {
    name: '产量',
    name: '完成数量',
    type: 'bar',
    barWidth: 20,
    barGap: '40%',
    yAxisIndex: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
@@ -68,11 +66,10 @@
    data: [],
  },
  {
    name: '工资',
    name: '工资金额',
    type: 'bar',
    barGap: '40%',
    barWidth: 20,
    yAxisIndex: 1,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
@@ -92,7 +89,7 @@
  {
    name: '合格率',
    type: 'line',
    yAxisIndex: 2,
    yAxisIndex: 1,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
@@ -103,31 +100,21 @@
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      let unit = 'ä»¶'
      if (item.seriesName === '合格率') unit = '%'
      else if (item.seriesName === '工资') unit = '元'
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    })
    return result
  },
}
const xAxis1 = ref([
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: '产量(ä»¶)', position: 'left', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  { type: 'value', name: '工资(元)', position: 'left', offset: 50, axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
    type: 'value',
    name: '数量/金额',
    axisLabel: { color: '#B8C8E0' },
    nameTextStyle: { color: '#B8C8E0' },
    // splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
  },
  {
    type: 'value',
    name: '合格率(%)',
    position: 'right',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
@@ -136,6 +123,19 @@
  },
]
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      const unit = item.seriesName === '合格率' ? '%' : (item.seriesName === '工资金额' ? ' å…ƒ' : ' ä¸ª')
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    })
    return result
  },
}
const handleDateTypeChange = () => {
  fetchData()
}
@@ -143,28 +143,19 @@
const fetchData = () => {
  productionAccountingAnalysis({ type: dateType.value })
    .then((res) => {
      console.log('res ======> ', res)
      if (!Array.isArray(res?.data)) return
      if (res.code === 200 && Array.isArray(res.data)) {
      const items = res.data
      xAxis1.value[0].data = items.map(d => d.dateStr)
      // äº§é‡
      chartSeries.value[0].data = items.map(d => Number(d.numberOfCompleted) || 0)
      // å·¥èµ„
      chartSeries.value[1].data = items.map(d => Number(d.amount) || 0)
      // åˆæ ¼çއ
      chartSeries.value[2].data = items.map(d => Number(d.passRate) || 0)
        xAxis1.value[0].data = items.map(item => item.dateStr)
        chartSeries.value[0].data = items.map(item => Number(item.numberOfCompleted) || 0)
        chartSeries.value[1].data = items.map(item => Number(item.amount) || 0)
        chartSeries.value[2].data = items.map(item => Number(item.passRate) || 0)
      }
    })
    .catch((err) => {
      console.error('获取产量、工资与合格率数据失败:', err)
      console.error('数据加载失败', err)
    })
}
onMounted(() => {
  fetchData()
@@ -191,5 +182,6 @@
  padding: 18px;
  width: 100%;
  height: 449px;
  box-sizing: border-box;
}
</style>
src/views/reportAnalysis/productionAnalysis/components/right-top.vue
@@ -2,9 +2,21 @@
  <div>
    <PanelHeader title="工单执行效率分析" />
    <div class="main-panel panel-item-customers">
      <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="barLegend" :series="chartSeries"
        :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }" style="height: 260px" />
      <div class="filters-row">
        <DateTypeSwitch v-model="dateType" @change="handleDateTypeChange" />
      </div>
      <Echarts
        ref="chart"
        :chartStyle="chartStyle"
        :grid="grid"
        :legend="barLegend"
        :series="chartSeries"
        :tooltip="tooltip"
        :xAxis="xAxis1"
        :yAxis="yAxis1"
        :options="{ backgroundColor: 'transparent', textStyle: { color: '#B8C8E0' } }"
        style="height: 260px"
      />
    </div>
  </div>
</template>
@@ -14,10 +26,13 @@
import { workOrderEfficiencyAnalysis } from '@/api/viewIndex.js'
import PanelHeader from './PanelHeader.vue'
import Echarts from '@/components/Echarts/echarts.vue'
import DateTypeSwitch from './DateTypeSwitch.vue'
const dateType = ref(1) // 1=周 2=月 3=季度
const chartStyle = {
  width: '100%',
  height: '160%',
  height: '140%',
}
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
@@ -115,24 +130,25 @@
  },
]
const handleDateTypeChange = () => {
  fetchData()
}
const fetchData = () => {
  workOrderEfficiencyAnalysis()
  workOrderEfficiencyAnalysis({ dateType: dateType.value })
    .then((res) => {
      // æ ¹æ®ä½ çš„结构,数据直接在 res.data ä¸­
      if (!res?.data || !Array.isArray(res.data)) return
      const list = res.data
      xAxis1.value[0].data = list.map((item) => item.date)
      chartSeries.value[0].data = list.map((item) => Number(item.startQuantity) || 0)
      chartSeries.value[1].data = list.map((item) => Number(item.finishQuantity) || 0)
      chartSeries.value[2].data = list.map((item) => Number(item.yieldRate) || 0)
      if (res.code !== 200 || !Array.isArray(res.data)) return
      const items = res.data
      xAxis1.value[0].data = items.map((d) => d.date)
      // å¼€å·¥
      chartSeries.value[0].data = items.map((d) => Number(d.startQuantity) || 0)
      // å®Œæˆ
      chartSeries.value[1].data = items.map((d) => Number(d.finishQuantity) || 0)
      // è‰¯å“çއ
      chartSeries.value[2].data = items.map((d) => Math.min(100, parseFloat(d.yieldRate) || 0))
    })
    .catch((err) => {
      console.error('获取工单效率数据失败:', err)
      console.error('获取工单执行效率分析失败:', err)
    })
}
@@ -148,6 +164,14 @@
  gap: 20px;
}
.filters-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.panel-item-customers {
  border: 1px solid #1a58b0;
  padding: 18px;
src/views/reportAnalysis/qualityAnalysis/components/center-bottom.vue
@@ -1,7 +1,9 @@
<template>
  <div>
    <div class="chart-header">
      <div class="chart-header-title">
      <PanelHeader title="完成检验数" />
      </div>
      <div class="warn-range" @click="handleRangeClick">近7天</div>
    </div>
    <div class="main-panel panel-item-customers">
@@ -29,28 +31,23 @@
const chartStyle = {
  width: '100%',
  height: '135%',
  height: '140%',
}
const grid = { left: '8%', right: '8%', bottom: '8%', top: '15%', containLabel: true }
const grid = { left: '3%', right: '4%', bottom: '3%', top: '10%', containLabel: true }
const barLegend = {
  show: true,
  top: '5%',
  left: 'center',
  textStyle: { color: '#B8C8E0', fontSize: 14 },
  itemGap: 30,
  textStyle: { color: '#B8C8E0' },
  data: ['合格', '不合格', '合格率'],
}
// æŸ±çŠ¶å›¾ï¼šåˆæ ¼ï¼ˆé»„è‰²ï¼‰ã€ä¸åˆæ ¼ï¼ˆç´«è‰²ï¼‰ï¼›æŠ˜çº¿å›¾ï¼šåˆæ ¼çŽ‡ï¼ˆè“è‰²ï¼‰
const chartSeries = ref([
  {
    name: '合格',
    type: 'bar',
    barWidth: 20,
    barGap: '20%',
    yAxisIndex: 0,
    barGap: '40%',
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
@@ -60,8 +57,8 @@
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 0, color: 'rgba(255, 215, 0, 1)' }, // é‡‘黄色顶部
          { offset: 1, color: 'rgba(255, 215, 0, 0.5)' }, // åŠé€æ˜Žåº•部
          { offset: 1, color: 'rgba(0, 164, 237, 0)' },
          { offset: 0, color: 'rgba(78, 228, 255, 1)' },
        ],
      },
    },
@@ -70,9 +67,8 @@
  {
    name: '不合格',
    type: 'bar',
    barGap: '20%',
    barGap: '40%',
    barWidth: 20,
    yAxisIndex: 0,
    emphasis: { focus: 'series' },
    itemStyle: {
      color: {
@@ -82,8 +78,8 @@
        x2: 0,
        y2: 1,
        colorStops: [
          { offset: 0, color: 'rgba(144, 97, 248, 1)' }, // ç´«è‰²é¡¶éƒ¨
          { offset: 1, color: 'rgba(144, 97, 248, 0.6)' }, // åŠé€æ˜Žåº•部
          { offset: 1, color: 'rgba(83, 126, 245, 0.19)' },
          { offset: 0, color: 'rgba(144, 97, 248, 1)' },
        ],
      },
    },
@@ -93,87 +89,43 @@
    name: '合格率',
    type: 'line',
    yAxisIndex: 1,
    smooth: true,
    showSymbol: true,
    symbol: 'circle',
    symbolSize: 8,
    lineStyle: {
      color: 'rgba(78, 228, 255, 1)', // é’色
      width: 2,
    },
    itemStyle: {
      color: 'rgba(78, 228, 255, 1)',
      borderWidth: 2,
      borderColor: '#fff',
    },
    emphasis: {
      focus: 'series',
      itemStyle: {
        shadowBlur: 10,
        shadowColor: 'rgba(78, 228, 255, 0.8)',
      },
    },
    lineStyle: { color: 'rgba(90, 216, 166, 1)', width: 2 },
    itemStyle: { color: 'rgba(90, 216, 166, 1)' },
    data: [],
    emphasis: { focus: 'series' },
  },
])
const tooltip = {
  trigger: 'axis',
  axisPointer: { type: 'cross' },
  backgroundColor: 'rgba(0, 0, 0, 0.8)',
  borderColor: 'rgba(78, 228, 255, 0.5)',
  borderWidth: 1,
  textStyle: { color: '#B8C8E0' },
  formatter(params) {
    let result = params[0].axisValueLabel + '<br/>'
    params.forEach((item) => {
      let unit = ''
      if (item.seriesName === '合格率') {
        unit = '%'
      } else {
        unit = 'ä»¶'
      }
      result += `<div style="margin: 4px 0;">${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
      const unit = item.seriesName === '合格率' ? '%' : 'ä»¶'
      result += `<div>${item.marker} ${item.seriesName}: ${item.value}${unit}</div>`
    })
    return result
  },
}
const xAxis1 = ref([
  {
    type: 'category',
    axisTick: { show: false },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    axisLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.3)' } },
    data: [],
  },
  { type: 'category', axisTick: { show: false }, axisLabel: { color: '#B8C8E0' }, data: [] },
])
const yAxis1 = [
  { type: 'value', name: 'ä»¶', axisLabel: { color: '#B8C8E0' }, nameTextStyle: { color: '#B8C8E0' } },
  {
    type: 'value',
    name: '单位: ä»¶',
    nameLocation: 'start',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 10] },
    axisLabel: { color: '#B8C8E0', fontSize: 12 },
    axisLine: { show: false },
    splitLine: {
      show: true,
      lineStyle: { color: 'rgba(184, 200, 224, 0.2)', type: 'dashed' },
    },
  },
  {
    type: 'value',
    name: '单位: %',
    nameLocation: 'end',
    nameTextStyle: { color: '#B8C8E0', fontSize: 12, padding: [0, 0, 0, 10] },
    name: '合格率(%)',
    min: 0,
    max: 100,
    axisLabel: { color: '#B8C8E0', fontSize: 12, formatter: '{value}' },
    axisLine: { show: false },
    splitLine: {
      show: true,
      lineStyle: { color: 'rgba(184, 200, 224, 0.2)', type: 'dashed' },
    },
    axisLabel: { color: '#B8C8E0', formatter: '{value}%' },
    nameTextStyle: { color: '#B8C8E0' },
    splitLine: { lineStyle: { color: 'rgba(184, 200, 224, 0.2)' } },
  },
]
@@ -212,6 +164,13 @@
  position: relative;
  display: flex;
  align-items: center;
  width: 100%;
}
.chart-header-title {
  flex: 1;
  min-width: 0;
  width: 100%;
}
.warn-range {
@@ -247,8 +206,6 @@
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 449px;
  position: relative;
  background: radial-gradient(circle at 50% 50%, rgba(78, 228, 255, 0.05) 0%, rgba(0, 0, 0, 0) 70%);
  height: 436px;
}
</style>
src/views/reportAnalysis/qualityAnalysis/components/center-top.vue
@@ -117,6 +117,8 @@
  gap: 6px;
  font-size: 15px;
  color: #d0e7ff;
  white-space: nowrap;
  flex-wrap: nowrap;
}
.card-compare>span:first-child {
src/views/reportAnalysis/qualityAnalysis/components/left-bottom.vue
ÎļþÒÑɾ³ý
src/views/reportAnalysis/qualityAnalysis/components/left-top.vue
@@ -220,7 +220,7 @@
})
</script>
<style scoped>
<style scoped lang="scss">
.main-panel {
  display: flex;
  flex-direction: column;
@@ -304,7 +304,7 @@
  border: 1px solid #1a58b0;
  padding: 14px 18px;
  width: 100%;
  height: 960px;
  height: 958px;
  box-sizing: border-box;
}
src/views/reportAnalysis/qualityAnalysis/components/right-bottom.vue
@@ -163,7 +163,7 @@
  border: 1px solid #1a58b0;
  padding: 18px;
  width: 100%;
  height: 420px;
  height: 449px;
}
.pie-chart-wrapper {
src/views/reportAnalysis/qualityAnalysis/index.vue
@@ -13,7 +13,7 @@
    <!-- é¡¶éƒ¨æ ‡é¢˜æ  -->
    <div class="dashboard-header">
      <div class="factory-name">进销质量类分析</div>
      <div class="factory-name">质量数据分析</div>
    </div>
    <!-- ä¸»è¦å†…容区域 -->
@@ -43,7 +43,6 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
import autofit from 'autofit.js'
import LeftBottom from './components/left-bottom.vue'
import CenterCenter from './components/center-center.vue'
import RightTop from './components/right-top.vue'
import RightBottom from './components/right-bottom.vue'
src/views/system/menu/index.vue
@@ -84,8 +84,8 @@
      </el-table>
      <!-- æ·»åŠ æˆ–ä¿®æ”¹èœå•å¯¹è¯æ¡† -->
      <el-dialog :title="title" v-model="open" width="680px" append-to-body>
         <el-form ref="menuRef" :model="form" :rules="rules" label-width="100px">
      <el-dialog :title="title" v-model="open" width="880px" append-to-body>
         <el-form ref="menuRef" :model="form" :rules="rules" label-width="130px">
            <el-row>
               <el-col :span="24">
                  <el-form-item label="上级菜单">
@@ -194,6 +194,19 @@
                        </span>
                     </template>
                     <el-input v-model="form.component" placeholder="请输入组件路径" />
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType == 'C'">
                  <el-form-item prop="appComponent">
                     <template #label>
                        <span>
                           <el-tooltip content="APP ç«¯è®¿é—®çš„组件路径,如:`app/system/user/index`" placement="top">
                              <el-icon><question-filled /></el-icon>
                           </el-tooltip>
                           APP组件路径
                        </span>
                     </template>
                     <el-input v-model="form.appComponent" placeholder="请输入 APP ç»„件路径(可选)" />
                  </el-form-item>
               </el-col>
               <el-col :span="12" v-if="form.menuType != 'M'">
@@ -316,7 +329,8 @@
  rules: {
    menuName: [{ required: true, message: "菜单名称不能为空", trigger: "blur" }],
    orderNum: [{ required: true, message: "菜单顺序不能为空", trigger: "blur" }],
    path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }]
    path: [{ required: true, message: "路由地址不能为空", trigger: "blur" }],
    appComponent: [{ required: false, message: "APP组件路径不能为空", trigger: "blur" }]
  },
})
@@ -359,7 +373,8 @@
    isFrame: "1",
    isCache: "0",
    visible: "0",
    status: "0"
    status: "0",
    appComponent: undefined
  }
  proxy.resetForm("menuRef")
}