| src/api/costAccounting/productionSettlementBatches.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/productionPlan/productionPlan.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/reportAnalysis/salesStatistics.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/costAccounting/stdVsActCostAnalysis/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/processRoute/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionManagement/processRoute/processRouteItem/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionPlan/productionPlan/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/productionPlan/trackProgress/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/reportAnalysis/salesStatistics/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/api/costAccounting/productionSettlementBatches.js
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,53 @@ import request from "@/utils/request"; // è·å产åç±»å«ï¼ææä»½ï¼ export function getProductTypes(params) { return request({ url: "/productionSettlementBatches/getProductTypes", method: "get", params, }); } // è·åç§ç®ç±»å«ï¼ææä»½ï¼ export function getSubjectNames(params) { return request({ url: "/productionSettlementBatches/getSubjectNames", method: "get", params, }); } // æ åææ¬å¯¼å ¥ï¼el-upload éè¦å®æ´ URLï¼ export function getImportActionUrl() { return `${import.meta.env.VITE_APP_BASE_API}/productionSettlementBatches/import`; } // ä¸è½½å¯¼å ¥æ¨¡æ¿ï¼GETï¼è¿å blobï¼ export function downloadTemplate(params) { return request({ url: "/productionSettlementBatches/downloadTemplate", method: "get", params, responseType: "blob", }); } // æ ¸ç®æ¥è¯¢ï¼ææä»½/产åç±»å/ç§ç®/ææ¬ç±»åï¼ export function getSettlement(params) { return request({ url: "/productionSettlementBatches/getSettlement", method: "get", params, }); } // æ±æ»ææ¬ï¼ç¬¬äºæ¨¡å KPIï¼ export function getTotalCosts(params) { return request({ url: "/productionSettlementBatches/getTotalCosts", method: "get", params, }); } src/api/productionPlan/productionPlan.js
@@ -68,4 +68,14 @@ method: "post", data: query, }); } } // 追踪è¿åº¦ export function trackProgressByNo(query) { return request({ url: "/track/trackProgressByNo", method: "get", params: query, }); } src/api/reportAnalysis/salesStatistics.js
@@ -14,4 +14,20 @@ url: '/home/total', method: 'get' }) } // ééåæè¶å¿å¾ export function getSalesAnalysisTrend(params) { return request({ url: '/home/salesAnalysis', method: 'get', params: params }) } // éå®éé¢åæ export function getSalesAmountAnalysis(params) { return request({ url: '/home/salesAmount', method: 'get', params: params }) } src/views/costAccounting/stdVsActCostAnalysis/index.vue
@@ -29,32 +29,30 @@ <div class="filter-layout"> <el-form :model="searchForm" :inline="true" class="filter-form"> <el-form-item label="æä»½èå´"> <el-form-item label="æä»½"> <el-date-picker v-model="searchForm.monthRange" type="monthrange" range-separator="è³" start-placeholder="å¼å§æä»½" end-placeholder="ç»ææä»½" v-model="searchForm.month" type="month" value-format="YYYY-MM" placeholder="éæ©æä»½" class="w-260" @change="handleQuery" @change="handleMonthChange" /> </el-form-item> <el-form-item label="产åç±»å«"> <el-form-item label="产åç±»å"> <el-select v-model="searchForm.category" v-model="searchForm.productType" clearable filterable placeholder="å ¨é¨ç±»å«" placeholder="å ¨é¨ç±»å" class="w-180" @change="handleQuery" > <el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> @@ -62,12 +60,16 @@ <el-select v-model="searchForm.costType" clearable placeholder="å ¨é¨ç±»å" placeholder="å ¨é¨ææ¬ç±»å" class="w-180" @change="handleQuery" > <el-option label="è½èææ¬" value="è½èææ¬" /> <el-option label="çäº§ææ¬" value="çäº§ææ¬" /> <el-option v-for="item in costTypeOptions" :key="item.value" :label="item.label" :value="item.value" /> </el-select> </el-form-item> </el-form> @@ -78,30 +80,31 @@ <el-button class="lux-btn" @click="handleReset">éç½®</el-button> </div> <div class="action-group"> <el-dropdown trigger="click" @command="handleImportCommand"> <el-button class="lux-btn" type="success" plain> æ åææ¬å¯¼å ¥ <el-icon class="el-icon--right"><ArrowDown /></el-icon> </el-button> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="template">ä¸è½½å¯¼å ¥æ¨¡æ¿</el-dropdown-item> <el-dropdown-item command="upload">Excel å¯¼å ¥</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> <el-upload ref="uploadRef" class="hidden-upload" :auto-upload="false" :show-file-list="false" accept=".xlsx,.xls" :on-change="handleFileChange" /> <el-button class="lux-btn" type="success" plain @click="openImportDialog"> æ åææ¬å¯¼å ¥ </el-button> </div> </div> </div> </el-card> <ImportDialog ref="importDialogRef" v-model="importDialogVisible" title="æ åææ¬å¯¼å ¥" width="520px" :headers="importHeaders" :action="importAction" :auto-upload="false" :limit="1" tip-text="ä» å è®¸å¯¼å ¥ xlsãxlsx æ ¼å¼æä»¶ã" :show-download-template="true" :on-success="handleImportSuccess" @confirm="handleImportConfirm" @download-template="downloadTemplate" @close="handleImportDialogClose" @cancel="handleImportDialogClose" /> <el-card class="panel-card glass-card kpi-card" shadow="never"> <div class="kpi-strip"> @@ -194,29 +197,23 @@ </div> </template> <el-table :data="pagedTableData" stripe class="lux-table" @sort-change="handleSortChange"> <el-table-column prop="month" label="æä»½" width="110" /> <el-table-column prop="category" label="产åç±»å«" min-width="140" /> <el-table-column prop="periodTime" label="æä»½" width="110" /> <el-table-column prop="productType" label="产åç±»å" min-width="140" /> <el-table-column prop="costType" label="ææ¬ç±»å" min-width="120" /> <el-table-column prop="standardCost" label="æ åææ¬(å )" sortable="custom" align="right"> <template #default="scope">Â¥{{ formatMoney(scope.row.standardCost) }}</template> <el-table-column prop="subjectName" label="ç§ç®" min-width="140" show-overflow-tooltip /> <el-table-column prop="budgetQty" label="é¢ç®èé" sortable="custom" align="right" min-width="120" /> <el-table-column prop="budgetPrice" label="é¢ç®åä»·" sortable="custom" align="right" min-width="120" /> <el-table-column prop="budgetTotal" label="é¢ç®æ»ææ¬" sortable="custom" align="right" min-width="130"> <template #default="scope">Â¥{{ formatMoney(scope.row.budgetTotal) }}</template> </el-table-column> <el-table-column prop="actualCost" label="å®é ææ¬(å )" sortable="custom" align="right"> <template #default="scope">Â¥{{ formatMoney(scope.row.actualCost) }}</template> <el-table-column prop="actualQty" label="å®é èé" sortable="custom" align="right" min-width="120" /> <el-table-column prop="actualPrice" label="å®é åä»·" sortable="custom" align="right" min-width="120" /> <el-table-column prop="actualTotal" label="å®é æ»ææ¬" sortable="custom" align="right" min-width="130"> <template #default="scope">Â¥{{ formatMoney(scope.row.actualTotal) }}</template> </el-table-column> <el-table-column prop="diff" label="å·®å¼(å )" sortable="custom" align="right"> <template #default="scope"> <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'"> {{ formatSignedMoney(scope.row.diff) }} </span> </template> </el-table-column> <el-table-column prop="diffRate" label="å·®å¼ç" sortable="custom" align="right"> <template #default="scope"> <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'"> {{ formatPercent(scope.row.diffRate) }} </span> </template> </el-table-column> <el-table-column prop="diffQty" label="èéå·®å¼" min-width="110" align="right" /> <el-table-column prop="diffPrice" label="åä»·å·®å¼" min-width="110" align="right" /> <el-table-column prop="diffTotal" label="æ»ææ¬å·®å¼" min-width="110" align="right" /> </el-table> <div class="pagination-container"> <el-pagination @@ -247,23 +244,40 @@ ZoomIn, } from "@element-plus/icons-vue"; import { ElMessage } from "element-plus"; import ImportDialog from "@/components/Dialog/ImportDialog.vue"; import { getToken } from "@/utils/auth.js"; import * as echarts from "echarts"; // import * as XLSX from "xlsx"; import { downloadTemplate as downloadProductionSettlementTemplate, getImportActionUrl, getSettlement, getTotalCosts, getProductTypes, } from "@/api/costAccounting/productionSettlementBatches"; const getDefaultMonthRange = () => { const getDefaultMonth = () => { const end = new Date(); const start = new Date(); start.setMonth(start.getMonth() - 2); return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)]; return end.toISOString().slice(0, 7); }; const searchForm = reactive({ monthRange: getDefaultMonthRange(), category: "", month: getDefaultMonth(), productType: "", costType: "", }); const uploadRef = ref(); const categoryOptions = ref([]); const costTypeOptions = ref([]); const importDialogVisible = ref(false); const importDialogRef = ref(null); const importHeaders = computed(() => ({ Authorization: `Bearer ${getToken()}`, })); const importAction = computed(() => getImportActionUrl()); const chartRef = ref(null); const largeChartRef = ref(null); let chartInstance = null; @@ -271,150 +285,22 @@ const largeChartVisible = ref(false); const currentChartOption = ref(null); // ------------------------------ // åæ°æ®ï¼ç¨äºå èè°é¡µé¢æ¸²æ // ------------------------------ const actualCostSource = ref([]); const standardCostSource = ref([]); const fakeMonths = ["2026-01", "2026-02", "2026-03"]; const fakeCategories = [ "ç²ç ¤ç°", "ç³ç°", "æ°´æ³¥", "éç²è", "è±æ¨¡å", "ç³è", "æå 带", "é²è åï¼æ¿æç¨ï¼", "æ°§åéï¼æ¿æç¨ï¼", "å·æ¤ä¸ï¼æ¿æç¨ï¼", "塿£ï¼æ¿æç¨ï¼", "ææå°è®¡", "æ°´", "çµ", "è¸æ±½", ]; const fakeCostType = (category) => (["æ°´", "çµ", "è¸æ±½"].includes(category) ? "è½èææ¬" : "çäº§ææ¬"); // æ¯ä¸ªç±»å«çæ åææ¬åºåå¼ï¼ä» ç¨äºåæ°æ®ï¼ const baseStandardCostByCategory = { ç²ç ¤ç°: 98000, ç³ç°: 52000, æ°´æ³¥: 175000, éç²è: 32000, è±æ¨¡å: 21000, ç³è: 41000, æå 带: 14500, "é²è åï¼æ¿æç¨ï¼": 12500, "æ°§åéï¼æ¿æç¨ï¼": 22000, "å·æ¤ä¸ï¼æ¿æç¨ï¼": 9800, "塿£ï¼æ¿æç¨ï¼": 8600, ææå°è®¡: 420000, æ°´: 6800, çµ: 26000, è¸æ±½: 52000, }; // æä»½æ³¢å¨ç³»æ°ï¼è®©å¾è¡¨çèµ·æ¥æ´âçå®âä¸äºï¼ const monthFactorByMonth = { "2026-01": 1.0, "2026-02": 1.06, "2026-03": 0.97, }; // å®é ææ¬ç¸å¯¹æ åææ¬çåç§»æ¯ä¾ï¼ç¨äºæµè¯æ£è´å·®å¼å±ç¤ºï¼ const diffRatioByCategory = { ç²ç ¤ç°: 0.05, ç³ç°: -0.01, æ°´æ³¥: 0.03, éç²è: 0.0, è±æ¨¡å: -0.04, ç³è: 0.02, æå 带: -0.03, "é²è åï¼æ¿æç¨ï¼": 0.06, "æ°§åéï¼æ¿æç¨ï¼": -0.02, "å·æ¤ä¸ï¼æ¿æç¨ï¼": 0.01, "塿£ï¼æ¿æç¨ï¼": -0.05, ææå°è®¡: 0.02, æ°´: -0.01, çµ: 0.04, è¸æ±½: -0.03, }; const buildFakeSources = () => { const stdRows = []; const actRows = []; for (const month of fakeMonths) { const monthFactor = monthFactorByMonth[month] ?? 1; const monthAdj = month === "2026-02" ? 0.005 : month === "2026-03" ? -0.006 : 0; for (const category of fakeCategories) { const costType = fakeCostType(category); const base = baseStandardCostByCategory[category] ?? 0; const standardCost = Math.round(base * monthFactor); const diffRatio = (diffRatioByCategory[category] ?? 0) + monthAdj; const actualCost = Math.round(standardCost * (1 + diffRatio)); stdRows.push({ month, category, costType, standardCost }); actRows.push({ month, category, costType, actualCost }); } } standardCostSource.value = stdRows; actualCostSource.value = actRows; }; buildFakeSources(); const categoryOptions = computed(() => { const all = [...actualCostSource.value, ...standardCostSource.value]; return Array.from(new Set(all.map((item) => item.category))); const settlementRows = ref([]); const totalCosts = reactive({ budgetTotal: 0, actualTotal: 0, diffTotal: 0, diffRate: "0%", }); const inRange = (value, range) => { if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true; return value >= range[0] && value <= range[1]; }; const mergedRows = computed(() => { const key = (item) => `${item.month}__${item.category}__${item.costType}`; const stdMap = new Map(standardCostSource.value.map((item) => [key(item), item])); const actMap = new Map(actualCostSource.value.map((item) => [key(item), item])); const keySet = new Set([...stdMap.keys(), ...actMap.keys()]); const rows = []; for (const k of keySet) { const std = stdMap.get(k); const act = actMap.get(k); const month = std?.month || act?.month || ""; const category = std?.category || act?.category || ""; const costType = std?.costType || act?.costType || ""; const standardCost = Number(std?.standardCost || 0); const actualCost = Number(act?.actualCost || 0); const diff = actualCost - standardCost; const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100; rows.push({ month, category, costType, standardCost, actualCost, diff, diffRate }); } return rows.sort((a, b) => { if (a.month !== b.month) return a.month > b.month ? 1 : -1; if (a.category !== b.category) return a.category.localeCompare(b.category, "zh-Hans-CN"); return a.costType.localeCompare(b.costType, "zh-Hans-CN"); const tableData = computed(() => { return (Array.isArray(settlementRows.value) ? settlementRows.value : []).filter((item) => { const hitMonth = !searchForm.month || item.periodTime === searchForm.month; const hitProductType = !searchForm.productType || item.productType === searchForm.productType; const hitCostType = !searchForm.costType || item.costType === searchForm.costType; return hitMonth && hitProductType && hitCostType; }); }); const tableData = computed(() => mergedRows.value.filter((item) => { const hitMonth = inRange(item.month, searchForm.monthRange); const hitCategory = !searchForm.category || item.category === searchForm.category; const hitCostType = !searchForm.costType || item.costType === searchForm.costType; return hitMonth && hitCategory && hitCostType; }) ); const page = reactive({ current: 1, @@ -454,26 +340,45 @@ return sortedTableData.value.slice(start, start + page.size); }); const overview = computed(() => { const standardCost = tableData.value.reduce((sum, item) => sum + item.standardCost, 0); const actualCost = tableData.value.reduce((sum, item) => sum + item.actualCost, 0); const diff = actualCost - standardCost; const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100; return { standardCost, actualCost, diff, diffRate }; }); const overview = computed(() => ({ standardCost: Number(totalCosts.budgetTotal || 0), actualCost: Number(totalCosts.actualTotal || 0), diff: Number(totalCosts.diffTotal || 0), diffRate: totalCosts.diffRate ?? "0%", })); const getChartData = () => { const xAxis = tableData.value.map( (item) => `${item.month}\n${item.category}-${item.costType.replace("ææ¬", "")}` // å¾è¡¨å£å¾ï¼æâç§ç®âæ±æ»å±ç¤ºå ¨é¨ç§ç® const agg = new Map(); for (const row of tableData.value) { const subjectName = String(row?.subjectName || "").trim() || "-"; const budgetTotal = Number(row?.budgetTotal || 0); const actualTotal = Number(row?.actualTotal || 0); const bucket = agg.get(subjectName) || { subjectName, budgetTotal: 0, actualTotal: 0 }; bucket.budgetTotal += Number.isFinite(budgetTotal) ? budgetTotal : 0; bucket.actualTotal += Number.isFinite(actualTotal) ? actualTotal : 0; agg.set(subjectName, bucket); } const rows = Array.from(agg.values()).sort((a, b) => String(a.subjectName).localeCompare(String(b.subjectName), "zh-Hans-CN") ); const standard = tableData.value.map((item) => item.standardCost); const actual = tableData.value.map((item) => item.actualCost); const diffRate = tableData.value.map((item) => Number(item.diffRate.toFixed(2))); return { xAxis, standard, actual, diffRate }; const xAxis = rows.map((item) => item.subjectName); const standard = rows.map((item) => item.budgetTotal); const actual = rows.map((item) => item.actualTotal); const diffRate = rows.map((item) => { const base = Number(item.budgetTotal || 0); const diff = Number(item.actualTotal || 0) - base; if (!base) return 0; return (diff / base) * 100; }); return { xAxis, standard, actual, diffRate, rows }; }; const buildChartOption = () => { const { xAxis, standard, actual, diffRate } = getChartData(); const { xAxis, standard, actual, diffRate, rows } = getChartData(); return { animation: true, animationDuration: 920, @@ -493,13 +398,17 @@ textStyle: { color: "rgba(15, 23, 42, 0.88)" }, extraCssText: "box-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); border-radius: 12px;", formatter: (params) => { const row = tableData.value[params[0]?.dataIndex] || {}; const row = rows?.[params[0]?.dataIndex] || {}; const budgetTotal = Number(row?.budgetTotal || 0); const actualTotal = Number(row?.actualTotal || 0); const diff = actualTotal - budgetTotal; const rate = budgetTotal ? (diff / budgetTotal) * 100 : 0; return [ `${row.month || ""} ${row.category || ""} ${row.costType || ""}`, `æ åææ¬ï¼Â¥${formatMoney(row.standardCost || 0)}`, `å®é ææ¬ï¼Â¥${formatMoney(row.actualCost || 0)}`, `å·®å¼ï¼${formatSignedMoney(row.diff || 0)}`, `å·®å¼çï¼${formatPercent(row.diffRate || 0)}`, `ç§ç®ï¼${row.subjectName || "-"}`, `é¢ç®æ»ææ¬ï¼Â¥${formatMoney(budgetTotal)}`, `å®é æ»ææ¬ï¼Â¥${formatMoney(actualTotal)}`, `å·®å¼ï¼${formatSignedMoney(diff)}`, `å·®å¼çï¼${formatPercent(rate)}`, ].join("<br/>"); }, }, @@ -636,72 +545,144 @@ standardCostSource.value = Array.from(map.values()); }; const handleFileChange = async (uploadFile) => { try { const file = uploadFile.raw; if (!file) return; const data = await file.arrayBuffer(); const workbook = XLSX.read(data, { type: "array" }); const sheetName = workbook.SheetNames[0]; const sheet = workbook.Sheets[sheetName]; const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" }); const parsed = parseImportedRows(rows); if (!parsed.length) { ElMessage.warning("å¯¼å ¥å¤±è´¥ï¼æ¨¡æ¿å 容为空æå段ä¸å¹é "); return; } replaceStandardSourceByImport(parsed); ElMessage.success(`å¯¼å ¥æåï¼${parsed.length} æ¡æ åææ¬è®°å½`); handleQuery(); } catch (error) { console.error(error); ElMessage.error("å¯¼å ¥å¤±è´¥ï¼è¯·æ£æ¥ Excel æ ¼å¼"); } finally { uploadRef.value?.clearFiles?.(); } const openImportDialog = () => { importDialogVisible.value = true; }; const openUploadSelector = () => { const input = uploadRef.value?.$el?.querySelector?.("input[type='file']"); if (!input) { ElMessage.warning("ä¸ä¼ ç»ä»¶å°æªå°±ç»ªï¼è¯·ç¨åéè¯"); return; } input.click(); const handleImportConfirm = () => { importDialogRef.value?.submit?.(); }; const handleImportCommand = (command) => { if (command === "template") { downloadTemplate(); return; } if (command === "upload") { openUploadSelector(); } const handleImportDialogClose = () => { importDialogRef.value?.clearFiles?.(); }; const downloadTemplate = () => { const sample = [ { æä»½: "2026-03", 产åç±»å«: "ç²ç ¤ç°", ææ¬ç±»å: "æ åçäº§ææ¬", æ åææ¬: 98000 }, { æä»½: "2026-03", 产åç±»å«: "æ°´æ³¥", ææ¬ç±»å: "æ åçäº§ææ¬", æ åææ¬: 175000 }, { æä»½: "2026-03", 产åç±»å«: "çµ", ææ¬ç±»å: "æ åè½èææ¬", æ åææ¬: 26000 }, { æä»½: "2026-03", 产åç±»å«: "è¸æ±½", ææ¬ç±»å: "æ åè½èææ¬", æ åææ¬: 52000 }, { æä»½: "2026-03", 产åç±»å«: "æ°´", ææ¬ç±»å: "æ åè½èææ¬", æ åææ¬: 6800 }, ]; const ws = XLSX.utils.json_to_sheet(sample); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "æ åææ¬æ¨¡æ¿"); XLSX.writeFile(wb, "æ åææ¬ææå¯¼å ¥æ¨¡æ¿.xlsx"); ElMessage.success("模æ¿å·²ä¸è½½"); downloadProductionSettlementTemplate({ periodTime: searchForm.month || undefined }) .then((data) => { const blob = data instanceof Blob ? data : new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = "æ åææ¬å¯¼å ¥æ¨¡æ¿.xlsx"; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); ElMessage.success("模æ¿ä¸è½½æå"); }) .catch(() => { ElMessage.error("模æ¿ä¸è½½å¤±è´¥"); }); }; const handleQuery = () => { updateChart(); const handleImportSuccess = (response) => { const code = response?.code; const msg = response?.msg || response?.message; if (code === 200) { ElMessage.success(msg || "å¯¼å ¥æå"); importDialogVisible.value = false; importDialogRef.value?.clearFiles?.(); handleQuery(); return; } ElMessage.error(msg || "å¯¼å ¥å¤±è´¥"); }; const normalizeSettlementResponse = (data) => { const map = data && typeof data === "object" ? data : {}; const rows = []; for (const [costType, list] of Object.entries(map)) { if (!Array.isArray(list)) continue; for (const item of list) { rows.push({ ...item, periodTime: searchForm.month, costType: costType, }); } } return rows; }; const fetchCategoryOptions = async () => { if (!searchForm.month) { categoryOptions.value = []; return; } try { const { data } = await getProductTypes({ periodTime: searchForm.month }); const list = Array.isArray(data) ? data : data?.records || []; categoryOptions.value = list.map((item) => ({ label: item.label || item.name || item.typeName || item, value: item.value || item.code || item.typeCode || item, })); } catch (e) { categoryOptions.value = []; } }; const fetchCostTypeOptions = async () => { if (!searchForm.month) { costTypeOptions.value = []; return; } try { // ä¸å¸¦ costTypeï¼æ¿å°å®æ´åç» key ç¨äºä¸æé项 const res = await getSettlement({ periodTime: searchForm.month }); const map = res?.data || {}; const keys = Object.keys(map || {}); costTypeOptions.value = keys.map((k) => ({ label: k, value: k })); } catch (e) { costTypeOptions.value = []; } }; const handleMonthChange = () => { searchForm.productType = ""; searchForm.costType = ""; Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => { handleQuery(); }); }; const handleQuery = async () => { try { const params = { periodTime: searchForm.month || undefined, productType: searchForm.productType || undefined, costType: searchForm.costType || undefined, }; const [settlementRes, totalRes] = await Promise.all([getSettlement(params), getTotalCosts(params)]); const map = settlementRes?.data || {}; const rows = normalizeSettlementResponse(map); settlementRows.value = rows; const totals = totalRes?.data || {}; totalCosts.budgetTotal = Number(totals.budgetTotal || 0); totalCosts.actualTotal = Number(totals.actualTotal || 0); totalCosts.diffTotal = Number(totals.diffTotal || 0); totalCosts.diffRate = totals.diffRate ?? "0%"; updateChart(); } catch (e) { settlementRows.value = []; totalCosts.budgetTotal = 0; totalCosts.actualTotal = 0; totalCosts.diffTotal = 0; totalCosts.diffRate = "0%"; updateChart(); } }; const handleReset = () => { searchForm.monthRange = getDefaultMonthRange(); searchForm.category = ""; searchForm.month = getDefaultMonth(); searchForm.productType = ""; searchForm.costType = ""; tableSort.prop = ""; tableSort.order = ""; @@ -735,6 +716,7 @@ }; const formatPercent = (v) => { if (typeof v === "string" && v.trim().endsWith("%")) return v.trim(); const n = Number.parseFloat(v); const value = Number.isFinite(n) ? n : 0; const sign = value >= 0 ? "+" : ""; @@ -802,6 +784,9 @@ } updateChart(); }); Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => { handleQuery(); }); window.addEventListener("resize", handleResize); }); src/views/index.vue
@@ -2,9 +2,9 @@ <div class="home-page"> <div class="top-bar"> <div class="user-box"> <!-- <img :src="userStore.avatar" <img :src="userStore.avatar" class="avatar" alt="" /> --> alt="" /> <div> <div class="hello">{{ userStore.roleName || "ç³»ç»ç®¡çå" }}ï¼ä½ 好</div> <div class="sub">ç»å½æ¶é´ï¼{{ userStore.currentLoginTime }}</div> @@ -16,9 +16,9 @@ type="primary" plain @click="refreshDashboardData">å·æ°æ°æ®</el-button> <el-button size="small" <!-- <el-button size="small" plain @click="configDialogVisible = true">é¦é¡µé ç½®</el-button> @click="configDialogVisible = true">é¦é¡µé ç½®</el-button> --> </div> </div> <div class="content-grid"> @@ -40,7 +40,7 @@ </el-button> </div> </section> <section class="section-card"> <!-- <section class="section-card"> <div class="section-title">éç¹å¾ å</div> <div class="todo-row" v-for="todo in todos" @@ -49,7 +49,7 @@ :type="todo.type">{{ todo.level }}</el-tag> <span>{{ todo.title }}</span> </div> </section> </section> --> <section class="section-card"> <div class="section-title">ç»è¥å ³æ³¨</div> <div class="focus-row" @@ -61,33 +61,17 @@ </section> <section class="section-card flex-fill-card"> <div class="section-title-row"> <div class="section-title">仿¥å¾ å¤ç</div> <div class="section-title">ä»å¹´éå®éé¢åæ</div> <el-radio-group v-model="pendingFilter" size="small"> <el-radio-button label="all">å ¨é¨</el-radio-button> <el-radio-button label="mine">æç</el-radio-button> <el-radio-button label="high">é«ä¼å </el-radio-button> <el-radio-button label="mine">æ¿æ</el-radio-button> <el-radio-button label="high">ç å</el-radio-button> </el-radio-group> </div> <div class="task-row" v-for="task in filteredPendingTasks" :key="task.id"> <div class="task-left"> <el-tag size="small" :type="task.type">{{ task.level }}</el-tag> <span class="task-title">{{ task.title }}</span> </div> <el-button link type="primary" @click="goTo(task.path)">å»å¤ç</el-button> </div> <el-empty v-if="filteredPendingTasks.length === 0" description="ææ å¾ å¤çäºé¡¹" :image-size="80" /> </section> </div> <div class="right-col"> <section class="section-card" <!-- <section class="section-card" v-if="isSectionVisible('trendCards')"> <div class="section-title">æè¿7å¤©å ³é®ææ è¶å¿</div> <div class="trend-cards"> @@ -111,7 +95,7 @@ </div> </div> </div> </section> </section> --> <section class="section-card" v-if="isSectionVisible('planTrend')"> <div class="section-title-row"> @@ -137,35 +121,25 @@ v-if="isSectionVisible('qualityChart') || isSectionVisible('costChart')"> <section class="section-card" v-if="isSectionVisible('qualityChart')"> <div class="section-title-row"> <div class="section-title">è´¨æ£å¼å¸¸åå¸</div> <el-radio-group v-model="chartRangeQuality" size="small" @change="loadQualityData"> <el-radio-button :label="1">å¨</el-radio-button> <el-radio-button :label="2">æ</el-radio-button> <el-radio-button :label="3">å£åº¦</el-radio-button> </el-radio-group> </div> <div class="section-title">ä»å¹´è½èç¨éè¶å¿</div> <Echarts :chartStyle="chartStyle" :grid="grid" :tooltip="barTooltip" :xAxis="qualityXAxis" :xAxis="energyConsumptionXAxis" :yAxis="valueYAxis" :series="qualitySeries" :series="energyConsumptionSeries" style="height: 260px" /> </section> <section class="section-card" v-if="isSectionVisible('costChart')"> <div class="section-title">è½è䏿æ¬ç»æ</div> <div class="section-title">ä»å¹´è½èç±»åå æ¯</div> <Echarts :chartStyle="chartStyle" :legend="costLegend" :tooltip="pieTooltip" :series="costSeries" :series="energyTypeSeries" style="height: 260px" /> </section> </div> <section class="section-card" <!-- <section class="section-card" v-if="isSectionVisible('warningCenter')"> <div class="section-title">å¼å¸¸é¢è¦ä¸å¿</div> <div class="warning-row" @@ -183,7 +157,7 @@ <el-empty v-if="warningList.length === 0" description="ææ å¼å¸¸é¢è¦" :image-size="80" /> </section> </section> --> <section class="section-card mini-table-wrap" v-if="isSectionVisible('planTable')"> <div class="section-title">çäº§è®¡åæ§è¡æç»</div> @@ -299,6 +273,7 @@ qualityInspectionStatistics, nonComplianceWarning, } from "@/api/viewIndex.js"; import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType"; const router = useRouter(); const userStore = useUserStore(); @@ -542,6 +517,150 @@ data: [], }, ]); // è½èç±»åå æ¯æ°æ® const energyTypeSeries = reactive([ { type: "pie", radius: ["40%", "70%"], center: ["50%", "50%"], avoidLabelOverlap: false, itemStyle: { borderRadius: 10, borderColor: "#fff", borderWidth: 2, }, label: { show: true, formatter: "{b}: {d}%", }, data: [ { value: 0, name: "æ°´", itemStyle: { color: "#409EFF" } }, { value: 0, name: "çµ", itemStyle: { color: "#E6A23C" } }, { value: 0, name: "æ°", itemStyle: { color: "#F56C6C" } }, ], }, ]); // è½èç¨éè¶å¿å¾è¡¨ const energyConsumptionXAxis = reactive({ type: "category", data: [], axisLabel: { rotate: 45, }, }); const energyConsumptionSeries = reactive([ { name: "ç¨æ°´é", type: "bar", data: [], itemStyle: { color: "#409EFF" }, }, { name: "ç¨çµé", type: "bar", data: [], itemStyle: { color: "#E6A23C" }, }, { name: "ç¨æ°é", type: "bar", data: [], itemStyle: { color: "#67C23A" }, }, ]); // 模æè½èæ°æ® const energyData = reactive({ water: 120, electricity: 350, gas: 80, }); // æ´æ°è½èç±»åå æ¯å¾è¡¨åè½èç¨éè¶å¿å¾è¡¨ const updateEnergyTypeChart = () => { // æå»ºåæ°ï¼ä»å¹´çå¹´åå°å¹´æ«ä»¥åå¤©æ° const currentYear = new Date().getFullYear(); const params = { startDate: `${currentYear}-01-01`, endDate: `${currentYear}-12-31`, days: 365, state: "å¹´", }; // è°ç¨æ¥å£è·åæ°æ® energyConsumptionDetailStatistics(params) .then(res => { if (res.code === 200) { const data = res.data; // å¤çè½èç±»åå æ¯æ°æ® const energyTypeData = data.energyCostDtos || []; // 计ç®åè½èç±»åçæ»æ¶èé let total = 0; const typeMap = { æ°´: 0, çµ: 0, æ°: 0, }; // åå¤è½èç¨éè¶å¿å¾è¡¨æ°æ® const dates = []; const waterConsumptionData = []; const electricityConsumptionData = []; const gasConsumptionData = []; energyTypeData.forEach(item => { // æ¶éæ¥æååè½èç±»åæ°æ® if (item.meterReadingDate) { dates.push(item.meterReadingDate); waterConsumptionData.push(item.waterConsumption || 0); electricityConsumptionData.push(item.electricityConsumption || 0); gasConsumptionData.push(item.gasConsumption || 0); } // è®¡ç®æ»æ¶èé if (item.waterConsumption) typeMap.æ°´ += Number(item.waterConsumption); if (item.electricityConsumption) typeMap.çµ += Number(item.electricityConsumption); if (item.gasConsumption) typeMap.æ° += Number(item.gasConsumption); }); total = typeMap.æ°´ + typeMap.çµ + typeMap.æ°; // æ´æ°è½èç±»åå æ¯å¾è¡¨æ°æ® energyTypeSeries[0].data = [ { value: total > 0 ? ((typeMap.æ°´ / total) * 100).toFixed(2) : 0, name: "æ°´", itemStyle: { color: "#409EFF" }, }, { value: total > 0 ? ((typeMap.çµ / total) * 100).toFixed(2) : 0, name: "çµ", itemStyle: { color: "#E6A23C" }, }, { value: total > 0 ? ((typeMap.æ° / total) * 100).toFixed(2) : 0, name: "æ°", itemStyle: { color: "#F56C6C" }, }, ]; // æ´æ°è½èç¨éè¶å¿å¾è¡¨æ°æ® energyConsumptionXAxis.data = dates; energyConsumptionSeries[0].data = waterConsumptionData; energyConsumptionSeries[1].data = electricityConsumptionData; energyConsumptionSeries[2].data = gasConsumptionData; } }) .catch(err => { console.error("è·åè½èæ°æ®å¼å¸¸ï¼", err); }); }; const planTable = reactive([]); const recentTrendCards = reactive([ @@ -976,18 +1095,19 @@ }; const refreshDashboardData = () => { loadHomeTodos(); loadOrderAndProgress(); loadPlanTrend(); loadQualityData(); loadCostComposition(); loadWarningCenter(); // loadHomeTodos(); // loadOrderAndProgress(); // loadPlanTrend(); // loadQualityData(); // loadCostComposition(); // loadWarningCenter(); updateEnergyTypeChart(); lastUpdatedAt.value = new Date().toLocaleString(); }; onMounted(() => { // initSectionConfig(); // refreshDashboardData(); refreshDashboardData(); }); </script> src/views/productionManagement/processRoute/index.vue
@@ -247,6 +247,7 @@ bomId: row.bomId || null, description: row.description || "", type: "route", status: row.status || false, }, }); }; src/views/productionManagement/processRoute/processRouteItem/index.vue
@@ -47,13 +47,15 @@ class="section-header"> <div class="section-title">å·¥èºè·¯çº¿é¡¹ç®å表</div> <div class="section-actions"> <div class="sort-tip">ææ½è¡¨æ ¼æåº</div> <div v-if="!routeInfo.status" class="sort-tip">ææ½è¡¨æ ¼æåº</div> <el-button icon="Grid" @click="toggleView" style="margin-right: 10px;"> å¡çè§å¾ </el-button> <el-button type="primary" <el-button v-if="!routeInfo.status" type="primary" @click="handleAdd">æ°å¢</el-button> </div> </div> @@ -97,6 +99,7 @@ </template> </el-table-column> <el-table-column label="æä½" v-if="!routeInfo.status" align="center" fixed="right" width="150"> @@ -119,13 +122,15 @@ <div class="section-header"> <div class="section-title">å·¥èºè·¯çº¿é¡¹ç®å表</div> <div class="section-actions"> <div class="sort-tip">é¿æææ½å¡çæåº</div> <div v-if="!routeInfo.status" class="sort-tip">é¿æææ½å¡çæåº</div> <el-button icon="Menu" @click="toggleView" style="margin-right: 10px;"> è¡¨æ ¼è§å¾ </el-button> <el-button type="primary" v-if="!routeInfo.status" @click="handleAdd">æ°å¢</el-button> </div> </div> @@ -150,6 +155,7 @@ <el-button type="primary" link size="small" v-if="!routeInfo.status" @click="handleEdit(item)" :disabled="item.isComplete">ç¼è¾</el-button> <el-button type="info" @@ -159,6 +165,7 @@ <el-button type="danger" link size="small" v-if="!routeInfo.status" @click="handleDelete(item)" :disabled="item.isComplete">å é¤</el-button> </div> @@ -262,7 +269,7 @@ <span v-else>{{ row.unit }}</span> </template> </el-table-column> <el-table-column prop="unitPrice" <el-table-column prop="unitPrice" label="åä»·"> <template #default="{ row }"> <el-form-item v-if="bomDataValue.isEdit" @@ -541,6 +548,7 @@ dictLabel: route.query.dictLabel || "", bomId: route.query.bomId || null, description: route.query.description || "", status: route.query.status === "true" ? true : false, }; if (pageType.value === "order") { queryList2(route.query.orderId) src/views/productionPlan/productionPlan/index.vue
@@ -674,7 +674,10 @@ router.push({ path: "/productionPlan/trackProgress", query: { applyNo: encodeURIComponent(row.applyNo), id: row.id, applyNo: row.applyNo, productName: row.productName, model: row.model, }, }); }; @@ -1559,4 +1562,7 @@ // margin-bottom: 0px !important; // } // } :deep(.el-table .el-table__body-wrapper tr td) { background-color: #fff; } </style> src/views/productionPlan/trackProgress/index.vue
@@ -10,92 +10,118 @@ :model="searchForm" class="search-form"> <el-form-item label="ç³è¯·åç¼å·"> <el-input v-model="searchForm.applyNo" placeholder="请è¾å ¥ç³è¯·åç¼å·" style="width: 400px;"></el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">æç´¢</el-button> <el-select v-model="selectedApplyNo" filterable remote reserve-keyword placeholder="请è¾å ¥ç³è¯·åç¼å·" :loading="applyNoLoading" :remote-method="handleApplyNoSearch" @change="handleSearch" style="width: 400px;"> <el-option v-for="option in applyNoOptions" :key="option.id" :label="option.applyNo+'-'+option.productName+'-'+option.model" :value="option.id" /> </el-select> </el-form-item> </el-form> </div> </template> <!-- åºç¡ä¿¡æ¯ --> <div class="detail-section"> <div v-if="rowData.productionPlanDto" class="detail-section"> <h3 class="section-title">åºç¡ä¿¡æ¯</h3> <el-descriptions :column="3" border> <el-descriptions-item label="ç³è¯·åç¼å·">{{ rowData.applyNo || '-' }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ rowData.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="产åè§æ ¼">{{ rowData.model || '-' }}</el-descriptions-item> <el-descriptions-item label="ç©æç¼ç ">{{ rowData.materialCode || '-' }}</el-descriptions-item> <el-descriptions-item label="ä¸åæ°é">{{ rowData.assignedQuantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="å½åç¶æ"> <el-tag :type="getStatusType(rowData.status)"> {{ getStatusText(rowData.status) }} </el-tag> </el-descriptions-item> </el-descriptions> <el-skeleton :loading="loading" animated> <template #template> <el-skeleton-item variant="p" style="width: 80%" /> <el-skeleton-item variant="p" style="width: 60%" /> <el-skeleton-item variant="p" style="width: 40%" /> </template> <el-descriptions :column="3" border> <el-descriptions-item label="ç³è¯·åç¼å·">{{ rowData.productionPlanDto?.applyNo || '-' }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ rowData.productionPlanDto?.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="产åè§æ ¼">{{ rowData.productionPlanDto?.model || '-' }}</el-descriptions-item> <el-descriptions-item label="ç©æç¼ç ">{{ rowData.productionPlanDto?.materialCode || '-' }}</el-descriptions-item> <el-descriptions-item label="ä¸åæ°é">{{ rowData.productionPlanDto?.assignedQuantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="å½åç¶æ"> <el-tag :type="getStatusType(rowData.productionPlanDto?.status)"> {{ getStatusText(rowData.productionPlanDto?.status) }} </el-tag> </el-descriptions-item> </el-descriptions> </el-skeleton> </div> <div class="progress-container"> <el-empty v-else description="请æç´¢ç³è¯·åç¼å·" /> <div v-if="rowData.orderDtoList" class="progress-container"> <div class="progress-section"> <h3 class="section-title">订åä¿¡æ¯</h3> <div v-for="item in rowData.orderList" :key="item.orderNo" class="order-item"> <el-descriptions :column="3" border> <el-descriptions-item label="订åç¼å·">{{ item.orderNo || '-' }}</el-descriptions-item> <!-- <el-descriptions-item label="订åç¶æ"> <el-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</el-tag> </el-descriptions-item> --> <el-descriptions-item label="å¼å§æ¥æ">{{ item.startTime || '-' }}</el-descriptions-item> <el-descriptions-item label="宿è¿åº¦"> <el-progress :percentage="item.completionRate" :color="customColors(item.completionRate)" :status="item.completionRate === 100 ? 'success' : ''" style="width: 120px;" /> </el-descriptions-item> <el-descriptions-item label="éæ±æ°é">{{ item.quantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="宿æ°é">{{ item.completeQuantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="å©ä½æ°é">{{ item.remainingQuantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> </el-descriptions> <el-table :data="trackProgressForm.progressDetails" border style="width: auto; height: 200px"> <el-table-column prop="step" label="æ¥éª¤ï¼ç¹å»æ¥ç详æ ï¼" align="center"> <template #default="{ row, $index }"> <el-link v-if="$index!=0" @click="handleClickStep(row)" type="primary">{{ row.step }}</el-link> <span v-else>{{ row.step }}</span> </template> </el-table-column> <!-- <el-table-column prop="status" label="ç¶æ" align="center"> <template #default="scope"> <el-tag :type="scope.row.status === 'completed' ? 'success' : scope.row.status === 'processing' ? 'warning' : 'info'"> {{ scope.row.status === 'completed' ? '已宿' : scope.row.status === 'processing' ? 'è¿è¡ä¸' : 'å¾ å¼å§' }} </el-tag> </template> </el-table-column> --> <el-table-column prop="quantity" label="æ°é" align="center" /> <el-table-column prop="startTime" label="æ¶é´" align="center" /> <el-table-column prop="startTime1" label="å²ä½äººå" align="center" /> </el-table> </div> <el-skeleton :loading="loading" animated> <template #template> <el-skeleton-item variant="p" style="width: 80%" /> <el-skeleton-item variant="p" style="width: 60%" /> <el-skeleton-item variant="p" style="width: 40%" /> <el-skeleton-item variant="p" style="width: 100%" /> </template> <div v-for="(item, index) in rowData.orderDtoList" :key="item.productOrderDto?.npsNo || index" class="order-item"> <el-descriptions :column="3" border> <el-descriptions-item label="订åç¼å·">{{ item.productOrderDto?.npsNo || '-' }}</el-descriptions-item> <el-descriptions-item label="å¼å§æ¥æ">{{ item.productOrderDto?.startTime || '-' }}</el-descriptions-item> <el-descriptions-item label="宿è¿åº¦"> <el-progress :percentage="item.productOrderDto?.completionStatus" :color="customColors(item.productOrderDto?.completionStatus)" :status="item.productOrderDto?.completionStatus === 100 ? 'success' : ''" style="width: 120px;" /> </el-descriptions-item> <el-descriptions-item label="éæ±æ°é">{{ item.productOrderDto?.quantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="宿æ°é">{{ item.productOrderDto?.completeQuantity || 0 }} <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="å©ä½æ°é">{{ (item.productOrderDto?.quantity - item.productOrderDto?.completeQuantity) || 0 }} <span class="unit">æ¹</span></el-descriptions-item> </el-descriptions> <el-table :data="item.productionProductMainDtos" border style="width: auto; max-height: 200px"> <el-table-column prop="step" label="æ¥å·¥ï¼ç¹å»æ¥ç详æ ï¼" align="center"> <template #default="{ row }"> <el-link @click="handleClickStep(row)" type="primary">{{ row.productNo }}</el-link> </template> </el-table-column> <el-table-column prop="quantity" label="æ°éï¼æ¹ï¼" align="center" /> <el-table-column prop="reportingTime" label="æ¶é´" align="center" /> <el-table-column prop="schedule" label="çæ¬¡" align="center" /> <el-table-column prop="postName" label="å²ä½äººå" align="center" /> </el-table> </div> </el-skeleton> </div> </div> <el-empty v-else-if="rowData.productionPlanDto" description="ææ è¿åº¦" /> </el-card> <!-- ç产æ¥å·¥è¯¦æ å¼¹çª --> <el-dialog v-model="detailDialogVisible" @@ -104,142 +130,159 @@ :close-on-click-modal="false" custom-class="custom-dialog"> <div class="detail-container"> <!-- åºç¡ä¿¡æ¯ --> <div class="detail-section"> <h3 class="section-title">åºç¡ä¿¡æ¯</h3> <el-descriptions :column="3" border> <el-descriptions-item label="ç产订åå·">{{ detailData.npsNo || '-' }}</el-descriptions-item> <el-descriptions-item label="çç»"><el-tag :type="detailData.schedule == 'ç½ç' ? 'primary' : 'warning'">{{ detailData.schedule || '-' }}</el-tag></el-descriptions-item> <el-descriptions-item label="å²ä½äººå">{{ detailData.postName || '-' }}</el-descriptions-item> <el-descriptions-item label="产åç¼ç ">{{ detailData.materialCode || '-' }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ detailData.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼">{{ detailData.model || '-' }}</el-descriptions-item> <el-descriptions-item label="åæ ¼æ°é"><span class="num2">{{ detailData.qualifiedQuantity || 0 }}</span> <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="ä¸åæ ¼æ°é"><span class="num3">{{ detailData.unqualifiedQuantity || 0 }}</span> <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="æ»æ°é"><span class="num1">{{ detailData.quantity || 0 }}</span> <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="æ¥å·¥æ¶é´">{{ formatTime(detailData.reportingTime) }}</el-descriptions-item> <el-descriptions-item label="å建æ¶é´">{{ formatTime(detailData.createTime) }}</el-descriptions-item> <el-descriptions-item label="æ´æ°æ¶é´">{{ formatTime(detailData.updateTime) }}</el-descriptions-item> </el-descriptions> </div> <!-- å·¥åºä¿¡æ¯ --> <div class="detail-section" v-if="detailData.productionProductRouteItemDtoList && detailData.productionProductRouteItemDtoList.length > 0"> <h3 class="section-title">å·¥åºä¿¡æ¯</h3> <div v-for="(process) in detailData.productionProductRouteItemDtoList" :key="process.id" class="process-item"> <div class="process-header"> <h4 class="process-title">{{ process.processName || '-' }}</h4> <div class="process-info"> <span class="process-label">å²ä½äººåï¼{{ process.postName || '-' }}</span> <span class="process-label">å·¥åºIDï¼{{ process.processNo || '-' }}</span> <el-skeleton :loading="dialogLoading" animated> <template #template> <el-skeleton-item variant="p" style="width: 80%" /> <el-skeleton-item variant="p" style="width: 60%" /> <el-skeleton-item variant="p" style="width: 40%" /> <el-skeleton-item variant="h3" style="width: 50%" /> <el-skeleton-item variant="p" style="width: 100%" /> <el-skeleton-item variant="p" style="width: 100%" /> </template> <!-- åºç¡ä¿¡æ¯ --> <div class="detail-section"> <h3 class="section-title">åºç¡ä¿¡æ¯</h3> <el-descriptions :column="3" border> <el-descriptions-item label="ç产订åå·">{{ detailData.npsNo || '-' }}</el-descriptions-item> <el-descriptions-item label="çç»"><el-tag :type="detailData.schedule == 'ç½ç' ? 'primary' : 'warning'">{{ detailData.schedule || '-' }}</el-tag></el-descriptions-item> <el-descriptions-item label="å²ä½äººå">{{ detailData.postName || '-' }}</el-descriptions-item> <el-descriptions-item label="产åç¼ç ">{{ detailData.materialCode || '-' }}</el-descriptions-item> <el-descriptions-item label="产ååç§°">{{ detailData.productName || '-' }}</el-descriptions-item> <el-descriptions-item label="è§æ ¼">{{ detailData.model || '-' }}</el-descriptions-item> <el-descriptions-item label="åæ ¼æ°é"><span class="num2">{{ detailData.qualifiedQuantity || 0 }}</span> <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="ä¸åæ ¼æ°é"><span class="num3">{{ detailData.unqualifiedQuantity || 0 }}</span> <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="æ»æ°é"><span class="num1">{{ detailData.quantity || 0 }}</span> <span class="unit">æ¹</span></el-descriptions-item> <el-descriptions-item label="æ¥å·¥æ¶é´">{{ formatTime(detailData.reportingTime) }}</el-descriptions-item> <el-descriptions-item label="å建æ¶é´">{{ formatTime(detailData.createTime) }}</el-descriptions-item> <el-descriptions-item label="æ´æ°æ¶é´">{{ formatTime(detailData.updateTime) }}</el-descriptions-item> </el-descriptions> </div> <!-- å·¥åºä¿¡æ¯ --> <div class="detail-section" v-if="detailData.productionProductRouteItemDtoList && detailData.productionProductRouteItemDtoList.length > 0"> <h3 class="section-title">å·¥åºä¿¡æ¯</h3> <div v-for="(process) in detailData.productionProductRouteItemDtoList" :key="process.id" class="process-item"> <div class="process-header"> <h4 class="process-title">{{ process.processName || '-' }}</h4> <div class="process-info"> <span class="process-label">å²ä½äººåï¼{{ process.postName || '-' }}</span> <span class="process-label">å·¥åºIDï¼{{ process.processNo || '-' }}</span> </div> </div> </div> <!-- å·¥åºåºæ¬ä¿¡æ¯ --> <div class="process-details"> <el-descriptions :column="2" border> <el-descriptions-item label="设å¤å¼å¸¸æ åµ">{{ process.equipmentMalfunction || '-' }}</el-descriptions-item> <el-descriptions-item label="å½ç设å¤å¤ç½®">{{ process.equipmentDisposal || '-' }}</el-descriptions-item> <el-descriptions-item label="å·¥èºäººåäº¤å¾ " :span="2">{{ process.processExplained || '-' }}</el-descriptions-item> </el-descriptions> </div> <!-- å·¥åºåæ° --> <div v-if="process.productionProductRouteItemParamDtoList && process.productionProductRouteItemParamDtoList.length > 0"> <!-- BOMä¿¡æ¯ --> <div class="param-section" v-if="getBomList(process.productionProductRouteItemParamDtoList).length > 0"> <h5 class="param-title">æå ¥åä¿¡æ¯</h5> <el-table :data="getBomList(process.productionProductRouteItemParamDtoList)" style="width: 100%" size="small"> <el-table-column prop="paramName" label="产ååç§°" min-width="120"></el-table-column> <el-table-column prop="model" label="è§æ ¼åå·" min-width="120"></el-table-column> <el-table-column prop="productValue" label="æå ¥é" min-width="100"></el-table-column> <el-table-column prop="unit" label="åä½" width="80"></el-table-column> </el-table> <!-- å·¥åºåºæ¬ä¿¡æ¯ --> <div class="process-details"> <el-descriptions :column="2" border> <el-descriptions-item label="设å¤å¼å¸¸æ åµ">{{ process.equipmentMalfunction || '-' }}</el-descriptions-item> <el-descriptions-item label="å½ç设å¤å¤ç½®">{{ process.equipmentDisposal || '-' }}</el-descriptions-item> <el-descriptions-item label="å·¥èºäººåäº¤å¾ " :span="2">{{ process.processExplained || '-' }}</el-descriptions-item> </el-descriptions> </div> <!-- åæ°ä¿¡æ¯ --> <div class="param-section" v-if="getParamList(process.productionProductRouteItemParamDtoList).length > 0"> <h5 class="param-title">ç产记å½</h5> <el-card v-for="group in getParamGroups(process.productionProductRouteItemParamDtoList)" :key="group.sourceSort" class="detail-card" style="margin-top: 10px;"> <template #header> <div class="card-header"> <span v-if="Object.keys(getParamGroups(process.productionProductRouteItemParamDtoList)).length > 1">ç产记å½ç» - {{ group.sourceSort }}</span> <span v-else>ç产记å½</span> </div> </template> <el-table :data="group.items" <!-- å·¥åºåæ° --> <div v-if="process.productionProductRouteItemParamDtoList && process.productionProductRouteItemParamDtoList.length > 0"> <!-- BOMä¿¡æ¯ --> <div class="param-section" v-if="getBomList(process.productionProductRouteItemParamDtoList).length > 0"> <h5 class="param-title">æå ¥åä¿¡æ¯</h5> <el-table :data="getBomList(process.productionProductRouteItemParamDtoList)" style="width: 100%" :row-class-name="rowClassName"> size="small"> <el-table-column prop="paramName" label="ææ " /> label="产ååç§°" min-width="120"></el-table-column> <el-table-column prop="model" label="è§æ ¼åå·" min-width="120"></el-table-column> <el-table-column prop="productValue" label="æå ¥é" min-width="100"></el-table-column> <el-table-column prop="unit" label="åä½" width="100"> <template #default="scope"> {{ scope.row.unit || "/" }} </template> </el-table-column> <el-table-column prop="standardText" label="æ åå¼" /> <el-table-column prop="paramValue" label="å®é å¼" /> <el-table-column prop="result" label="ç»æ" width="100"> <template #default="scope"> <el-tag :type="scope.row.result === 'åæ ¼' ? 'success' : 'danger'"> {{ scope.row.result }} </el-tag> </template> </el-table-column> width="80"></el-table-column> </el-table> </el-card> </div> <!-- åæ°ä¿¡æ¯ --> <div class="param-section" v-if="getParamList(process.productionProductRouteItemParamDtoList).length > 0"> <h5 class="param-title">ç产记å½</h5> <el-card v-for="group in getParamGroups(process.productionProductRouteItemParamDtoList)" :key="group.sourceSort" class="detail-card" style="margin-top: 10px;"> <template #header> <div class="card-header"> <span v-if="Object.keys(getParamGroups(process.productionProductRouteItemParamDtoList)).length > 1">ç产记å½ç» - {{ group.sourceSort }}</span> <span v-else>ç产记å½</span> </div> </template> <el-table :data="group.items" style="width: 100%" :row-class-name="rowClassName"> <el-table-column prop="paramName" label="ææ " /> <el-table-column prop="unit" label="åä½" width="100"> <template #default="scope"> {{ scope.row.unit || "/" }} </template> </el-table-column> <el-table-column prop="standardText" label="æ åå¼" /> <el-table-column prop="paramValue" label="å®é å¼" /> <el-table-column prop="result" label="ç»æ" width="100"> <template #default="scope"> <el-tag :type="scope.row.result === 'åæ ¼' ? 'success' : 'danger'"> {{ scope.row.result }} </el-tag> </template> </el-table-column> </el-table> </el-card> </div> </div> </div> <!-- ä¸ä¼ æä»¶ --> <div class="file-section" v-if="process.fileList && process.fileList.length > 0"> <h5 class="file-title">ä¸ä¼ æä»¶</h5> <div class="file-grid"> <div v-for="file in process.fileList" :key="file.id" class="file-item"> <el-image style="width: 100px; height: 100px" v-if="file.fileUrl" :src="baseUrl + file.fileUrl" :zoom-rate="1.2" :max-scale="7" :alt="file.fileName" :min-scale="0.2" :preview-src-list="formatFileList(process.fileList)" show-progress :initial-index="4" fit="cover" /> <div class="file-info"> <span class="file-name">{{ file.fileName || '-' }}</span> <!-- ä¸ä¼ æä»¶ --> <div class="file-section" v-if="process.fileList && process.fileList.length > 0"> <h5 class="file-title">ä¸ä¼ æä»¶</h5> <div class="file-grid"> <div v-for="file in process.fileList" :key="file.id" class="file-item"> <el-image style="width: 100px; height: 100px" v-if="file.fileUrl" :src="baseUrl + file.fileUrl" :zoom-rate="1.2" :max-scale="7" :alt="file.fileName" :min-scale="0.2" :preview-src-list="formatFileList(process.fileList)" show-progress :initial-index="4" fit="cover" /> <div class="file-info"> <span class="file-name">{{ file.fileName || '-' }}</span> </div> </div> </div> </div> </div> </div> </div> </el-skeleton> </div> <template #footer> <div class="dialog-footer"> @@ -255,6 +298,11 @@ import { ElMessage } from "element-plus"; import { useRouter, useRoute } from "vue-router"; import dayjs from "dayjs"; import { trackProgressByNo, productionPlanListPage, } from "@/api/productionPlan/productionPlan"; import { productionReportDetail } from "@/api/productionManagement/productionReporting.js"; const router = useRouter(); const route = useRoute(); @@ -271,15 +319,21 @@ remark: "", }); // æç´¢è¡¨å const searchForm = reactive({ applyNo: "", }); // ç产æ¥å·¥è¯¦æ å¼¹çª const detailDialogVisible = ref(false); const detailData = ref({}); const baseUrl = import.meta.env.VITE_APP_BASE_API; // å è½½ç¶æ const loading = ref(false); // å¼¹çªå è½½ç¶æ const dialogLoading = ref(false); // ç³è¯·åä¸ææ¡æ°æ® const applyNoOptions = ref([]); const applyNoLoading = ref(false); const applyNoQuery = ref(""); const selectedApplyNo = ref(null); // è·åç¶æç±»å const getStatusType = status => { @@ -392,28 +446,52 @@ router.push("/productionPlan/productionPlan"); }; // å¤çç³è¯·åç¼å·æç´¢ const handleApplyNoSearch = query => { if (query) { applyNoLoading.value = true; productionPlanListPage({ current: -1, size: -1, applyNo: query, }) .then(res => { // è½¬æ¢æ°æ®æ ¼å¼ä¸ºä¸ææ¡æéçæ ¼å¼ applyNoOptions.value = res.data.records; }) .catch(error => { console.error(error); }) .finally(() => { applyNoLoading.value = false; }); } else { applyNoOptions.value = []; } }; // å¤çæç´¢ const handleSearch = () => { const applyNo = searchForm.applyNo.trim(); if (!applyNo) { ElMessage.warning("请è¾å ¥ç³è¯·åç¼å·"); if (!selectedApplyNo.value) { ElMessage.warning("è¯·éæ©ç³è¯·åç¼å·"); return; } // è¿éå¯ä»¥æ·»å æç´¢é»è¾ï¼ä¾å¦è°ç¨APIè·åæ°æ® // ç®åä½¿ç¨æ¨¡ææ°æ® ElMessage.success(`æç´¢ç³è¯·åç¼å·: ${applyNo}`); // 模ææç´¢ç»æ rowData.applyNo = applyNo; rowData.productName = "æç´¢ç»æäº§å"; rowData.model = "æç´¢ç»æè§æ ¼"; rowData.materialCode = "MAT-" + applyNo; rowData.assignedQuantity = 100; rowData.status = 1; trackProgressForm.progressDetails = generateProgressDetails(1); trackProgressForm.completionRate = calculateCompletionRate( trackProgressForm.progressDetails ); rowData.orderList = generateOrderList(); // è°ç¨APIè·åæ°æ® loading.value = true; trackProgressByNo({ productionPlanId: selectedApplyNo.value }) .then(res => { console.log(res, "æç´¢ç»æ"); // åå¹¶æ°æ®å°rowData Object.assign(rowData, res.data); ElMessage.success("æç´¢æå"); }) .catch(error => { ElMessage.error("æç´¢å¤±è´¥ï¼è¯·ç¨åéè¯"); console.error(error); }) .finally(() => { loading.value = false; }); }; // çææ¨¡æè®¢åæ°æ® @@ -451,97 +529,23 @@ // å¤çç¹å»æ¥éª¤æ¥ç详æ const handleClickStep = row => { // è¿éå¯ä»¥æ·»å è·åæ¥å·¥è¯¦æ æ°æ®çé»è¾ // ç®åä½¿ç¨æ¨¡ææ°æ® detailData.value = { npsNo: "NPS-2026-001", schedule: "ç½ç", postName: "å¼ ä¸", materialCode: rowData.materialCode || "MAT-001", productName: rowData.productName || "产åA", model: rowData.model || "è§æ ¼A", qualifiedQuantity: 100, unqualifiedQuantity: 5, quantity: 105, reportingTime: new Date(), createTime: new Date(), updateTime: new Date(), productionProductRouteItemDtoList: [ { id: 1, processName: "å·¥åº1", postName: "å¼ ä¸", processNo: "PROC-001", equipmentMalfunction: "æ å¼å¸¸", equipmentDisposal: "æ£å¸¸è¿è¡", processExplained: "æç §æ åå·¥èºæä½", productionProductRouteItemParamDtoList: [ { id: 11, paramName: "åææA", model: "åå·A", productValue: "100", unit: "kg", bomId: 101, }, { id: 12, paramName: "温度", paramValue: "25", unit: "°C", sourceSort: 1, valueMode: 2, minValue: 20, maxValue: 30, }, { id: 13, paramName: "åå", paramValue: "1.5", unit: "MPa", sourceSort: 1, valueMode: 2, minValue: 1.0, maxValue: 2.0, }, { id: 14, paramName: "转é", paramValue: "1500", unit: "rpm", sourceSort: 2, valueMode: 1, standardValue: "1500", }, { id: 15, paramName: "çµæµ", paramValue: "12", unit: "A", sourceSort: 2, valueMode: 2, minValue: 10, maxValue: 15, }, ], fileList: [ { id: 21, fileName: "ç产记å½1.jpg", fileUrl: "/upload/files/20260301/12345.jpg", fileSize: 1024000, }, { id: 22, fileName: "ç产记å½2.jpg", fileUrl: "/upload/files/20260301/67890.jpg", fileSize: 2048000, }, ], }, ], }; detailDialogVisible.value = true; // è·åæ¥å·¥è¯¦æ æ°æ® dialogLoading.value = true; productionReportDetail(row.id) .then(res => { console.log(res, "æ¥å·¥è¯¦æ "); // å°APIè¿åçæ°æ®èµå¼ç»detailData detailData.value = res.data; // æå¼å¼¹çª detailDialogVisible.value = true; }) .catch(error => { ElMessage.error("è·åæ¥å·¥è¯¦æ 失败ï¼è¯·ç¨åéè¯"); console.error(error); }) .finally(() => { dialogLoading.value = false; }); }; // æ ¼å¼åæ¶é´ @@ -637,29 +641,26 @@ // 页é¢å è½½æ¶è·åæ°æ® onMounted(() => { // ä»è·¯ç±åæ°ä¸è·åæ°æ® applyNo.value = route.query.applyNo ? decodeURIComponent(route.query.applyNo) selectedApplyNo.value = route.query.applyNo ? route.query.applyNo + "-" + route.query.productName + "-" + route.query.model : null; searchForm.applyNo = applyNo.value; // çæåæ°æ® rowData.applyNo = applyNo.value || "APPLY-2026-001"; rowData.productName = "æµè¯äº§å"; rowData.model = "æµè¯è§æ ¼"; rowData.materialCode = "MAT-001"; rowData.assignedQuantity = 233; rowData.status = 1; // èµå¼ç»è¡¨åæ°æ® trackProgressForm.materialCode = rowData.materialCode; trackProgressForm.currentStatus = rowData.status; trackProgressForm.progressDetails = generateProgressDetails(rowData.status); trackProgressForm.completionRate = calculateCompletionRate( trackProgressForm.progressDetails ); trackProgressForm.remark = ""; // çææ¨¡æè®¢åæ°æ® rowData.orderList = generateOrderList(); if (route.query.id) { loading.value = true; trackProgressByNo({ productionPlanId: route.query.id }) .then(res => { console.log(res, "追踪è¿åº¦"); // åå¹¶æ°æ®å°rowData Object.assign(rowData, res.data); }) .finally(() => { loading.value = false; }); } }); </script> src/views/reportAnalysis/salesStatistics/index.vue
@@ -6,7 +6,7 @@ <div class="bi-topbar"> <img class="bi-topbar-title-bg" src="@/assets/BI/biaoti.png" alt="éå®çæ¿ç»è®¡" /> alt="éå®ç»è®¡çæ¿" /> <div class="bi-topbar-content"> <div class="bi-topbar-left"> <button class="fullscreen-btn" @@ -35,7 +35,7 @@ <span>26â</span> <span class="bi-topbar-sep">湿度ï¼1</span> --> </div> <div class="bi-topbar-title">éå®çæ¿ç»è®¡</div> <div class="bi-topbar-title">éå®ç»è®¡çæ¿</div> <div class="bi-topbar-meta"> <span class="bi-topbar-time">{{ currentTime }}</span> <span class="bi-topbar-sep">|</span> @@ -50,19 +50,19 @@ title="ééåæè¶å¿å¾" /> <div class="panel-tabs"> <span class="tab-item" :class="{ active: blockTimeDimension === 'year' }" @click="handleBlockTimeDimensionChange('year')">å¹´</span> :class="{ active: chartTimeDimension === 'å¹´' }" @click="handleChartTimeDimensionChange('å¹´')">å¹´</span> <span class="tab-item" :class="{ active: blockTimeDimension === 'month' }" @click="handleBlockTimeDimensionChange('month')">æ</span> :class="{ active: chartTimeDimension === 'æ' }" @click="handleChartTimeDimensionChange('æ')">æ</span> </div> <div class="panel-tabs2"> <span class="tab-item" :class="{ active: blockProductType === 'ç å' }" @click="handleBlockProductTypeChange('ç å')">ç å</span> :class="{ active: chartProductType === 'ç å' }" @click="handleChartProductTypeChange('ç å')">ç å</span> <span class="tab-item" :class="{ active: blockProductType === 'æ¿æ' }" @click="handleBlockProductTypeChange('æ¿æ')">æ¿æ</span> :class="{ active: chartProductType === 'æ¿æ' }" @click="handleChartProductTypeChange('æ¿æ')">æ¿æ</span> </div> <div class="bi-panel-body"> <div class="chart-unit-row"> @@ -78,19 +78,19 @@ title="éå®éé¢åæ" /> <div class="panel-tabs"> <span class="tab-item" :class="{ active: boardTimeDimension === 'year' }" @click="handleBoardTimeDimensionChange('year')">å¹´</span> :class="{ active: chartTimeDimension2 === 'å¹´' }" @click="handleChartTimeDimensionChange2('å¹´')">å¹´</span> <span class="tab-item" :class="{ active: boardTimeDimension === 'month' }" @click="handleBoardTimeDimensionChange('month')">æ</span> :class="{ active: chartTimeDimension2 === 'æ' }" @click="handleChartTimeDimensionChange2('æ')">æ</span> </div> <div class="panel-tabs2"> <span class="tab-item" :class="{ active: boardProductType === 'ç å' }" @click="handleBoardProductTypeChange('ç å')">ç å</span> :class="{ active: chartProductType2 === 'ç å' }" @click="handleChartProductTypeChange2('ç å')">ç å</span> <span class="tab-item" :class="{ active: boardProductType === 'æ¿æ' }" @click="handleBoardProductTypeChange('æ¿æ')">æ¿æ</span> :class="{ active: chartProductType2 === 'æ¿æ' }" @click="handleChartProductTypeChange2('æ¿æ')">æ¿æ</span> </div> <div class="bi-panel-body"> <div class="chart-unit-row"> @@ -131,27 +131,27 @@ title="ééæ°æ®ç»è®¡" /> <div class="panel-tabs"> <span class="tab-item" :class="{ active: blockTimeDimension === 'year' }" @click="handleBlockTimeDimensionChange('year')">å¹´</span> :class="{ active: tableTimeDimension === 'å¹´' }" @click="handleTableTimeDimensionChange('å¹´')">å¹´</span> <span class="tab-item" :class="{ active: blockTimeDimension === 'month' }" @click="handleBlockTimeDimensionChange('month')">æ</span> :class="{ active: tableTimeDimension === 'æ' }" @click="handleTableTimeDimensionChange('æ')">æ</span> </div> <div class="panel-tabs2"> <span class="tab-item" :class="{ active: blockProductType === 'ç å' }" @click="handleBlockProductTypeChange('ç å')">ç å</span> :class="{ active: tableProductType === 'ç å' }" @click="handleTableProductTypeChange('ç å')">ç å</span> <span class="tab-item" :class="{ active: blockProductType === 'æ¿æ' }" @click="handleBlockProductTypeChange('æ¿æ')">æ¿æ</span> :class="{ active: tableProductType === 'æ¿æ' }" @click="handleTableProductTypeChange('æ¿æ')">æ¿æ</span> </div> <div class="bi-panel-body"> <div class="chart-filter-tabs"> <span v-for="area in salesAreas" <span v-for="area in tableSalesAreas" :key="area" class="cf-tab" :class="{ active: blockSelectedArea === area }" @click="handleBlockAreaChange(area)">{{ area }}</span> :class="{ active: tableSelectedArea === area }" @click="handleTableAreaChange(area)">{{ area }}</span> </div> <div class="scroll-table-container"> <table class="scroll-table"> @@ -166,10 +166,10 @@ </thead> <div class="scroll-table-content"> <tbody ref="blockTableBody"> <tr :class="item.sort % 2 === 0 ? 'evenTableTr' : 'oddTableTr'" v-for="(item, index) in blockSalesData" <tr :class="(index + 1) % 2 === 0 ? 'evenTableTr' : 'oddTableTr'" v-for="(item, index) in filteredTableSalesData" :key="item.period + item.area + index"> <td>{{ item.sort }}</td> <td>{{ index + 1 }}</td> <td>{{ item.productType }}</td> <td>{{ item.period }}</td> <td>{{ item.area }}</td> @@ -181,7 +181,7 @@ </div> <div class="panel-summary-row"> <div class="summary-label">å计</div> <div class="summary-value">127384 m³</div> <div class="summary-value">{{ filteredTableSalesTotal }} m³</div> </div> </div> </div> @@ -211,44 +211,46 @@ title="éå®é¢æ°æ®ç»è®¡" /> <div class="panel-tabs"> <span class="tab-item" :class="{ active: boardTimeDimension === 'year' }" @click="handleBoardTimeDimensionChange('year')">å¹´</span> :class="{ active: tableTimeDimension2 === 'å¹´' }" @click="handleTableTimeDimensionChange2('å¹´')">å¹´</span> <span class="tab-item" :class="{ active: boardTimeDimension === 'month' }" @click="handleBoardTimeDimensionChange('month')">æ</span> :class="{ active: tableTimeDimension2 === 'æ' }" @click="handleTableTimeDimensionChange2('æ')">æ</span> </div> <div class="panel-tabs2"> <span class="tab-item" :class="{ active: boardProductType === 'ç å' }" @click="handleBoardProductTypeChange('ç å')">ç å</span> :class="{ active: tableProductType2 === 'ç å' }" @click="handleTableProductTypeChange2('ç å')">ç å</span> <span class="tab-item" :class="{ active: boardProductType === 'æ¿æ' }" @click="handleBoardProductTypeChange('æ¿æ')">æ¿æ</span> :class="{ active: tableProductType2 === 'æ¿æ' }" @click="handleTableProductTypeChange2('æ¿æ')">æ¿æ</span> </div> <div class="bi-panel-body"> <div class="chart-filter-tabs"> <span v-for="area in salesAreas" <span v-for="area in tableSalesAreas" :key="area" class="cf-tab" :class="{ active: boardSelectedArea === area }" @click="handleBoardAreaChange(area)">{{ area }}</span> :class="{ active: tableSelectedArea === area }" @click="handleTableAreaChange2(area)">{{ area }}</span> </div> <div class="scroll-table-container"> <table class="scroll-table"> <thead> <tr> <th>åºå·</th> <th>产åç±»å</th> <th>å¹´æ</th> <th>éå®åº</th> <th>éå®é¢ï¼ä¸å ï¼</th> <th>éå®é¢ï¼å ï¼</th> </tr> </thead> <div class="scroll-table-content"> <tbody ref="boardTableBody"> <tr :class="item.sort % 2 === 0 ? 'evenTableTr' : 'oddTableTr'" v-for="(item, index) in boardSalesData" <tr :class="(index + 1) % 2 === 0 ? 'evenTableTr' : 'oddTableTr'" v-for="(item, index) in filteredAmountSalesData" :key="item.period + item.area + index"> <td>{{ item.sort }}</td> <td>{{ index + 1 }}</td> <td>{{ item.productType }}</td> <td>{{ item.period }}</td> <td>{{ item.area }}</td> <td>{{ item.sales }}</td> @@ -259,7 +261,7 @@ </div> <div class="panel-summary-row"> <div class="summary-label">å计</div> <div class="summary-value2">127384 ä¸å </div> <div class="summary-value2">{{ filteredAmountSalesTotal }} å </div> </div> </div> </div> @@ -283,6 +285,8 @@ import { getDashboardStatistics, getCustomerTrends, getSalesAnalysisTrend, getSalesAmountAnalysis, } from "@/api/reportAnalysis/salesStatistics"; const router = useRouter(); @@ -343,10 +347,21 @@ const boardTableBody = ref(null); // 鿩卿°æ® const blockTimeDimension = ref("year"); const blockSelectedArea = ref("å ¨é¨"); const blockProductType = ref("ç å"); const boardTimeDimension = ref("year"); // ééåæè¶å¿å¾ const chartTimeDimension = ref("å¹´"); const chartTimeDimension2 = ref("å¹´"); const chartSelectedArea = ref("å ¨é¨"); const chartProductType = ref("ç å"); const chartProductType2 = ref("ç å"); // ééæ°æ®ç»è®¡ const tableTimeDimension = ref("å¹´"); const tableSelectedArea = ref("å ¨é¨"); const tableSelectedArea2 = ref("å ¨é¨"); const tableProductType = ref("ç å"); const boardTimeDimension = ref("å¹´"); const boardSelectedArea = ref("å ¨é¨"); const boardProductType = ref("æ¿æ"); const customerTimeDimension = ref("å¹´"); @@ -675,6 +690,14 @@ // 客æ·è¶å¿æ°æ® const customerTrendsData = ref([]); // ééåæè¶å¿æ°æ® const salesAnalysisTrendData = ref([]); // ééæ°æ®ç»è®¡è¡¨æ ¼æ°æ® const tableSalesData = ref([]); // ééæ°æ®ç»è®¡è¡¨æ ¼æ»è®¡ const tableSalesTotal = ref(0); // 卿éå®åºåå表 const tableSalesAreas = ref([]); // ååç计ç®ï¼æ¨¡æï¼ const salesVolumeChange = ref("+5.2"); @@ -717,6 +740,126 @@ } }; // è·åééåæè¶å¿æ°æ® const fetchSalesAnalysisTrendData = async () => { try { const response = await getSalesAnalysisTrend({ type: chartProductType.value, // ç åææ¿æ days: chartTimeDimension.value, // å¹´ææ }); if (response && response.data) { // APIè¿åçæ°æ®ç»æå¦ä¸ï¼ // { // "dates": ["2026-01-01", "2025-01-01", ...], // "customerTrends": [{"å èå¤": 470, "é¶å·": 3600, ...}, ...] // } salesAnalysisTrendData.value = response.data; updateCharts(); } } catch (error) { console.error("è·åééåæè¶å¿æ°æ®å¤±è´¥:", error); } }; // è·åééæ°æ®ç»è®¡è¡¨æ ¼æ°æ® const fetchTableSalesData = async () => { try { const response = await getSalesAnalysisTrend({ type: tableProductType.value, // ç åææ¿æ days: tableTimeDimension.value, // å¹´ææ }); if (response && response.data) { // APIè¿åçæ°æ®ç»æå¦ä¸ï¼ // { // "dates": ["2026-01-01", "2025-01-01", ...], // "customerTrends": [{"å èå¤": 470, "é¶å·": 3600, ...}, ...] // } updateTableSalesData(response.data); } } catch (error) { console.error("è·åééæ°æ®ç»è®¡è¡¨æ ¼æ°æ®å¤±è´¥:", error); } }; // æ´æ°ééæ°æ®ç»è®¡è¡¨æ ¼ const updateTableSalesData = data => { if (!data || !data.dates || !data.customerTrends) { return; } console.log(data, "datas"); const dates = data.dates; const customerTrends = data.customerTrends; const tableData = []; let total = 0; const areaSet = new Set(); dates.forEach((date, index) => { const trend = customerTrends[index]; if (trend) { // æåææéå®åºå Object.keys(trend).forEach(area => { if (area !== "å ¨é¨") { areaSet.add(area); const sales = trend[area] || 0; tableData.push({ period: date, area: area, productType: tableProductType.value, sales: sales, sort: tableData.length + 1, }); total += sales; } }); } }); // æ´æ°éå®åºååè¡¨ï¼æ·»å "å ¨é¨"é项 tableSalesAreas.value = ["å ¨é¨", ...Array.from(areaSet)]; // ç¡®ä¿ tableSelectedArea å¨éå®åºååè¡¨ä¸ if ( tableSalesAreas.value.length > 0 && !tableSalesAreas.value.includes(tableSelectedArea.value) ) { tableSelectedArea.value = "å ¨é¨"; } console.log(tableData); tableSalesData.value = tableData; tableSalesTotal.value = total; }; // å¤çå¾è¡¨æ¶é´ç»´åº¦åå const handleChartTimeDimensionChange = dimension => { chartTimeDimension.value = dimension; fetchSalesAnalysisTrendData(); }; // å¤çå¾è¡¨äº§åç±»ååå const handleChartProductTypeChange = type => { chartProductType.value = type; fetchSalesAnalysisTrendData(); }; // å¤çè¡¨æ ¼æ¶é´ç»´åº¦åå const handleTableTimeDimensionChange = dimension => { tableTimeDimension.value = dimension; fetchTableSalesData(); // éæ°å¯å¨æ»å¨ï¼æ ¹æ®æ¶é´ç»´åº¦å³å®æ¯å¦æ»å¨ startBlockTableScroll(); }; // å¤çè¡¨æ ¼äº§åç±»ååå const handleTableProductTypeChange = type => { tableProductType.value = type; fetchTableSalesData(); }; // å¤çè¡¨æ ¼éå®åºååå const handleTableAreaChange = area => { tableSelectedArea.value = area; }; // è¡¨æ ¼æ°æ® const tableData = computed(() => { return filteredData.value.map(item => { @@ -734,17 +877,45 @@ }); }); // çéåçè¡¨æ ¼æ°æ® const filteredTableSalesData = computed(() => { if (tableSelectedArea.value === "å ¨é¨") { // æå¹´æåç»æ±æ»æ°æ® const groupedData = {}; tableSalesData.value.forEach(item => { const key = item.period; if (!groupedData[key]) { groupedData[key] = { period: item.period, area: "å ¨é¨", productType: item.productType, sales: 0, sort: 0, }; } groupedData[key].sales += item.sales; }); // 转æ¢ä¸ºæ°ç»å¹¶æå¹´ææåº return Object.values(groupedData).sort((a, b) => { return new Date(b.period) - new Date(a.period); }); } else { return tableSalesData.value.filter( item => item.area === tableSelectedArea.value ); } }); // çéåçè¡¨æ ¼æ°æ®æ»è®¡ const filteredTableSalesTotal = computed(() => { return filteredTableSalesData.value.reduce( (total, item) => total + item.sales, 0 ); }); // ééè¶å¿å¾è¡¨é ç½® const salesVolumeChartOption = computed(() => { // 为æ¯ä¸ªéå®åºçææ°æ® const salesAreas = [ "å ¨é¨", "Aéå®åº", "Béå®åº", "Céå®åº", "Déå®åº", "Eéå®åº", ]; const colors = [ "#00A4ED", "#34D8F7", @@ -752,54 +923,111 @@ "#8A6BFF", "#C8C447", "#FF6B6B", "#FF9500", "#4CD964", "#5AC8FA", ]; const year = 2024; const periodType = blockTimeDimension.value; // çææ¶é´æ®µ let periods = []; if (periodType === "year") { // å¹´åº¦æ°æ®ï¼12个æ for (let month = 1; month <= 12; month++) { periods.push(`${year}-${month.toString().padStart(2, "0")}`); let salesAreas = []; let series = []; if ( salesAnalysisTrendData.value && salesAnalysisTrendData.value.dates && salesAnalysisTrendData.value.customerTrends ) { // 使ç¨APIè¿åçæ¥æ periods = salesAnalysisTrendData.value.dates; // æåéå®åºå const customerTrends = salesAnalysisTrendData.value.customerTrends; if (customerTrends.length > 0) { // æåéå®åºåå¹¶ç¡®ä¿"å ¨é¨"å¨ç¬¬ä¸ä¸ªä½ç½® const allAreas = Object.keys(customerTrends[0]); salesAreas = allAreas.sort((a, b) => { if (a === "å ¨é¨") return -1; if (b === "å ¨é¨") return 1; return 0; }); // 为æ¯ä¸ªéå®åºçææ°æ® series = salesAreas.map((area, index) => { const data = customerTrends.map(trend => trend[area] || 0); return { name: area, data: data, type: "line", smooth: false, // symbolSize: getResponsiveValue(8), lineStyle: { width: getResponsiveValue(1), color: colors[index % colors.length], }, itemStyle: { color: colors[index % colors.length] }, areaStyle: { opacity: 0.4, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: colors[index % colors.length] + "80" }, { offset: 1, color: colors[index % colors.length] + "00" }, ]), }, }; }); } } else { // æåº¦æ°æ®ï¼30天 const month = 1; for (let day = 1; day <= 30; day++) { periods.push( `${year}-${month.toString().padStart(2, "0")}-${day .toString() .padStart(2, "0")}` ); // æ¨¡ææ°æ® salesAreas = [ "å ¨é¨", "Aéå®åº", "Béå®åº", "Céå®åº", "Déå®åº", "Eéå®åº", ]; const year = 2024; if (periodType === "year") { // å¹´åº¦æ°æ®ï¼12个æ for (let month = 1; month <= 12; month++) { periods.push(`${year}-${month.toString().padStart(2, "0")}`); } } else { // æåº¦æ°æ®ï¼30天 const month = 1; for (let day = 1; day <= 30; day++) { periods.push( `${year}-${month.toString().padStart(2, "0")}-${day .toString() .padStart(2, "0")}` ); } } } // 为æ¯ä¸ªéå®åºçææ°æ® series = salesAreas.map((area, index) => { const data = periods.map(() => { return periodType === "year" ? Math.floor(Math.random() * 500) + 800 : Math.floor(Math.random() * 50) + 20; }); // 为æ¯ä¸ªéå®åºçææ°æ® const series = salesAreas.map((area, index) => { const data = periods.map(() => { return periodType === "year" ? Math.floor(Math.random() * 500) + 800 : Math.floor(Math.random() * 50) + 20; return { name: area, data: data, type: "line", smooth: false, // symbolSize: getResponsiveValue(8), lineStyle: { width: getResponsiveValue(1), color: colors[index] }, itemStyle: { color: colors[index] }, areaStyle: { opacity: 0.4, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: colors[index] + "80" }, { offset: 1, color: colors[index] + "00" }, ]), }, }; }); return { name: area, data: data, type: "line", smooth: false, // symbolSize: getResponsiveValue(8), lineStyle: { width: getResponsiveValue(1), color: colors[index] }, itemStyle: { color: colors[index] }, areaStyle: { opacity: 0.4, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: colors[index] + "80" }, { offset: 1, color: colors[index] + "00" }, ]), }, }; }); } return { backgroundColor: "transparent", @@ -864,15 +1092,20 @@ // éå®éé¢è¶å¿å¾è¡¨é ç½® const salesAmountChartOption = computed(() => { // 为æ¯ä¸ªéå®åºçææ°æ® const salesAreas = [ "å ¨é¨", "Aéå®åº", "Béå®åº", "Céå®åº", "Déå®åº", "Eéå®åº", ]; const { dates = [], customerTrends = [] } = salesAmountChartData.value; // æåææéå®åºå const areaSet = new Set(); customerTrends.forEach(item => { Object.keys(item).forEach(key => areaSet.add(key)); }); // ç¡®ä¿"å ¨é¨"å¨ç¬¬ä¸ä¸ªä½ç½® const salesAreas = Array.from(areaSet).sort((a, b) => { if (a === "å ¨é¨") return -1; if (b === "å ¨é¨") return 1; return 0; }); const colors = [ "#00A4ED", "#34D8F7", @@ -881,48 +1114,26 @@ "#C8C447", "#FF6B6B", ]; const year = 2024; const periodType = boardTimeDimension.value; // çææ¶é´æ®µ let periods = []; if (periodType === "year") { // å¹´åº¦æ°æ®ï¼12个æ for (let month = 1; month <= 12; month++) { periods.push(`${year}-${month.toString().padStart(2, "0")}`); } } else { // æåº¦æ°æ®ï¼30天 const month = 1; for (let day = 1; day <= 30; day++) { periods.push( `${year}-${month.toString().padStart(2, "0")}-${day .toString() .padStart(2, "0")}` ); } } // 为æ¯ä¸ªéå®åºçææ°æ® const series = salesAreas.map((area, index) => { const data = periods.map(() => { return periodType === "year" ? Math.floor(Math.random() * 50000) + 80000 : Math.floor(Math.random() * 5000) + 2000; }); const data = customerTrends.map(item => item[area] || 0); return { name: area, data: data, type: "bar", smooth: true, lineStyle: { width: getResponsiveValue(3), color: colors[index] }, itemStyle: { color: colors[index] }, lineStyle: { width: getResponsiveValue(3), color: colors[index % colors.length], }, itemStyle: { color: colors[index % colors.length] }, areaStyle: { opacity: 0.2, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: colors[index] + "80" }, { offset: 1, color: colors[index] + "00" }, { offset: 0, color: colors[index % colors.length] + "80" }, { offset: 1, color: colors[index % colors.length] + "00" }, ]), }, }; @@ -937,7 +1148,7 @@ borderWidth: getResponsiveValue(1), textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, formatter: function (params) { let result = params[0].name + "<br/>"; let result = params[0]?.name + "<br/>" || ""; params.forEach(param => { result += `${param.marker}${param.seriesName}: ${param.value} å <br/>`; }); @@ -964,7 +1175,7 @@ }, xAxis: { type: "category", data: periods, data: dates, axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, axisTick: { show: false }, axisLabel: { @@ -1164,7 +1375,12 @@ areaSet.add(key); }); }); salesAreas = Array.from(areaSet); // ç¡®ä¿"å ¨é¨"å¨ç¬¬ä¸ä¸ªä½ç½® salesAreas = Array.from(areaSet).sort((a, b) => { if (a === "å ¨é¨") return -1; if (b === "å ¨é¨") return 1; return 0; }); // 为æ¯ä¸ªéå®åºåçææ°æ® series = salesAreas.map((area, index) => { @@ -1382,15 +1598,18 @@ let boardScrollTimer = null; let blockCurrentIndex = 0; let boardCurrentIndex = 0; const startBlockTableScroll = () => { if (blockScrollTimer) { clearInterval(blockScrollTimer); } const scrollTable = () => { if (!blockTableBody.value || blockSalesData.value.length === 0) return; // åªæå½æ¶é´ç»´åº¦ä¸æ¯"å¹´"æ¶æå¯å¨æ»å¨ if (tableTimeDimension.value === "å¹´") { return; } const scrollTable = () => { if (!blockTableBody.value) return; const rows = blockTableBody.value.querySelectorAll("tr"); if (rows.length === 0) return; @@ -1403,9 +1622,10 @@ blockTableBody.value.style.transition = "none"; blockTableBody.value.style.transform = "translateY(0)"; const firstItem = blockSalesData.value[0]; blockSalesData.value.shift(); blockSalesData.value.push(firstItem); // ç´æ¥æä½DOMï¼å°ç¬¬ä¸è¡ç§»å°æå const firstRow = rows[0]; blockTableBody.value.removeChild(firstRow); blockTableBody.value.appendChild(firstRow); }, 500); }; @@ -1450,6 +1670,10 @@ clearInterval(boardScrollTimer); boardScrollTimer = null; } if (amountScrollTimer.value) { clearInterval(amountScrollTimer.value); amountScrollTimer.value = null; } }; // å¤çæ¶é´ç»´åº¦éæ© @@ -1462,7 +1686,7 @@ boardTimeDimension.value = dimension; generateBoardSalesData(); }; const blockProductType = ref("ç å"); // å¤ç产åç±»åéæ© const handleBlockProductTypeChange = type => { blockProductType.value = type; @@ -1473,7 +1697,7 @@ boardProductType.value = type; generateBoardSalesData(); }; const blockSelectedArea = ref("å ¨é¨"); // å¤çéå®åºéæ© const handleBlockAreaChange = area => { blockSelectedArea.value = area; @@ -1490,6 +1714,7 @@ customerTimeDimension.value = dimension; fetchCustomerTrendsData(); }; const blockTimeDimension = ref("å¹´"); // çæç åé宿°æ® const generateBlockSalesData = () => { @@ -1622,18 +1847,269 @@ // è·åæ°æ® await fetchDashboardData(); await fetchCustomerTrendsData(); await fetchSalesAnalysisTrendData(); await fetchTableSalesData(); await fetchSalesAmountChartData(); await fetchSalesAmountTableData(); // çå¾ DOMæ´æ°ååå§åå¾è¡¨ nextTick(() => { initCharts(); // å¯å¨è¡¨æ ¼æ»å¨å¨ç» startBlockTableScroll(); startBoardTableScroll(); startAmountTableScroll(); }); // æ·»å çªå£å¤§å°ååçå¬ window.addEventListener("resize", handleResize); document.addEventListener("fullscreenchange", handleFullscreenChange); }); // çå¬å¾è¡¨æ¶é´ç»´åº¦å产åç±»ååå watch([chartTimeDimension, chartProductType], async () => { await fetchSalesAnalysisTrendData(); }); // çå¬è¡¨æ ¼æ¶é´ç»´åº¦å产åç±»ååå watch([tableTimeDimension, tableProductType], async () => { await fetchTableSalesData(); }); // éå®éé¢åæå¾è¡¨æ°æ®ï¼å³ä¸ï¼ const salesAmountChartData = ref({ dates: [], customerTrends: [], }); // éå®é¢æ°æ®ç»è®¡è¡¨æ ¼æ°æ®ï¼å³ä¸ï¼ const salesAmountTableData = ref({ dates: [], customerTrends: [], }); // éå®é¢æ°æ®ç»è®¡è¡¨æ ¼çéç¶æï¼å³ä¸ï¼ const tableTimeDimension2 = ref("å¹´"); const tableProductType2 = ref("ç å"); // éå®é¢æ°æ®ç»è®¡è¡¨æ ¼æ°æ® const amountSalesData = ref([]); const amountScrollTimer = ref(null); // è·åéå®éé¢åæå¾è¡¨æ°æ®ï¼å³ä¸ï¼ const fetchSalesAmountChartData = async () => { try { const response = await getSalesAmountAnalysis({ type: chartProductType2.value, days: chartTimeDimension2.value, }); if (response?.data) { salesAmountChartData.value = response.data; updateCharts(); } } catch (error) { console.error("è·åéå®éé¢åæå¾è¡¨æ°æ®å¤±è´¥:", error); // ä½¿ç¨æ¨¡ææ°æ® salesAmountChartData.value = { dates: [ "2026-01-01", "2025-01-01", "2024-01-01", "2023-01-01", "2022-01-01", ], customerTrends: [ { å èå¤: 100, é¶å·: 200, èªæ: 300, å ¶ä»: 150, å ¨é¨: 750 }, { å èå¤: 80, é¶å·: 180, èªæ: 280, å ¶ä»: 130, å ¨é¨: 670 }, { å èå¤: 90, é¶å·: 190, èªæ: 290, å ¶ä»: 140, å ¨é¨: 710 }, { å èå¤: 70, é¶å·: 170, èªæ: 270, å ¶ä»: 120, å ¨é¨: 630 }, { å èå¤: 110, é¶å·: 210, èªæ: 310, å ¶ä»: 160, å ¨é¨: 790 }, ], }; } }; // è·åéå®é¢æ°æ®ç»è®¡è¡¨æ ¼æ°æ®ï¼å³ä¸ï¼ const fetchSalesAmountTableData = async () => { try { const response = await getSalesAmountAnalysis({ type: tableProductType2.value, days: tableTimeDimension2.value, }); if (response?.data) { salesAmountTableData.value = response.data; updateAmountSalesData(); } } catch (error) { console.error("è·åéå®é¢æ°æ®ç»è®¡è¡¨æ ¼æ°æ®å¤±è´¥:", error); // ä½¿ç¨æ¨¡ææ°æ® salesAmountTableData.value = { dates: [ "2026-01-01", "2025-01-01", "2024-01-01", "2023-01-01", "2022-01-01", ], customerTrends: [ { å èå¤: 100, é¶å·: 200, èªæ: 300, å ¶ä»: 150, å ¨é¨: 750 }, { å èå¤: 80, é¶å·: 180, èªæ: 280, å ¶ä»: 130, å ¨é¨: 670 }, { å èå¤: 90, é¶å·: 190, èªæ: 290, å ¶ä»: 140, å ¨é¨: 710 }, { å èå¤: 70, é¶å·: 170, èªæ: 270, å ¶ä»: 120, å ¨é¨: 630 }, { å èå¤: 110, é¶å·: 210, èªæ: 310, å ¶ä»: 160, å ¨é¨: 790 }, ], }; updateAmountSalesData(); } }; // æ´æ°éå®éé¢åæè¡¨æ ¼æ°æ® const updateAmountSalesData = () => { const data = []; const { dates, customerTrends } = salesAmountTableData.value; // æåææéå®åºå const areaSet = new Set(); customerTrends.forEach(item => { Object.keys(item).forEach(key => areaSet.add(key)); }); // æ´æ°éå®åºåå表ï¼ç¡®ä¿"å ¨é¨"å¨ç¬¬ä¸ä½ tableSalesAreas.value = [ "å ¨é¨", ...Array.from(areaSet).filter(area => area !== "å ¨é¨"), ]; // ç¡®ä¿éä¸çåºåå¨åè¡¨ä¸ if (!tableSalesAreas.value.includes(tableSelectedArea.value)) { tableSelectedArea.value = "å ¨é¨"; } // çæè¡¨æ ¼æ°æ® dates.forEach((date, index) => { const trends = customerTrends[index] || {}; Object.keys(trends).forEach(area => { data.push({ period: date, area: area, productType: tableProductType2.value, sales: trends[area], sort: data.length + 1, }); }); }); amountSalesData.value = data; }; // çéåçéå®éé¢åæè¡¨æ ¼æ°æ® const filteredAmountSalesData = computed(() => { if (tableSelectedArea.value === "å ¨é¨") { // æå¹´æåç»æ±æ»æ°æ® const groupedData = {}; amountSalesData.value.forEach(item => { const key = item.period; if (!groupedData[key]) { groupedData[key] = { period: item.period, area: "å ¨é¨", productType: tableProductType2.value, sales: 0, }; } groupedData[key].sales += item.sales; }); // 转æ¢ä¸ºæ°ç»å¹¶æå¹´ææåº return Object.values(groupedData).sort((a, b) => { return new Date(b.period) - new Date(a.period); }); } else { return amountSalesData.value.filter( item => item.area === tableSelectedArea.value ); } }); // éå®éé¢åæè¡¨æ ¼æ»è®¡ const filteredAmountSalesTotal = computed(() => { return filteredAmountSalesData.value.reduce( (total, item) => total + item.sales, 0 ); }); // å¤çéå®éé¢åæå¾è¡¨æ¶é´ç»´åº¦ååï¼å³ä¸ï¼ const handleChartTimeDimensionChange2 = dimension => { chartTimeDimension2.value = dimension; fetchSalesAmountChartData(); }; // å¤çéå®éé¢åæå¾è¡¨äº§åç±»åååï¼å³ä¸ï¼ const handleChartProductTypeChange2 = type => { chartProductType2.value = type; fetchSalesAmountChartData(); }; // å¤çéå®é¢æ°æ®ç»è®¡è¡¨æ ¼æ¶é´ç»´åº¦ååï¼å³ä¸ï¼ const handleTableTimeDimensionChange2 = dimension => { tableTimeDimension2.value = dimension; fetchSalesAmountTableData(); // éæ°å¯å¨æ»å¨ï¼æ ¹æ®æ¶é´ç»´åº¦å³å®æ¯å¦æ»å¨ startAmountTableScroll(); }; // å¤çéå®é¢æ°æ®ç»è®¡è¡¨æ ¼äº§åç±»åååï¼å³ä¸ï¼ const handleTableProductTypeChange2 = type => { tableProductType2.value = type; fetchSalesAmountTableData(); }; // å¤çéå®é¢æ°æ®ç»è®¡è¡¨æ ¼éå®åºååï¼å³ä¸ï¼ const handleTableAreaChange2 = area => { tableSelectedArea.value = area; }; // å¯å¨éå®éé¢åæè¡¨æ ¼æ»å¨ const startAmountTableScroll = () => { if (amountScrollTimer.value) { clearInterval(amountScrollTimer.value); } // åªæå½æ¶é´ç»´åº¦ä¸æ¯"å¹´"æ¶æå¯å¨æ»å¨ if (tableTimeDimension2.value === "å¹´") { return; } const scrollTable = () => { if (!boardTableBody.value) return; const rows = boardTableBody.value.querySelectorAll("tr"); if (rows.length === 0) return; const rowHeight = rows[0].offsetHeight; boardTableBody.value.style.transition = "transform 0.5s ease-in-out"; boardTableBody.value.style.transform = `translateY(-${rowHeight}px)`; setTimeout(() => { boardTableBody.value.style.transition = "none"; boardTableBody.value.style.transform = "translateY(0)"; // ç´æ¥æä½DOMï¼å°ç¬¬ä¸è¡ç§»å°æå const firstRow = rows[0]; boardTableBody.value.removeChild(firstRow); boardTableBody.value.appendChild(firstRow); }, 500); }; amountScrollTimer.value = setInterval(scrollTable, 2000); }; // çå¬éå®éé¢åæå¾è¡¨æ¶é´ç»´åº¦å产åç±»åååï¼å³ä¸ï¼ watch([chartTimeDimension2, chartProductType2], async () => { // await fetchSalesAmountChartData(); }); // çå¬éå®é¢æ°æ®ç»è®¡è¡¨æ ¼æ¶é´ç»´åº¦å产åç±»åååï¼å³ä¸ï¼ watch([tableTimeDimension2, tableProductType2], async () => { // await fetchSalesAmountTableData(); }); // è·å产åç±»åæ ç¾ç±»å @@ -1958,12 +2434,12 @@ } /* .scroll-table tbody tr:nth-child(odd) { background-color: rgba(64, 158, 255, 0.05); } background-color: rgba(64, 158, 255, 0.05); } .scroll-table tbody tr:nth-child(even) { background-color: rgba(64, 158, 255, 0.1); } */ .scroll-table tbody tr:nth-child(even) { background-color: rgba(64, 158, 255, 0.1); } */ .oddTableTr { background-color: rgba(64, 158, 255, 0.05); } @@ -2062,7 +2538,7 @@ font-size: 1.4vh; font-weight: 800; color: #00a4ed; margin-right: 5.8vh; margin-right: 1.8vh; text-shadow: 0 0 1vh rgba(0, 164, 237, 0.5); } .diamond {