| | |
| | | |
| | | <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); |
| | | }); |
| | | |