| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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, |
| | | }); |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | // è·åå·¥åºåæ°å表 |
| | | export function getProcessParamList(processId) { |
| | | export function getProcessParamList(processId,params) { |
| | | return request({ |
| | | url: `/productProcessParam/list/${processId}`, |
| | | method: "get", |
| | | params, |
| | | }); |
| | | } |
| | | |
| | |
| | | method: "post", |
| | | data: query, |
| | | }); |
| | | } |
| | | } |
| | | |
| | | |
| | | // 追踪è¿åº¦ |
| | | export function trackProgressByNo(query) { |
| | | return request({ |
| | | url: "/track/trackProgressByNo", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | |
| | | 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 |
| | | }) |
| | | } |
| | |
| | | prop: "customerType", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "è·è¿è¿åº¦", |
| | | prop: "followUpLevel", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "è·è¿æ¶é´", |
| | | prop: "followUpTime", |
| | | width: 120, |
| | | }, |
| | | // { |
| | | // label: "è·è¿è¿åº¦", |
| | | // prop: "followUpLevel", |
| | | // width: 120, |
| | | // }, |
| | | // { |
| | | // label: "è·è¿æ¶é´", |
| | | // prop: "followUpTime", |
| | | // width: 120, |
| | | // }, |
| | | { |
| | | label: "é¶è¡åºæ¬æ·", |
| | | prop: "basicBankAccount", |
| | |
| | | }; |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "è§æ ¼åå·", |
| | | label: "产å", |
| | | prop: "productName", |
| | | }, |
| | | { |
| | | label: "è§æ ¼", |
| | | label: "è§æ ¼åå·", |
| | | prop: "model", |
| | | }, |
| | | { |
| | |
| | | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | <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"> |
| | |
| | | </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 |
| | |
| | | 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; |
| | |
| | | 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, |
| | |
| | | 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, |
| | |
| | | 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/>"); |
| | | }, |
| | | }, |
| | |
| | | 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 = ""; |
| | |
| | | }; |
| | | |
| | | 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 ? "+" : ""; |
| | |
| | | } |
| | | updateChart(); |
| | | }); |
| | | Promise.all([fetchCategoryOptions(), fetchCostTypeOptions()]).then(() => { |
| | | handleQuery(); |
| | | }); |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | |
| | | 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"> |
| | |
| | | </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" |
| | |
| | | :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" |
| | |
| | | </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> |
| | |
| | | }, |
| | | ]); |
| | | |
| | | // è½èç±»åå æ¯æ°æ® |
| | | 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 energyData = reactive({ |
| | | water: 120, |
| | | electricity: 350, |
| | | gas: 80, |
| | | }); |
| | | |
| | | // æ´æ°è½èç±»åå æ¯å¾è¡¨ |
| | | const updateEnergyTypeChart = () => { |
| | | const { water, electricity, gas } = energyData; |
| | | const total = water + electricity + gas; |
| | | |
| | | energyTypeSeries[0].data = [ |
| | | { |
| | | value: total > 0 ? ((water / total) * 100).toFixed(2) : 0, |
| | | name: "æ°´", |
| | | itemStyle: { color: "#409EFF" }, |
| | | }, |
| | | { |
| | | value: total > 0 ? ((electricity / total) * 100).toFixed(2) : 0, |
| | | name: "çµ", |
| | | itemStyle: { color: "#E6A23C" }, |
| | | }, |
| | | { |
| | | value: total > 0 ? ((gas / total) * 100).toFixed(2) : 0, |
| | | name: "æ°", |
| | | itemStyle: { color: "#F56C6C" }, |
| | | }, |
| | | ]; |
| | | }; |
| | | |
| | | const planTable = reactive([]); |
| | | const recentTrendCards = reactive([ |
| | | { |
| | |
| | | }; |
| | | |
| | | const loadHomeTodos = async () => { |
| | | try { |
| | | const res = await homeTodos(); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.slice(0, 4).map((item, idx) => { |
| | | const text = |
| | | item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | const levelType = idx === 0 ? "danger" : idx <= 2 ? "warning" : "success"; |
| | | const level = idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½"; |
| | | return { level, title: text, type: levelType }; |
| | | }); |
| | | updateArray(todos, mapped); |
| | | const pendingMapped = list.slice(0, 4).map((item, idx) => { |
| | | const title = |
| | | item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | const path = inferTodoPath(item); |
| | | return { |
| | | id: item?.id || `${idx}-${title}`, |
| | | title, |
| | | level: idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½", |
| | | type: idx === 0 ? "danger" : idx <= 2 ? "warning" : "success", |
| | | path, |
| | | ownerId: item?.approveUserId || item?.userId || "", |
| | | ownerName: item?.approveUserName || item?.userName || "", |
| | | }; |
| | | }); |
| | | updateArray(pendingTasks, pendingMapped); |
| | | } catch (error) { |
| | | console.error("homeTodosæ¥å£è·å失败:", error); |
| | | } |
| | | // try { |
| | | // const res = await homeTodos(); |
| | | // const list = Array.isArray(res?.data) ? res.data : []; |
| | | // const mapped = list.slice(0, 4).map((item, idx) => { |
| | | // const text = |
| | | // item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | // const levelType = idx === 0 ? "danger" : idx <= 2 ? "warning" : "success"; |
| | | // const level = idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½"; |
| | | // return { level, title: text, type: levelType }; |
| | | // }); |
| | | // updateArray(todos, mapped); |
| | | // const pendingMapped = list.slice(0, 4).map((item, idx) => { |
| | | // const title = |
| | | // item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | // const path = inferTodoPath(item); |
| | | // return { |
| | | // id: item?.id || `${idx}-${title}`, |
| | | // title, |
| | | // level: idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½", |
| | | // type: idx === 0 ? "danger" : idx <= 2 ? "warning" : "success", |
| | | // path, |
| | | // ownerId: item?.approveUserId || item?.userId || "", |
| | | // ownerName: item?.approveUserName || item?.userName || "", |
| | | // }; |
| | | // }); |
| | | // updateArray(pendingTasks, pendingMapped); |
| | | // } catch (error) { |
| | | // console.error("homeTodosæ¥å£è·å失败:", error); |
| | | // } |
| | | }; |
| | | |
| | | const loadOrderAndProgress = async () => { |
| | | try { |
| | | const [orderRes, progressRes] = await Promise.allSettled([ |
| | | orderCount(), |
| | | getProgressStatistics(), |
| | | ]); |
| | | |
| | | if (orderRes.status === "fulfilled") { |
| | | const items = Array.isArray(orderRes.value?.data) |
| | | ? orderRes.value.data |
| | | : []; |
| | | const byName = Object.fromEntries( |
| | | items.map(i => [String(i?.name || "").replace(/\s/g, ""), i?.value]) |
| | | ); |
| | | businessFocus[0].value = `${ |
| | | pickFirstNumber(byName, ["çäº§è®¢åæ°", "çäº§è®¢åæ»æ°", "æ»è®¢åæ°"]) || 0 |
| | | } å`; |
| | | businessFocus[1].value = `${ |
| | | pickFirstNumber(byName, ["å·²å®æè®¢åæ°"]) || 0 |
| | | } å`; |
| | | businessFocus[2].value = `${ |
| | | pickFirstNumber(byName, ["å¾
çäº§è®¢åæ°", "æªå®æè®¢åæ°"]) || 0 |
| | | } å`; |
| | | businessFocus[3].value = `${ |
| | | pickFirstNumber(byName, ["é¨åå®æè®¢åæ°"]) || 0 |
| | | } å`; |
| | | } |
| | | |
| | | if (progressRes.status === "fulfilled") { |
| | | const p = progressRes.value?.data || {}; |
| | | const detail = Array.isArray(p.completedOrderDetails) |
| | | ? p.completedOrderDetails |
| | | : []; |
| | | const rows = detail.slice(0, 6).map((item, index) => { |
| | | const qty = pickFirstNumber(item, ["quantity", "planQuantity"]); |
| | | const done = pickFirstNumber(item, [ |
| | | "completeQuantity", |
| | | "completedQuantity", |
| | | ]); |
| | | return { |
| | | planNo: item.npsNo || item.productionPlanNo || `NO-${index + 1}`, |
| | | product: item.productCategory || item.productName || "-", |
| | | qty, |
| | | issued: done, |
| | | status: |
| | | qty > 0 && done >= qty ? "已宿" : done > 0 ? "æ§è¡ä¸" : "å¾
ä¸å", |
| | | }; |
| | | }); |
| | | updateArray(planTable, rows); |
| | | setTrendCard( |
| | | "planIssued", |
| | | detail |
| | | .slice(-7) |
| | | .map(i => |
| | | pickFirstNumber(i, [ |
| | | "completeQuantity", |
| | | "completedQuantity", |
| | | "issueNum", |
| | | ]) |
| | | ) |
| | | ); |
| | | } |
| | | } catch (error) { |
| | | console.error("orderCount/getProgressStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | // try { |
| | | // const [orderRes, progressRes] = await Promise.allSettled([ |
| | | // orderCount(), |
| | | // getProgressStatistics(), |
| | | // ]); |
| | | // if (orderRes.status === "fulfilled") { |
| | | // const items = Array.isArray(orderRes.value?.data) |
| | | // ? orderRes.value.data |
| | | // : []; |
| | | // const byName = Object.fromEntries( |
| | | // items.map(i => [String(i?.name || "").replace(/\s/g, ""), i?.value]) |
| | | // ); |
| | | // businessFocus[0].value = `${ |
| | | // pickFirstNumber(byName, ["çäº§è®¢åæ°", "çäº§è®¢åæ»æ°", "æ»è®¢åæ°"]) || 0 |
| | | // } å`; |
| | | // businessFocus[1].value = `${ |
| | | // pickFirstNumber(byName, ["å·²å®æè®¢åæ°"]) || 0 |
| | | // } å`; |
| | | // businessFocus[2].value = `${ |
| | | // pickFirstNumber(byName, ["å¾
çäº§è®¢åæ°", "æªå®æè®¢åæ°"]) || 0 |
| | | // } å`; |
| | | // businessFocus[3].value = `${ |
| | | // pickFirstNumber(byName, ["é¨åå®æè®¢åæ°"]) || 0 |
| | | // } å`; |
| | | // } |
| | | // if (progressRes.status === "fulfilled") { |
| | | // const p = progressRes.value?.data || {}; |
| | | // const detail = Array.isArray(p.completedOrderDetails) |
| | | // ? p.completedOrderDetails |
| | | // : []; |
| | | // const rows = detail.slice(0, 6).map((item, index) => { |
| | | // const qty = pickFirstNumber(item, ["quantity", "planQuantity"]); |
| | | // const done = pickFirstNumber(item, [ |
| | | // "completeQuantity", |
| | | // "completedQuantity", |
| | | // ]); |
| | | // return { |
| | | // planNo: item.npsNo || item.productionPlanNo || `NO-${index + 1}`, |
| | | // product: item.productCategory || item.productName || "-", |
| | | // qty, |
| | | // issued: done, |
| | | // status: |
| | | // qty > 0 && done >= qty ? "已宿" : done > 0 ? "æ§è¡ä¸" : "å¾
ä¸å", |
| | | // }; |
| | | // }); |
| | | // updateArray(planTable, rows); |
| | | // setTrendCard( |
| | | // "planIssued", |
| | | // detail |
| | | // .slice(-7) |
| | | // .map(i => |
| | | // pickFirstNumber(i, [ |
| | | // "completeQuantity", |
| | | // "completedQuantity", |
| | | // "issueNum", |
| | | // ]) |
| | | // ) |
| | | // ); |
| | | // } |
| | | // } catch (error) { |
| | | // console.error("orderCount/getProgressStatisticsæ¥å£è·å失败:", error); |
| | | // } |
| | | }; |
| | | |
| | | const inferTodoPath = todo => { |
| | |
| | | }; |
| | | |
| | | const loadPlanTrend = async () => { |
| | | try { |
| | | const res = await processDataProductionStatistics({ |
| | | type: chartRangePlan.value, |
| | | }); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | planXAxis[0].data = list.map( |
| | | (i, index) => i.processName || `å·¥åº${index + 1}` |
| | | ); |
| | | planSeries[0].data = list.map(i => |
| | | pickFirstNumber(i, ["totalInput", "input", "planNum"]) |
| | | ); |
| | | planSeries[1].data = list.map(i => |
| | | pickFirstNumber(i, ["totalOutput", "output", "issueNum"]) |
| | | ); |
| | | planSeries[2].data = list.map(i => |
| | | pickFirstNumber(i, ["totalScrap", "scrap", "completeNum"]) |
| | | ); |
| | | } catch (error) { |
| | | console.error("processDataProductionStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | // try { |
| | | // const res = await processDataProductionStatistics({ |
| | | // type: chartRangePlan.value, |
| | | // }); |
| | | // const list = Array.isArray(res?.data) ? res.data : []; |
| | | // planXAxis[0].data = list.map( |
| | | // (i, index) => i.processName || `å·¥åº${index + 1}` |
| | | // ); |
| | | // planSeries[0].data = list.map(i => |
| | | // pickFirstNumber(i, ["totalInput", "input", "planNum"]) |
| | | // ); |
| | | // planSeries[1].data = list.map(i => |
| | | // pickFirstNumber(i, ["totalOutput", "output", "issueNum"]) |
| | | // ); |
| | | // planSeries[2].data = list.map(i => |
| | | // pickFirstNumber(i, ["totalScrap", "scrap", "completeNum"]) |
| | | // ); |
| | | // } catch (error) { |
| | | // console.error("processDataProductionStatisticsæ¥å£è·å失败:", error); |
| | | // } |
| | | }; |
| | | |
| | | const loadQualityData = async () => { |
| | | try { |
| | | const res = await qualityInspectionStatistics({ |
| | | type: chartRangeQuality.value, |
| | | }); |
| | | const data = res?.data || {}; |
| | | const items = Array.isArray(data.item) ? data.item : []; |
| | | if (items.length > 0) { |
| | | qualityXAxis[0].data = items.map(i => i.date || i.name || "-"); |
| | | qualitySeries[0].data = items.map(i => |
| | | pickFirstNumber(i, [ |
| | | "supplierNum", |
| | | "processNum", |
| | | "factoryNum", |
| | | "totalNum", |
| | | ]) |
| | | ); |
| | | setTrendCard( |
| | | "qualityRaw", |
| | | items.map(i => pickFirstNumber(i, ["supplierNum"])) |
| | | ); |
| | | setTrendCard( |
| | | "qualityProcess", |
| | | items.map(i => pickFirstNumber(i, ["processNum"])) |
| | | ); |
| | | setTrendCard( |
| | | "qualityFactory", |
| | | items.map(i => pickFirstNumber(i, ["factoryNum"])) |
| | | ); |
| | | } else { |
| | | qualityXAxis[0].data = ["æ¥ææ£", "è¿ç¨æ£", "æåæ£"]; |
| | | qualitySeries[0].data = [ |
| | | pickFirstNumber(data, ["supplierNum"]), |
| | | pickFirstNumber(data, ["processNum"]), |
| | | pickFirstNumber(data, ["factoryNum"]), |
| | | ]; |
| | | setTrendCard("qualityRaw", [pickFirstNumber(data, ["supplierNum"])]); |
| | | setTrendCard("qualityProcess", [pickFirstNumber(data, ["processNum"])]); |
| | | setTrendCard("qualityFactory", [pickFirstNumber(data, ["factoryNum"])]); |
| | | } |
| | | businessFocus[4].value = `${pickFirstNumber(data, [ |
| | | "supplierNum", |
| | | "totalNum", |
| | | ])} æ¡`; |
| | | businessFocus[5].value = `${pickFirstNumber(data, ["processNum"])} æ¡`; |
| | | } catch (error) { |
| | | console.error("qualityInspectionStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | // try { |
| | | // const res = await qualityInspectionStatistics({ |
| | | // type: chartRangeQuality.value, |
| | | // }); |
| | | // const data = res?.data || {}; |
| | | // const items = Array.isArray(data.item) ? data.item : []; |
| | | // if (items.length > 0) { |
| | | // qualityXAxis[0].data = items.map(i => i.date || i.name || "-"); |
| | | // qualitySeries[0].data = items.map(i => |
| | | // pickFirstNumber(i, [ |
| | | // "supplierNum", |
| | | // "processNum", |
| | | // "factoryNum", |
| | | // "totalNum", |
| | | // ]) |
| | | // ); |
| | | // setTrendCard( |
| | | // "qualityRaw", |
| | | // items.map(i => pickFirstNumber(i, ["supplierNum"])) |
| | | // ); |
| | | // setTrendCard( |
| | | // "qualityProcess", |
| | | // items.map(i => pickFirstNumber(i, ["processNum"])) |
| | | // ); |
| | | // setTrendCard( |
| | | // "qualityFactory", |
| | | // items.map(i => pickFirstNumber(i, ["factoryNum"])) |
| | | // ); |
| | | // } else { |
| | | // qualityXAxis[0].data = ["æ¥ææ£", "è¿ç¨æ£", "æåæ£"]; |
| | | // qualitySeries[0].data = [ |
| | | // pickFirstNumber(data, ["supplierNum"]), |
| | | // pickFirstNumber(data, ["processNum"]), |
| | | // pickFirstNumber(data, ["factoryNum"]), |
| | | // ]; |
| | | // setTrendCard("qualityRaw", [pickFirstNumber(data, ["supplierNum"])]); |
| | | // setTrendCard("qualityProcess", [pickFirstNumber(data, ["processNum"])]); |
| | | // setTrendCard("qualityFactory", [pickFirstNumber(data, ["factoryNum"])]); |
| | | // } |
| | | // businessFocus[4].value = `${pickFirstNumber(data, [ |
| | | // "supplierNum", |
| | | // "totalNum", |
| | | // ])} æ¡`; |
| | | // businessFocus[5].value = `${pickFirstNumber(data, ["processNum"])} æ¡`; |
| | | // } catch (error) { |
| | | // console.error("qualityInspectionStatisticsæ¥å£è·å失败:", error); |
| | | // } |
| | | }; |
| | | |
| | | const loadWarningCenter = async () => { |
| | | try { |
| | | const res = await nonComplianceWarning(); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.slice(0, 6).map((item, idx) => { |
| | | const levelNum = toNumber(item.level ?? item.warningLevel ?? 2); |
| | | const levelType = |
| | | levelNum >= 3 ? "danger" : levelNum === 2 ? "warning" : "info"; |
| | | const levelText = levelNum >= 3 ? "é«" : levelNum === 2 ? "ä¸" : "ä½"; |
| | | const title = |
| | | item.name || item.title || item.paramName || `å¼å¸¸é¢è¦ ${idx + 1}`; |
| | | const text = `${title}${item.processName || ""}${ |
| | | item.orderNo || "" |
| | | }`.toLowerCase(); |
| | | const path = text.includes("è´¨æ£") |
| | | ? routePathMap.processInspection |
| | | : text.includes("订å") |
| | | ? routePathMap.order |
| | | : routePathMap.processInspection || |
| | | routePathMap.order || |
| | | routePathMap.plan; |
| | | return { |
| | | id: item.id || `${idx}-${title}`, |
| | | levelType, |
| | | levelText, |
| | | title, |
| | | path, |
| | | }; |
| | | }); |
| | | updateArray(warningList, mapped); |
| | | } catch (error) { |
| | | console.error("nonComplianceWarningæ¥å£è·å失败:", error); |
| | | updateArray(warningList, []); |
| | | } |
| | | // try { |
| | | // const res = await nonComplianceWarning(); |
| | | // const list = Array.isArray(res?.data) ? res.data : []; |
| | | // const mapped = list.slice(0, 6).map((item, idx) => { |
| | | // const levelNum = toNumber(item.level ?? item.warningLevel ?? 2); |
| | | // const levelType = |
| | | // levelNum >= 3 ? "danger" : levelNum === 2 ? "warning" : "info"; |
| | | // const levelText = levelNum >= 3 ? "é«" : levelNum === 2 ? "ä¸" : "ä½"; |
| | | // const title = |
| | | // item.name || item.title || item.paramName || `å¼å¸¸é¢è¦ ${idx + 1}`; |
| | | // const text = `${title}${item.processName || ""}${ |
| | | // item.orderNo || "" |
| | | // }`.toLowerCase(); |
| | | // const path = text.includes("è´¨æ£") |
| | | // ? routePathMap.processInspection |
| | | // : text.includes("订å") |
| | | // ? routePathMap.order |
| | | // : routePathMap.processInspection || |
| | | // routePathMap.order || |
| | | // routePathMap.plan; |
| | | // return { |
| | | // id: item.id || `${idx}-${title}`, |
| | | // levelType, |
| | | // levelText, |
| | | // title, |
| | | // path, |
| | | // }; |
| | | // }); |
| | | // updateArray(warningList, mapped); |
| | | // } catch (error) { |
| | | // console.error("nonComplianceWarningæ¥å£è·å失败:", error); |
| | | // updateArray(warningList, []); |
| | | // } |
| | | }; |
| | | |
| | | const initSectionConfig = () => { |
| | |
| | | }; |
| | | |
| | | const loadCostComposition = async () => { |
| | | try { |
| | | const res = await expenseCompositionAnalysis({ type: 1 }); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.map(i => ({ |
| | | name: i.name || "æªå½å", |
| | | value: pickFirstNumber(i, ["value", "amount", "cost"]), |
| | | })); |
| | | costSeries[0].data = mapped; |
| | | } catch (error) { |
| | | console.error("expenseCompositionAnalysisæ¥å£è·å失败:", error); |
| | | } |
| | | // try { |
| | | // const res = await expenseCompositionAnalysis({ type: 1 }); |
| | | // const list = Array.isArray(res?.data) ? res.data : []; |
| | | // const mapped = list.map(i => ({ |
| | | // name: i.name || "æªå½å", |
| | | // value: pickFirstNumber(i, ["value", "amount", "cost"]), |
| | | // })); |
| | | // costSeries[0].data = mapped; |
| | | // } catch (error) { |
| | | // console.error("expenseCompositionAnalysisæ¥å£è·å失败:", error); |
| | | // } |
| | | }; |
| | | |
| | | const refreshDashboardData = () => { |
| | |
| | | loadQualityData(); |
| | | loadCostComposition(); |
| | | loadWarningCenter(); |
| | | updateEnergyTypeChart(); |
| | | lastUpdatedAt.value = new Date().toLocaleString(); |
| | | }; |
| | | |
| | |
| | | rowKey="id" |
| | | :column="paramColumn" |
| | | :tableData="paramList" |
| | | :page="paramPage" |
| | | :page="paramPage2" |
| | | height="calc(100vh - 280px)" |
| | | :isSelection="false" |
| | | @pagination="handleParamPagination" /> |
| | |
| | | label: "æ¯å¦å¿
å¡«", |
| | | prop: "isRequired", |
| | | dataType: "tag", |
| | | formatType: row => (row.isRequired === true ? "success" : "info"), |
| | | formatData: row => (row.isRequired === true ? "æ¯" : "å¦"), |
| | | formatType: params => (params == true ? "success" : "info"), |
| | | formatData: params => (params == true ? "æ¯" : "å¦"), |
| | | }, |
| | | { |
| | | label: "æä½", |
| | |
| | | processLoading.value = false; |
| | | }); |
| | | }; |
| | | const paramPage2 = ref({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0, |
| | | }); |
| | | |
| | | // è·ååæ°å表 |
| | | const getParamList = processId => { |
| | | paramLoading.value = true; |
| | | getProcessParamList(processId) |
| | | console.log(paramPage2.value, "paramPage2.value"); |
| | | getProcessParamList(processId, { |
| | | current: paramPage2.value.current, |
| | | size: paramPage2.value.size, |
| | | }) |
| | | .then(res => { |
| | | paramList.value = res.data || []; |
| | | paramPage.total = paramList.value.length; |
| | | paramList.value = res.data.records || []; |
| | | paramPage2.value.total = res.data.total; |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("è·ååæ°å表失败"); |
| | |
| | | }; |
| | | |
| | | const handleParamPagination = obj => { |
| | | paramPage.current = obj.page; |
| | | paramPage.size = obj.limit; |
| | | console.log(obj, "obj"); |
| | | paramPage2.value.current = obj.page; |
| | | paramPage2.value.size = obj.limit; |
| | | getParamList(selectedProcess.value.id); |
| | | }; |
| | | |
| | |
| | | placeholder="è¯·éæ©å¼ºåº¦" |
| | | style="width: 100%"> |
| | | <el-option v-for="item in block_strength" |
| | | :key="item.id" |
| | | :key="item.label" |
| | | :label="item.label" |
| | | :value="item.id" /> |
| | | :value="item.label" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨ 1" |
| | |
| | | router.push({ |
| | | path: "/productionPlan/trackProgress", |
| | | query: { |
| | | applyNo: encodeURIComponent(row.applyNo), |
| | | id: row.id, |
| | | applyNo: row.applyNo, |
| | | productName: row.productName, |
| | | model: row.model, |
| | | }, |
| | | }); |
| | | }; |
| | |
| | | // margin-bottom: 0px !important; |
| | | // } |
| | | // } |
| | | :deep(.el-table .el-table__body-wrapper tr td) { |
| | | background-color: #fff; |
| | | } |
| | | </style> |
| | |
| | | :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" |
| | |
| | | :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"> |
| | |
| | | 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(); |
| | |
| | | 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 => { |
| | |
| | | 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; |
| | | }); |
| | | }; |
| | | |
| | | // çææ¨¡æè®¢åæ°æ® |
| | |
| | | |
| | | // å¤çç¹å»æ¥éª¤æ¥ç详æ
|
| | | 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; |
| | | }); |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¶é´ |
| | |
| | | // 页é¢å è½½æ¶è·åæ°æ® |
| | | 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> |
| | | |
| | |
| | | </div> |
| | | <!-- ä¸é´ä¸å¿ç¯ --> |
| | | <div class="center-ring"> |
| | | <!-- <div class="center-ring-box"> |
| | | <div class="center-metric m1"> |
| | | <div class="center-metric-label">项ç®äº§é</div> |
| | | <div class="center-metric-value">{{ projectProduction }}</div> |
| | | <div class="center-metric-unit">ä»¶</div> |
| | | <div class="center-ring-box"> |
| | | <div class="ring-box-topright"> |
| | | <div class="topright-label">åºåºå¤çé</div> |
| | | </div> |
| | | <div class="center-metric m2"> |
| | | <div class="center-metric-label">åºä½å¤çé</div> |
| | | <div class="center-metric-value">{{ solidWasteå¤çé }}</div> |
| | | <div class="center-metric-unit">å¨</div> |
| | | <div class="ring-box-left"> |
| | | <div class="left-label">ç²ç
¤ç°</div> |
| | | <div class="left-value">æå¤ç <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨ å¹´å¤ç <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨</div> |
| | | <div class="left-label" |
| | | style="margin-top: 2vh;">ç³è</div> |
| | | <div class="left-value">æå¤ç <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨ å¹´å¤ç <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨</div> |
| | | </div> |
| | | <div class="center-metric m3"> |
| | | <div class="center-metric-label">ç å产é</div> |
| | | <div class="center-metric-value">{{ blockProduction }}</div> |
| | | <div class="center-metric-unit">ä»¶</div> |
| | | <div class="ring-box-topleft"> |
| | | <div class="topleft-label">项ç®äº§é</div> |
| | | </div> |
| | | <div class="center-metric m4"> |
| | | <div class="center-metric-label">æ¿æäº§é</div> |
| | | <div class="center-metric-value">{{ boardProduction }}</div> |
| | | <div class="center-metric-unit">ä»¶</div> |
| | | <div class="ring-box-right"> |
| | | <div class="right-label">ç å产é</div> |
| | | <div class="right-value">æäº§é <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨ å¹´äº§é <span style="font-weight: bold;font-size: 1.3vh;">7812 |
| | | </span> å¨</div> |
| | | <div class="right-label" |
| | | style="margin-top: 2vh;">æ¿æäº§é</div> |
| | | <div class="right-value">æäº§é <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨ å¹´äº§é <span style="font-weight: bold;font-size: 1.3vh;">7812</span> å¨</div> |
| | | </div> |
| | | </div> --> |
| | | </div> |
| | | </div> |
| | | <!-- å·¦ä¸ï¼çäº§ææ¬åèç»è®¡ï¼æ¿æï¼ --> |
| | | <div class="bi-panel bi-panel-bottom-left"> |
| | |
| | | :class="{ active: customerTimeDimension === 'month' }" |
| | | @click="handleCustomerTimeDimensionChange('month')">æ</span> |
| | | </div> |
| | | <div ref="customerTrendChart" |
| | | class="echart-fill"></div> |
| | | <!-- <div class="bi-panel-body"> |
| | | <div class="chart-unit-row chart-unit-single"> |
| | | <span>åä½ï¼å®¶</span> |
| | |
| | | |
| | | // æ°å¢å®¢æ·è¶å¿å¾è¡¨é
ç½® |
| | | const customerTrendChartOption = computed(() => { |
| | | const customerTypes = ["å
¨é¨", "ç³ç°", "æ°´æ³¥", "éç²è", "è±æ¨¡å"]; |
| | | const colors = ["#00A4ED", "#34D8F7", "#4A8BFF", "#8A6BFF", "#C8C447"]; |
| | | const customerTypes = ["å
¨é¨", "ç²ç
¤ç°", "ç³è", "ç³ç°"]; |
| | | const colors = ["#00A4ED", "#4A8BFF", "#8A6BFF", "#C8C447"]; |
| | | const year = 2024; |
| | | const periodType = customerTimeDimension.value; |
| | | |
| | |
| | | } |
| | | |
| | | /* .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); |
| | | } |
| | |
| | | grid-row: 1 / span 2; |
| | | position: absolute; |
| | | background: url("@/assets/BI/imageSS@2x.png") no-repeat bottom center; |
| | | background-size: 100% 30%; |
| | | background-size: 80% 30%; |
| | | left: 25%; |
| | | top: 25%; |
| | | transform: translate(-50%, -50%); |
| | | width: 60vh; |
| | | transform: translate(-50%, -45%); |
| | | width: 50%; |
| | | height: 40.5vh; |
| | | z-index: 3; |
| | | pointer-events: none; |
| | |
| | | .center-ring-box { |
| | | position: absolute; |
| | | /* inset: 0; */ |
| | | height: 100%; |
| | | height: 88%; |
| | | width: 100%; |
| | | margin-top: 3%; |
| | | /* background-color: #fff; */ |
| | | background: url("@/assets/BI/imageSStop.png") no-repeat center center; |
| | | background-size: 80% 90%; |
| | | background: url("@/assets/BI/SCbg.png") no-repeat center center; |
| | | background-size: 100% 100%; |
| | | } |
| | | .ring-box-topright { |
| | | position: absolute; |
| | | top: 6vh; |
| | | right: 0; |
| | | width: 25%; |
| | | height: 15%; |
| | | background: url("@/assets/BI/SCbgright.png") no-repeat center center; |
| | | background-size: 100% 100%; |
| | | text-align: right; |
| | | } |
| | | .ring-box-topleft { |
| | | position: absolute; |
| | | top: 6vh; |
| | | left: 0; |
| | | width: 25%; |
| | | height: 15%; |
| | | background: url("@/assets/BI/SCbgleft.png") no-repeat center center; |
| | | background-size: 100% 100%; |
| | | text-align: left; |
| | | } |
| | | .topright-label { |
| | | font-size: 1.8vh; |
| | | font-weight: 500; |
| | | color: rgba(234, 246, 255, 0.9); |
| | | margin-top: 0; |
| | | position: relative; |
| | | bottom: 3vh; |
| | | right: 1vh; |
| | | } |
| | | |
| | | .topleft-label { |
| | | font-size: 1.8vh; |
| | | font-weight: 500; |
| | | color: rgba(234, 246, 255, 0.9); |
| | | margin-top: 0; |
| | | position: relative; |
| | | bottom: 3vh; |
| | | left: 1vh; |
| | | } |
| | | .ring-box-left { |
| | | /* background-color: #ebebeb; */ |
| | | width: 30%; |
| | | position: absolute; |
| | | left: 1vh; |
| | | top: 56%; |
| | | transform: translateY(-50%); |
| | | } |
| | | .left-label { |
| | | font-size: 1.4vh; |
| | | font-weight: 500; |
| | | color: #0effef; |
| | | margin-top: 0; |
| | | position: relative; |
| | | bottom: 3vh; |
| | | } |
| | | .left-value { |
| | | font-size: 1.2vh; |
| | | font-weight: 500; |
| | | color: rgba(234, 246, 255, 0.9); |
| | | margin-top: 0; |
| | | position: relative; |
| | | bottom: 3vh; |
| | | margin-top: 0.4vh; |
| | | } |
| | | .ring-box-right { |
| | | /* background-color: #ebebeb; */ |
| | | width: 30%; |
| | | float: right; |
| | | position: absolute; |
| | | right: -1vh; |
| | | top: 56%; |
| | | transform: translateY(-50%); |
| | | } |
| | | .right-label { |
| | | font-size: 1.4vh; |
| | | font-weight: 500; |
| | | color: #ffa60e; |
| | | margin-top: 0; |
| | | position: relative; |
| | | bottom: 3vh; |
| | | } |
| | | |
| | | .right-value { |
| | | font-size: 1.2vh; |
| | | font-weight: 500; |
| | | color: rgba(234, 246, 255, 0.9); |
| | | margin-top: 0; |
| | | position: relative; |
| | | bottom: 3vh; |
| | | margin-top: 0.4vh; |
| | | } |
| | | |
| | | .center-ring-bg { |
| | |
| | | <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" |
| | |
| | | <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> |
| | |
| | | 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"> |
| | |
| | | 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"> |
| | |
| | | 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"> |
| | |
| | | </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> |
| | |
| | | </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> |
| | |
| | | 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> |
| | |
| | | </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> |
| | |
| | | import { |
| | | getDashboardStatistics, |
| | | getCustomerTrends, |
| | | getSalesAnalysisTrend, |
| | | getSalesAmountAnalysis, |
| | | } from "@/api/reportAnalysis/salesStatistics"; |
| | | |
| | | const router = useRouter(); |
| | |
| | | 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("å¹´"); |
| | |
| | | |
| | | // 客æ·è¶å¿æ°æ® |
| | | const customerTrendsData = ref([]); |
| | | // ééåæè¶å¿æ°æ® |
| | | const salesAnalysisTrendData = ref([]); |
| | | // ééæ°æ®ç»è®¡è¡¨æ ¼æ°æ® |
| | | const tableSalesData = ref([]); |
| | | // ééæ°æ®ç»è®¡è¡¨æ ¼æ»è®¡ |
| | | const tableSalesTotal = ref(0); |
| | | // 卿éå®åºåå表 |
| | | const tableSalesAreas = ref([]); |
| | | |
| | | // ååç计ç®ï¼æ¨¡æï¼ |
| | | const salesVolumeChange = ref("+5.2"); |
| | |
| | | } |
| | | }; |
| | | |
| | | // è·åééåæè¶å¿æ°æ® |
| | | 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 => { |
| | |
| | | }); |
| | | }); |
| | | |
| | | // çéåçè¡¨æ ¼æ°æ® |
| | | 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", |
| | |
| | | "#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", |
| | |
| | | |
| | | // éå®éé¢è¶å¿å¾è¡¨é
ç½® |
| | | 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", |
| | |
| | | "#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" }, |
| | | ]), |
| | | }, |
| | | }; |
| | |
| | | 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/>`; |
| | | }); |
| | |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | data: dates, |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { |
| | |
| | | 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) => { |
| | |
| | | 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; |
| | | |
| | |
| | | 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); |
| | | }; |
| | | |
| | |
| | | clearInterval(boardScrollTimer); |
| | | boardScrollTimer = null; |
| | | } |
| | | if (amountScrollTimer.value) { |
| | | clearInterval(amountScrollTimer.value); |
| | | amountScrollTimer.value = null; |
| | | } |
| | | }; |
| | | |
| | | // å¤çæ¶é´ç»´åº¦éæ© |
| | |
| | | boardTimeDimension.value = dimension; |
| | | generateBoardSalesData(); |
| | | }; |
| | | |
| | | const blockProductType = ref("ç å"); |
| | | // å¤ç产åç±»åéæ© |
| | | const handleBlockProductTypeChange = type => { |
| | | blockProductType.value = type; |
| | |
| | | boardProductType.value = type; |
| | | generateBoardSalesData(); |
| | | }; |
| | | |
| | | const blockSelectedArea = ref("å
¨é¨"); |
| | | // å¤çéå®åºéæ© |
| | | const handleBlockAreaChange = area => { |
| | | blockSelectedArea.value = area; |
| | |
| | | customerTimeDimension.value = dimension; |
| | | fetchCustomerTrendsData(); |
| | | }; |
| | | const blockTimeDimension = ref("å¹´"); |
| | | |
| | | // çæç åé宿°æ® |
| | | const generateBlockSalesData = () => { |
| | |
| | | // è·åæ°æ® |
| | | 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(); |
| | | }); |
| | | |
| | | // è·å产åç±»åæ ç¾ç±»å |
| | |
| | | } |
| | | |
| | | /* .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); |
| | | } |
| | |
| | | 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 { |