| | |
| | | "favicon": "favicon/ZSJCLogo.ico" |
| | | }, |
| | | "screen": "/src/assets/images/login-background.png", |
| | | "logo": "/src/assets/logo/å®å¤ä¸ç建æç§ææéå
¬å¸.png", |
| | | "logo": "/src/assets/logo/logo.png", |
| | | "favicon": "/public/favicon.ico" |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request'; |
| | | |
| | | // çäº§ææ¬æ ¸ç®ç¸å
³API |
| | | export function getProductionCostSummary(params) { |
| | | return request({ |
| | | url: '/cost/productionCost/summary', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // æäº§åç©ææ±æ» |
| | | export function getProductionCostAggregateByProduct(params) { |
| | | return request({ |
| | | url: '/cost/productionCost/aggregate/product', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | |
| | | // ç产订åTop10 |
| | | export function getProductionCostTopOrders(params) { |
| | | return request({ |
| | | url: '/cost/productionCost/top/order', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | | // æè®¢åæ±æ» |
| | | export function getProductionCostAggregateByOrder(params) { |
| | | return request({ |
| | | url: '/cost/productionCost/aggregate/order', |
| | | method: 'get', |
| | | params |
| | | }); |
| | | } |
| | |
| | | <template> |
| | | <div class="production-cost-page"> |
| | | <el-card class="filter-card" shadow="never"> |
| | | <el-card class="filter-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="card-head"> |
| | | <div class="card-head-left"> |
| | | <el-icon class="card-icon ui-icon"><DataLine /></el-icon> |
| | | <el-icon class="card-icon ui-icon"> |
| | | <DataLine /> |
| | | </el-icon> |
| | | <span class="card-title">çäº§ææ¬æ ¸ç®</span> |
| | | <span class="subtle">ææ¬ = Σ æå
¥é à 对åºåä»·</span> |
| | | </div> |
| | | <div class="card-head-right"> |
| | | <!-- <div class="card-head-right"> |
| | | <el-radio-group |
| | | v-model="statisticsType" |
| | | size="small" |
| | |
| | | <el-radio-button label="day">ææ¥</el-radio-button> |
| | | <el-radio-button label="month">ææ</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> --> |
| | | </div> |
| | | </template> |
| | | |
| | | <div class="filter-layout"> |
| | | <el-form :model="searchForm" :inline="true" class="filter-form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true" |
| | | class="filter-form"> |
| | | <el-form-item label="æ¶é´èå´"> |
| | | <el-date-picker |
| | | v-if="statisticsType === 'day'" |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | class="w-260" |
| | | @change="handleQuery" |
| | | /> |
| | | <el-date-picker |
| | | v-else |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="YYYY-MM" |
| | | class="w-260" |
| | | @change="handleQuery" |
| | | /> |
| | | <el-date-picker v-if="statisticsType === 'day'" |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | class="w-260" |
| | | @change="handleQuery" /> |
| | | <el-date-picker v-else |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="YYYY-MM" |
| | | class="w-260" |
| | | @change="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="产åç±»å«"> |
| | | <el-select |
| | | v-model="searchForm.category" |
| | | clearable |
| | | filterable |
| | | placeholder="å
¨é¨ç±»å«" |
| | | class="w-180" |
| | | @change="handleQuery" |
| | | > |
| | | <el-option |
| | | v-for="item in categoryOptions" |
| | | :key="item" |
| | | :label="item" |
| | | :value="item" |
| | | /> |
| | | <el-select v-model="searchForm.dictCode" |
| | | clearable |
| | | filterable |
| | | placeholder="å
¨é¨ç±»å«" |
| | | class="w-180" |
| | | @change="handleQuery"> |
| | | <el-option v-for="item in categoryOptions" |
| | | :key="item.dictCode" |
| | | :label="item.dictLabel" |
| | | :value="item.dictCode" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç产订å"> |
| | | <el-select |
| | | v-model="searchForm.orderNo" |
| | | clearable |
| | | filterable |
| | | placeholder="å
¨é¨è®¢å" |
| | | class="w-180" |
| | | @change="handleQuery" |
| | | > |
| | | <el-option |
| | | v-for="item in orderOptions" |
| | | :key="item" |
| | | :label="item" |
| | | :value="item" |
| | | /> |
| | | <el-select v-model="searchForm.productOrderId" |
| | | clearable |
| | | filterable |
| | | placeholder="å
¨é¨è®¢å" |
| | | class="w-180" |
| | | @change="handleQuery"> |
| | | <el-option v-for="order in orderList" |
| | | :key="order.id" |
| | | :label="`${order.npsNo}`" |
| | | :value="order.id" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="filter-actions"> |
| | | <el-button class="lux-btn" type="primary" @click="handleQuery"> |
| | | <el-button class="lux-btn" |
| | | type="primary" |
| | | @click="handleQuery"> |
| | | å·æ° |
| | | </el-button> |
| | | <el-button class="lux-btn" @click="handleReset">éç½®</el-button> |
| | | <el-button class="lux-btn" type="success" plain @click="handleExport"> |
| | | <el-button class="lux-btn" |
| | | @click="handleReset">éç½®</el-button> |
| | | <el-button class="lux-btn" |
| | | type="success" |
| | | plain |
| | | @click="handleExport"> |
| | | å¯¼åº |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-card class="panel-card" shadow="never"> |
| | | <el-card class="panel-card" |
| | | shadow="never"> |
| | | <div class="kpi-strip"> |
| | | <div class="kpi-item kpi-total"> |
| | | <div class="kpi-label">æ»çäº§ææ¬</div> |
| | |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-row :gutter="14" class="summary-row"> |
| | | <el-row :gutter="14" |
| | | class="summary-row"> |
| | | <el-col :span="12"> |
| | | <el-card class="table-card" shadow="never"> |
| | | <el-card class="table-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">æäº§åç±»å«æ±æ»</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="categorySummary" stripe class="lux-table" height="260"> |
| | | <el-table-column prop="category" label="产åç±»å«" min-width="140" /> |
| | | <el-table-column prop="totalQuantity" label="ç¨é" align="right" min-width="120"> |
| | | <el-table :data="categorySummary" |
| | | stripe |
| | | class="lux-table" |
| | | height="260"> |
| | | <el-table-column prop="name" |
| | | label="产åç±»å«" |
| | | min-width="140" /> |
| | | <el-table-column prop="quantity" |
| | | label="ç¨é" |
| | | align="right" |
| | | min-width="120"> |
| | | <template #default="scope"> |
| | | <span class="quantity-cell"> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.totalQuantity, 2) }}</span> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span> |
| | | <span class="quantity-unit">{{ scope.row.unit || "-" }}</span> |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCost" label="ææ¬(å
)" align="right"> |
| | | <el-table-column prop="totalCost" |
| | | label="ææ¬(å
)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span> |
| | | </template> |
| | |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card class="table-card" shadow="never"> |
| | | <el-card class="table-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">æçäº§è®¢åæ±æ»</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="orderSummary" stripe class="lux-table" height="260"> |
| | | <el-table-column prop="orderNo" label="ç产订å" min-width="150" /> |
| | | <el-table-column prop="category" label="产åç±»å«" min-width="120" /> |
| | | <el-table-column prop="totalQuantity" label="ç¨é" align="right" min-width="120"> |
| | | <el-table :data="orderSummary" |
| | | stripe |
| | | class="lux-table" |
| | | height="260"> |
| | | <el-table-column prop="name" |
| | | label="ç产订å" |
| | | min-width="150" /> |
| | | <el-table-column prop="strength" |
| | | label="产åç±»å«" |
| | | min-width="120" /> |
| | | <el-table-column prop="quantity" |
| | | label="ç¨é" |
| | | align="right" |
| | | min-width="120"> |
| | | <template #default="scope"> |
| | | <span class="quantity-cell"> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.totalQuantity, 2) }}</span> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span> |
| | | <span class="quantity-unit">{{ scope.row.unit || "-" }}</span> |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCost" label="æ»ææ¬(å
)" align="right"> |
| | | <el-table-column prop="totalCost" |
| | | label="æ»ææ¬(å
)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span> |
| | | </template> |
| | |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-card class="table-card" shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">å¤ç»´åº¦æ±æ»æç»</span> |
| | | <span class="subtle">{{ timeColumnLabel }} + 产åç±»å« + ç产订å</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <el-table :data="pagedTableData" stripe class="lux-table"> |
| | | <el-table-column prop="timeLabel" :label="timeColumnLabel" min-width="110" /> |
| | | <el-table-column prop="category" label="产åç±»å«" min-width="120" /> |
| | | <el-table-column prop="orderNo" label="ç产订å" min-width="150" /> |
| | | <el-table-column prop="totalQuantity" label="ç¨é" align="right" min-width="130"> |
| | | <template #default="scope"> |
| | | <span class="quantity-cell"> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.totalQuantity, 2) }}</span> |
| | | <span class="quantity-unit">{{ scope.row.unit || "-" }}</span> |
| | | </span> |
| | | <el-row :gutter="14" |
| | | class="summary-row"> |
| | | <el-col :span="12"> |
| | | <el-card class="table-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">产åç©æTop10</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCost" label="ææ¬(å
)" align="right"> |
| | | <template #default="scope"> |
| | | <span class="cost-value">Â¥{{ formatMoney(scope.row.totalCost) }}</span> |
| | | <div ref="topOrdersChartRef" |
| | | class="chart-container" |
| | | style="height: 300px;"></div> |
| | | </el-card> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-card class="table-card" |
| | | shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">ç产订åTop10</span> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æåæç»" width="92" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="openDetail(scope.row)">æ¥ç</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div class="pagination-container"> |
| | | <el-pagination |
| | | v-model:current-page="page.current" |
| | | v-model:page-size="page.size" |
| | | :page-sizes="[10, 20, 50, 100]" |
| | | :total="tableData.length" |
| | | layout="total, sizes, prev, pager, next, jumper" |
| | | @size-change="handleSizeChange" |
| | | @current-change="handleCurrentChange" |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-drawer |
| | | v-model="detailVisible" |
| | | :with-header="false" |
| | | class="detail-drawer" |
| | | size="760px" |
| | | :close-on-click-modal="true" |
| | | :close-on-press-escape="true" |
| | | destroy-on-close |
| | | > |
| | | <div v-if="detailRow" class="drawer-head"> |
| | | </el-card> |
| | | </el-col> |
| | | </el-row> |
| | | <el-drawer v-model="detailVisible" |
| | | :with-header="false" |
| | | class="detail-drawer" |
| | | size="760px" |
| | | :close-on-click-modal="true" |
| | | :close-on-press-escape="true" |
| | | destroy-on-close> |
| | | <div v-if="detailRow" |
| | | class="drawer-head"> |
| | | <div class="meta-item"> |
| | | <span class="meta-label">{{ timeColumnLabel }}</span> |
| | | <span class="meta-value">{{ detailRow.timeLabel }}</span> |
| | |
| | | <span class="meta-value">{{ detailRow.orderNo }}</span> |
| | | </div> |
| | | </div> |
| | | <el-table :data="detailMaterials" class="lux-table" stripe> |
| | | <el-table-column prop="materialName" label="ç©æåç§°" min-width="120" /> |
| | | <el-table-column prop="quantity" label="æå
¥é" align="right" min-width="140"> |
| | | <el-table :data="detailMaterials" |
| | | class="lux-table" |
| | | stripe> |
| | | <el-table-column prop="materialName" |
| | | label="ç©æåç§°" |
| | | min-width="120" /> |
| | | <el-table-column prop="quantity" |
| | | label="æå
¥é" |
| | | align="right" |
| | | min-width="140"> |
| | | <template #default="scope"> |
| | | <span class="quantity-cell"> |
| | | <span class="quantity-value">{{ formatNumber(scope.row.quantity, 2) }}</span> |
| | |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitPrice" label="åä»·(å
)" align="right"> |
| | | <el-table-column prop="unitPrice" |
| | | label="åä»·(å
)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | {{ formatNumber(scope.row.unitPrice, 2) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="cost" label="ææ¬(å
)" align="right" min-width="132"> |
| | | <el-table-column prop="cost" |
| | | label="ææ¬(å
)" |
| | | align="right" |
| | | min-width="132"> |
| | | <template #default="scope"> |
| | | <span class="cost-value no-wrap-money">Â¥{{ formatMoney(scope.row.cost) }}</span> |
| | | </template> |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref, watch } from "vue"; |
| | | import { DataLine } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { computed, reactive, ref, watch, onMounted, nextTick } from "vue"; |
| | | import { DataLine } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import * as echarts from "echarts"; |
| | | import { getDicts } from "@/api/system/dict/data.js"; |
| | | import { productOrderListPage } from "@/api/productionManagement/productionOrder.js"; |
| | | import { |
| | | getProductionCostSummary, |
| | | getProductionCostAggregateByProduct, |
| | | getProductionCostAggregateByOrder, |
| | | getProductionCostTopOrders, |
| | | } from "@/api/costAccounting/productionCost.js"; |
| | | |
| | | const statisticsType = ref("day"); |
| | | const statisticsType = ref("day"); |
| | | |
| | | const getDefaultDateRange = () => { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setDate(start.getDate() - 6); |
| | | return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)]; |
| | | }; |
| | | |
| | | const getDefaultMonthRange = () => { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setMonth(start.getMonth() - 2); |
| | | return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)]; |
| | | }; |
| | | |
| | | const searchForm = reactive({ |
| | | dateRange: getDefaultDateRange(), |
| | | monthRange: getDefaultMonthRange(), |
| | | category: "", |
| | | orderNo: "", |
| | | }); |
| | | |
| | | const sourceRecords = ref([ |
| | | { date: "2026-03-17", category: "ç·ç ", orderNo: "PO-260317-01", materialName: "é¶ç·ç²", materialType: "åæ", quantity: 1200, unit: "kg", unitPrice: 2.8 }, |
| | | { date: "2026-03-17", category: "ç·ç ", orderNo: "PO-260317-01", materialName: "éæ", materialType: "è¾
æ", quantity: 180, unit: "kg", unitPrice: 8.6 }, |
| | | { date: "2026-03-17", category: "æ°´æ³¥", orderNo: "PO-260317-02", materialName: "çæ", materialType: "åæ", quantity: 2200, unit: "kg", unitPrice: 1.36 }, |
| | | { date: "2026-03-17", category: "æ°´æ³¥", orderNo: "PO-260317-02", materialName: "ç³è", materialType: "è¾
æ", quantity: 260, unit: "kg", unitPrice: 0.92 }, |
| | | { date: "2026-03-18", category: "ç æµ", orderNo: "PO-260318-01", materialName: "æºå¶ç ", materialType: "åæ", quantity: 1600, unit: "kg", unitPrice: 0.58 }, |
| | | { date: "2026-03-18", category: "ç æµ", orderNo: "PO-260318-01", materialName: "ä¿æ°´å", materialType: "è¾
æ", quantity: 65, unit: "kg", unitPrice: 11.4 }, |
| | | { date: "2026-03-19", category: "ç·ç ", orderNo: "PO-260319-01", materialName: "é¶ç·ç²", materialType: "åæ", quantity: 980, unit: "kg", unitPrice: 2.9 }, |
| | | { date: "2026-03-19", category: "ç·ç ", orderNo: "PO-260319-01", materialName: "è²æ", materialType: "è¾
æ", quantity: 42, unit: "kg", unitPrice: 15.8 }, |
| | | { date: "2026-03-19", category: "ç æµ", orderNo: "PO-260319-03", materialName: "æºå¶ç ", materialType: "åæ", quantity: 1400, unit: "kg", unitPrice: 0.56 }, |
| | | { date: "2026-03-19", category: "ç æµ", orderNo: "PO-260319-03", materialName: "åæ°´å", materialType: "è¾
æ", quantity: 74, unit: "kg", unitPrice: 7.2 }, |
| | | { date: "2026-03-20", category: "æ°´æ³¥", orderNo: "PO-260320-02", materialName: "çæ", materialType: "åæ", quantity: 2400, unit: "kg", unitPrice: 1.33 }, |
| | | { date: "2026-03-20", category: "æ°´æ³¥", orderNo: "PO-260320-02", materialName: "ç¿ç²", materialType: "è¾
æ", quantity: 380, unit: "kg", unitPrice: 1.08 }, |
| | | ]); |
| | | |
| | | const normalizedRecords = computed(() => |
| | | sourceRecords.value.map((item) => { |
| | | const month = item.date.slice(0, 7); |
| | | const cost = Number(item.quantity) * Number(item.unitPrice); |
| | | return { ...item, month, cost }; |
| | | }) |
| | | ); |
| | | |
| | | const categoryOptions = computed(() => |
| | | Array.from(new Set(normalizedRecords.value.map((item) => item.category))) |
| | | ); |
| | | |
| | | const orderOptions = computed(() => |
| | | Array.from(new Set(normalizedRecords.value.map((item) => item.orderNo))) |
| | | ); |
| | | |
| | | 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 getMonthRangeDays = (monthRange) => { |
| | | if (!Array.isArray(monthRange) || monthRange.length !== 2 || !monthRange[0] || !monthRange[1]) { |
| | | return 0; |
| | | } |
| | | const [startMonth, endMonth] = monthRange; |
| | | const startDate = new Date(`${startMonth}-01T00:00:00`); |
| | | const endDate = new Date(`${endMonth}-01T00:00:00`); |
| | | if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime()) || startDate > endDate) { |
| | | return 0; |
| | | } |
| | | const endMonthLastDay = new Date(endDate.getFullYear(), endDate.getMonth() + 1, 0); |
| | | const diffMs = endMonthLastDay.getTime() - startDate.getTime(); |
| | | return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1; |
| | | }; |
| | | |
| | | const buildQueryParams = () => { |
| | | const isDay = statisticsType.value === "day"; |
| | | const params = { |
| | | statisticsType: statisticsType.value, |
| | | category: searchForm.category || undefined, |
| | | orderNo: searchForm.orderNo || undefined, |
| | | const getDefaultDateRange = () => { |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setDate(start.getDate() - 6); |
| | | return [start.toISOString().slice(0, 10), end.toISOString().slice(0, 10)]; |
| | | }; |
| | | |
| | | if (isDay) { |
| | | const [startDate, endDate] = searchForm.dateRange || []; |
| | | params.startDate = startDate; |
| | | params.endDate = endDate; |
| | | } else { |
| | | const [startMonth, endMonth] = searchForm.monthRange || []; |
| | | params.startMonth = startMonth; |
| | | params.endMonth = endMonth; |
| | | params.days = getMonthRangeDays(searchForm.monthRange); |
| | | } |
| | | const getDefaultMonthRange = () => { |
| | | 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 params; |
| | | }; |
| | | const searchForm = reactive({ |
| | | dateRange: getDefaultDateRange(), |
| | | monthRange: getDefaultMonthRange(), |
| | | dictCode: "", |
| | | productOrderId: "", |
| | | }); |
| | | |
| | | const filteredRecords = computed(() => |
| | | normalizedRecords.value.filter((item) => { |
| | | const hitTime = |
| | | statisticsType.value === "day" |
| | | ? inRange(item.date, searchForm.dateRange) |
| | | : inRange(item.month, searchForm.monthRange); |
| | | const hitCategory = !searchForm.category || item.category === searchForm.category; |
| | | const hitOrder = !searchForm.orderNo || item.orderNo === searchForm.orderNo; |
| | | return hitTime && hitCategory && hitOrder; |
| | | }) |
| | | ); |
| | | const sourceRecords = ref([ |
| | | { |
| | | date: "2026-03-17", |
| | | category: "ç·ç ", |
| | | orderNo: "PO-260317-01", |
| | | materialName: "é¶ç·ç²", |
| | | materialType: "åæ", |
| | | quantity: 1200, |
| | | unit: "kg", |
| | | unitPrice: 2.8, |
| | | }, |
| | | { |
| | | date: "2026-03-17", |
| | | category: "ç·ç ", |
| | | orderNo: "PO-260317-01", |
| | | materialName: "éæ", |
| | | materialType: "è¾
æ", |
| | | quantity: 180, |
| | | unit: "kg", |
| | | unitPrice: 8.6, |
| | | }, |
| | | { |
| | | date: "2026-03-17", |
| | | category: "æ°´æ³¥", |
| | | orderNo: "PO-260317-02", |
| | | materialName: "çæ", |
| | | materialType: "åæ", |
| | | quantity: 2200, |
| | | unit: "kg", |
| | | unitPrice: 1.36, |
| | | }, |
| | | { |
| | | date: "2026-03-17", |
| | | category: "æ°´æ³¥", |
| | | orderNo: "PO-260317-02", |
| | | materialName: "ç³è", |
| | | materialType: "è¾
æ", |
| | | quantity: 260, |
| | | unit: "kg", |
| | | unitPrice: 0.92, |
| | | }, |
| | | { |
| | | date: "2026-03-18", |
| | | category: "ç æµ", |
| | | orderNo: "PO-260318-01", |
| | | materialName: "æºå¶ç ", |
| | | materialType: "åæ", |
| | | quantity: 1600, |
| | | unit: "kg", |
| | | unitPrice: 0.58, |
| | | }, |
| | | { |
| | | date: "2026-03-18", |
| | | category: "ç æµ", |
| | | orderNo: "PO-260318-01", |
| | | materialName: "ä¿æ°´å", |
| | | materialType: "è¾
æ", |
| | | quantity: 65, |
| | | unit: "kg", |
| | | unitPrice: 11.4, |
| | | }, |
| | | { |
| | | date: "2026-03-19", |
| | | category: "ç·ç ", |
| | | orderNo: "PO-260319-01", |
| | | materialName: "é¶ç·ç²", |
| | | materialType: "åæ", |
| | | quantity: 980, |
| | | unit: "kg", |
| | | unitPrice: 2.9, |
| | | }, |
| | | { |
| | | date: "2026-03-19", |
| | | category: "ç·ç ", |
| | | orderNo: "PO-260319-01", |
| | | materialName: "è²æ", |
| | | materialType: "è¾
æ", |
| | | quantity: 42, |
| | | unit: "kg", |
| | | unitPrice: 15.8, |
| | | }, |
| | | { |
| | | date: "2026-03-19", |
| | | category: "ç æµ", |
| | | orderNo: "PO-260319-03", |
| | | materialName: "æºå¶ç ", |
| | | materialType: "åæ", |
| | | quantity: 1400, |
| | | unit: "kg", |
| | | unitPrice: 0.56, |
| | | }, |
| | | { |
| | | date: "2026-03-19", |
| | | category: "ç æµ", |
| | | orderNo: "PO-260319-03", |
| | | materialName: "åæ°´å", |
| | | materialType: "è¾
æ", |
| | | quantity: 74, |
| | | unit: "kg", |
| | | unitPrice: 7.2, |
| | | }, |
| | | { |
| | | date: "2026-03-20", |
| | | category: "æ°´æ³¥", |
| | | orderNo: "PO-260320-02", |
| | | materialName: "çæ", |
| | | materialType: "åæ", |
| | | quantity: 2400, |
| | | unit: "kg", |
| | | unitPrice: 1.33, |
| | | }, |
| | | { |
| | | date: "2026-03-20", |
| | | category: "æ°´æ³¥", |
| | | orderNo: "PO-260320-02", |
| | | materialName: "ç¿ç²", |
| | | materialType: "è¾
æ", |
| | | quantity: 380, |
| | | unit: "kg", |
| | | unitPrice: 1.08, |
| | | }, |
| | | ]); |
| | | const orderList = ref([]); |
| | | |
| | | const timeColumnLabel = computed(() => (statisticsType.value === "day" ? "æ¥æ" : "æä»½")); |
| | | // å è½½ç产订åå表 |
| | | const loadOrders = () => { |
| | | productOrderListPage({ pageNum: -1, pageSize: -1 }) |
| | | .then(res => { |
| | | orderList.value = res.data.records || []; |
| | | }) |
| | | .finally(() => {}); |
| | | }; |
| | | const normalizedRecords = computed(() => |
| | | sourceRecords.value.map(item => { |
| | | const month = item.date.slice(0, 7); |
| | | const cost = Number(item.quantity) * Number(item.unitPrice); |
| | | return { ...item, month, cost }; |
| | | }) |
| | | ); |
| | | |
| | | const aggregateBy = (list, keyFn) => { |
| | | const map = new Map(); |
| | | for (const item of list) { |
| | | const key = keyFn(item); |
| | | if (!map.has(key)) { |
| | | map.set(key, { |
| | | totalCost: 0, |
| | | totalQuantity: 0, |
| | | materials: [], |
| | | const categoryOptions = ref([]); |
| | | // è·å产åç±»ååå
¸ |
| | | const getProductTypeOptions = () => { |
| | | getDicts("product_type") |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | categoryOptions.value = res.data; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·å产åç±»ååå
¸å¤±è´¥ï¼", err); |
| | | }); |
| | | }; |
| | | |
| | | const orderOptions = computed(() => |
| | | Array.from(new Set(normalizedRecords.value.map(item => item.orderNo))) |
| | | ); |
| | | |
| | | 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 getMonthRangeDays = monthRange => { |
| | | if ( |
| | | !Array.isArray(monthRange) || |
| | | monthRange.length !== 2 || |
| | | !monthRange[0] || |
| | | !monthRange[1] |
| | | ) { |
| | | return 0; |
| | | } |
| | | const [startMonth, endMonth] = monthRange; |
| | | const startDate = new Date(`${startMonth}-01T00:00:00`); |
| | | const endDate = new Date(`${endMonth}-01T00:00:00`); |
| | | if ( |
| | | Number.isNaN(startDate.getTime()) || |
| | | Number.isNaN(endDate.getTime()) || |
| | | startDate > endDate |
| | | ) { |
| | | return 0; |
| | | } |
| | | const endMonthLastDay = new Date( |
| | | endDate.getFullYear(), |
| | | endDate.getMonth() + 1, |
| | | 0 |
| | | ); |
| | | const diffMs = endMonthLastDay.getTime() - startDate.getTime(); |
| | | return Math.floor(diffMs / (24 * 60 * 60 * 1000)) + 1; |
| | | }; |
| | | |
| | | const buildQueryParams = () => { |
| | | const isDay = statisticsType.value === "day"; |
| | | const params = { |
| | | statisticsType: statisticsType.value, |
| | | dictCode: searchForm.dictCode || undefined, |
| | | productOrderId: searchForm.productOrderId || undefined, |
| | | }; |
| | | |
| | | if (isDay) { |
| | | const [startDate, endDate] = searchForm.dateRange || []; |
| | | params.startDate = startDate; |
| | | params.endDate = endDate; |
| | | } else { |
| | | const [startMonth, endMonth] = searchForm.monthRange || []; |
| | | params.startMonth = startMonth; |
| | | params.endMonth = endMonth; |
| | | params.days = getMonthRangeDays(searchForm.monthRange); |
| | | } |
| | | |
| | | return params; |
| | | }; |
| | | |
| | | const filteredRecords = computed(() => |
| | | normalizedRecords.value.filter(item => { |
| | | const hitTime = |
| | | statisticsType.value === "day" |
| | | ? inRange(item.date, searchForm.dateRange) |
| | | : inRange(item.month, searchForm.monthRange); |
| | | const hitCategory = |
| | | !searchForm.dictCode || item.dictCode === searchForm.dictCode; |
| | | const hitOrder = |
| | | !searchForm.productOrderId || |
| | | item.productOrderId === searchForm.productOrderId; |
| | | return hitTime && hitCategory && hitOrder; |
| | | }) |
| | | ); |
| | | |
| | | const timeColumnLabel = computed(() => |
| | | statisticsType.value === "day" ? "æ¥æ" : "æä»½" |
| | | ); |
| | | |
| | | const aggregateBy = (list, keyFn) => { |
| | | const map = new Map(); |
| | | for (const item of list) { |
| | | const key = keyFn(item); |
| | | if (!map.has(key)) { |
| | | map.set(key, { |
| | | totalCost: 0, |
| | | totalQuantity: 0, |
| | | materials: [], |
| | | }); |
| | | } |
| | | const bucket = map.get(key); |
| | | bucket.totalCost += item.cost; |
| | | bucket.totalQuantity += Number(item.quantity) || 0; |
| | | bucket.materials.push(item); |
| | | } |
| | | return map; |
| | | }; |
| | | |
| | | const groupedMap = computed(() => |
| | | aggregateBy(filteredRecords.value, item => { |
| | | const timeKey = statisticsType.value === "day" ? item.date : item.month; |
| | | return `${timeKey}__${item.category}__${item.orderNo}`; |
| | | }) |
| | | ); |
| | | |
| | | const tableData = computed(() => { |
| | | const rows = []; |
| | | for (const [key, val] of groupedMap.value) { |
| | | const [timeLabel, category, orderNo] = key.split("__"); |
| | | rows.push({ |
| | | key, |
| | | timeLabel, |
| | | category, |
| | | orderNo, |
| | | totalQuantity: val.totalQuantity, |
| | | unit: val.materials[0]?.unit || "", |
| | | totalCost: val.totalCost, |
| | | materials: val.materials, |
| | | }); |
| | | } |
| | | const bucket = map.get(key); |
| | | bucket.totalCost += item.cost; |
| | | bucket.totalQuantity += Number(item.quantity) || 0; |
| | | bucket.materials.push(item); |
| | | } |
| | | return map; |
| | | }; |
| | | return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1)); |
| | | }); |
| | | |
| | | const groupedMap = computed(() => |
| | | aggregateBy(filteredRecords.value, (item) => { |
| | | const timeKey = statisticsType.value === "day" ? item.date : item.month; |
| | | return `${timeKey}__${item.category}__${item.orderNo}`; |
| | | }) |
| | | ); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | |
| | | const tableData = computed(() => { |
| | | const rows = []; |
| | | for (const [key, val] of groupedMap.value) { |
| | | const [timeLabel, category, orderNo] = key.split("__"); |
| | | rows.push({ |
| | | key, |
| | | timeLabel, |
| | | category, |
| | | orderNo, |
| | | totalQuantity: val.totalQuantity, |
| | | unit: val.materials[0]?.unit || "", |
| | | totalCost: val.totalCost, |
| | | materials: val.materials, |
| | | }); |
| | | } |
| | | return rows.sort((a, b) => (a.timeLabel > b.timeLabel ? -1 : 1)); |
| | | }); |
| | | const pagedTableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return tableData.value.slice(start, start + page.size); |
| | | }); |
| | | |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | }); |
| | | const categorySummary = ref([]); |
| | | const orderSummary = ref([]); |
| | | const topOrders = ref([]); |
| | | |
| | | const pagedTableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return tableData.value.slice(start, start + page.size); |
| | | }); |
| | | const overview = ref({ |
| | | totalCost: 0, |
| | | orderCount: 0, |
| | | avgCostPerOrder: 0, |
| | | }); |
| | | |
| | | const categorySummary = computed(() => { |
| | | const map = aggregateBy(filteredRecords.value, (item) => item.category); |
| | | const rows = []; |
| | | for (const [category, val] of map) { |
| | | rows.push({ |
| | | category, |
| | | totalQuantity: val.totalQuantity, |
| | | unit: val.materials[0]?.unit || "", |
| | | totalCost: val.totalCost, |
| | | }); |
| | | } |
| | | return rows.sort((a, b) => b.totalCost - a.totalCost); |
| | | }); |
| | | // å¾è¡¨ç¸å
³ |
| | | const topOrdersChartRef = ref(null); |
| | | let topOrdersChartInstance = null; |
| | | |
| | | const orderSummary = computed(() => { |
| | | const map = aggregateBy(filteredRecords.value, (item) => item.orderNo); |
| | | const rows = []; |
| | | for (const [orderNo, val] of map) { |
| | | rows.push({ |
| | | orderNo, |
| | | category: val.materials[0]?.category || "-", |
| | | totalQuantity: val.totalQuantity, |
| | | unit: val.materials[0]?.unit || "", |
| | | totalCost: val.totalCost, |
| | | }); |
| | | } |
| | | return rows.sort((a, b) => b.totalCost - a.totalCost); |
| | | }); |
| | | const detailVisible = ref(false); |
| | | const detailRow = ref(null); |
| | | |
| | | const overview = computed(() => { |
| | | const orderCount = new Set(filteredRecords.value.map((item) => item.orderNo)).size; |
| | | const totalCost = filteredRecords.value.reduce((sum, item) => sum + item.cost, 0); |
| | | return { |
| | | totalCost, |
| | | orderCount, |
| | | avgCostPerOrder: orderCount === 0 ? 0 : totalCost / orderCount, |
| | | }; |
| | | }); |
| | | const detailMaterials = computed(() => detailRow.value?.materials || []); |
| | | |
| | | const detailVisible = ref(false); |
| | | const detailRow = ref(null); |
| | | |
| | | const detailMaterials = computed(() => detailRow.value?.materials || []); |
| | | |
| | | const detailTotalCost = computed(() => |
| | | detailMaterials.value.reduce((sum, item) => sum + item.cost, 0) |
| | | ); |
| | | |
| | | const openDetail = (row) => { |
| | | detailRow.value = row; |
| | | detailVisible.value = true; |
| | | }; |
| | | |
| | | const handleTypeChange = () => { |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | const queryParams = buildQueryParams(); |
| | | console.log("[productionCostAccounting] query params:", queryParams); |
| | | ElMessage.success("å·²ææ¡ä»¶å®ææ±æ»"); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.dateRange = getDefaultDateRange(); |
| | | searchForm.monthRange = getDefaultMonthRange(); |
| | | searchForm.category = ""; |
| | | searchForm.orderNo = ""; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const handleSizeChange = (val) => { |
| | | page.size = val; |
| | | page.current = 1; |
| | | }; |
| | | |
| | | const handleCurrentChange = (val) => { |
| | | page.current = val; |
| | | }; |
| | | |
| | | const handleExport = () => { |
| | | const headers = [timeColumnLabel.value, "产åç±»å«", "ç产订å", "ç¨é", "åä½", "ææ¬(å
)"]; |
| | | const lines = tableData.value.map((row) => |
| | | [ |
| | | row.timeLabel, |
| | | row.category, |
| | | row.orderNo, |
| | | row.totalQuantity.toFixed(2), |
| | | row.unit || "", |
| | | row.totalCost.toFixed(2), |
| | | ].join(",") |
| | | const detailTotalCost = computed(() => |
| | | detailMaterials.value.reduce((sum, item) => sum + item.cost, 0) |
| | | ); |
| | | const csv = [headers.join(","), ...lines].join("\n"); |
| | | const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" }); |
| | | const url = URL.createObjectURL(blob); |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.download = `çäº§ææ¬æ±æ»_${statisticsType.value}_${Date.now()}.csv`; |
| | | link.click(); |
| | | URL.revokeObjectURL(url); |
| | | ElMessage.success("å¯¼åºæå"); |
| | | }; |
| | | |
| | | const formatMoney = (v) => { |
| | | const n = Number.parseFloat(v); |
| | | const value = Number.isFinite(n) ? n : 0; |
| | | return value.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | const openDetail = row => { |
| | | detailRow.value = row; |
| | | detailVisible.value = true; |
| | | }; |
| | | |
| | | // åå§åç产订åTop10å¾è¡¨ |
| | | const initTopOrdersChart = () => { |
| | | nextTick(() => { |
| | | if (topOrdersChartRef.value) { |
| | | topOrdersChartInstance = echarts.init(topOrdersChartRef.value); |
| | | updateTopOrdersChart(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // æ´æ°ç产订åTop10å¾è¡¨ |
| | | const updateTopOrdersChart = () => { |
| | | if (!topOrdersChartInstance) return; |
| | | |
| | | const data = topOrders.value; |
| | | const xAxisData = data.map(item => item.name); |
| | | const seriesData = data.map(item => item.totalCost); |
| | | |
| | | const option = { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | backgroundColor: "rgba(255, 255, 255, 0.95)", |
| | | borderColor: "#409EFF", |
| | | borderWidth: 1, |
| | | textStyle: { color: "#303133" }, |
| | | }, |
| | | grid: { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "15%", |
| | | top: "3%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: xAxisData, |
| | | axisLabel: { |
| | | color: "#606266", |
| | | rotate: 45, |
| | | }, |
| | | axisLine: { lineStyle: { color: "#ebeef5" } }, |
| | | splitLine: { show: false }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "ææ¬(å
)", |
| | | nameTextStyle: { color: "#606266" }, |
| | | axisLabel: { color: "#606266" }, |
| | | axisLine: { show: false }, |
| | | splitLine: { lineStyle: { color: "#f0f2f5" } }, |
| | | }, |
| | | series: [ |
| | | { |
| | | name: "ææ¬", |
| | | type: "bar", |
| | | data: seriesData, |
| | | itemStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#409EFF" }, |
| | | { offset: 1, color: "#66B1FF" }, |
| | | ]), |
| | | }, |
| | | barWidth: "60%", |
| | | }, |
| | | ], |
| | | }; |
| | | |
| | | topOrdersChartInstance.setOption(option); |
| | | }; |
| | | |
| | | // çªå£å¤§å°ååæ¶éæ°æ¸²æå¾è¡¨ |
| | | const handleResize = () => { |
| | | topOrdersChartInstance && topOrdersChartInstance.resize(); |
| | | }; |
| | | |
| | | const handleTypeChange = () => { |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | // æå»ºAPI请æ±åæ° |
| | | const apiParams = { |
| | | startDate: searchForm.dateRange?.[0], |
| | | endDate: searchForm.dateRange?.[1], |
| | | dictCode: searchForm.dictCode, |
| | | productOrderId: searchForm.productOrderId, |
| | | current: -1, |
| | | size: -1, |
| | | }; |
| | | |
| | | // è°ç¨APIè·åæ¦è§æ°æ® |
| | | getProductionCostSummary(apiParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | const data = res.data; |
| | | overview.value = { |
| | | totalCost: parseFloat(data.totalCost) || 0, |
| | | orderCount: data.orderCount || 0, |
| | | avgCostPerOrder: parseFloat(data.averageOrderCost) || 0, |
| | | }; |
| | | } else { |
| | | ElMessage.error(res.message || "è·åæ¦è§æ°æ®å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åçäº§ææ¬æ±æ»æ°æ®å¤±è´¥ï¼", err); |
| | | ElMessage.error("ç³»ç»å¼å¸¸ï¼è·åæ¦è§æ°æ®å¤±è´¥"); |
| | | }); |
| | | getProductionCostAggregateByOrder; |
| | | // è°ç¨APIè·åæäº§åç©ææ±æ»æ°æ® |
| | | getProductionCostAggregateByProduct(apiParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | // æç©æåç§°åç»è®¡ç® |
| | | |
| | | // è¿éç®åå¤çï¼orderSummaryææ¶ä½¿ç¨ç¸åçæ°æ® |
| | | // å®é
项ç®ä¸å¯è½éè¦è°ç¨ä¸é¨çAPIè·åæè®¢åæ±æ»çæ°æ® |
| | | categorySummary.value = res.data.records || []; |
| | | } else { |
| | | ElMessage.error(res.message || "è·åç©ææ±æ»æ°æ®å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åæäº§åç©ææ±æ»æ°æ®å¤±è´¥ï¼", err); |
| | | ElMessage.error("ç³»ç»å¼å¸¸ï¼è·åç©ææ±æ»æ°æ®å¤±è´¥"); |
| | | }); |
| | | getProductionCostAggregateByOrder(apiParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | // æç©æåç§°åç»è®¡ç® |
| | | |
| | | // è¿éç®åå¤çï¼orderSummaryææ¶ä½¿ç¨ç¸åçæ°æ® |
| | | // å®é
项ç®ä¸å¯è½éè¦è°ç¨ä¸é¨çAPIè·åæè®¢åæ±æ»çæ°æ® |
| | | orderSummary.value = res.data.records || []; |
| | | } else { |
| | | ElMessage.error(res.message || "è·åè®¢åæ±æ»æ°æ®å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åæè®¢åæ±æ»æ°æ®å¤±è´¥ï¼", err); |
| | | ElMessage.error("ç³»ç»å¼å¸¸ï¼è·åè®¢åæ±æ»æ°æ®å¤±è´¥"); |
| | | }); |
| | | |
| | | // è°ç¨APIè·åç产订åTop10æ°æ® |
| | | getProductionCostTopOrders(apiParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | topOrders.value = res.data || []; |
| | | updateTopOrdersChart(); |
| | | } else { |
| | | ElMessage.error(res.message || "è·åç产订åTop10æ°æ®å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åç产订åTop10æ°æ®å¤±è´¥ï¼", err); |
| | | ElMessage.error("ç³»ç»å¼å¸¸ï¼è·åç产订åTop10æ°æ®å¤±è´¥"); |
| | | }); |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.dateRange = getDefaultDateRange(); |
| | | searchForm.monthRange = getDefaultMonthRange(); |
| | | searchForm.dictCode = ""; |
| | | searchForm.productOrderId = ""; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const handleSizeChange = val => { |
| | | page.size = val; |
| | | page.current = 1; |
| | | }; |
| | | |
| | | const handleCurrentChange = val => { |
| | | page.current = val; |
| | | }; |
| | | onMounted(() => { |
| | | getProductTypeOptions(); |
| | | loadOrders(); |
| | | handleQuery(); |
| | | initTopOrdersChart(); |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | }; |
| | | |
| | | const formatNumber = (v, digits = 2) => { |
| | | const n = Number.parseFloat(v); |
| | | if (!Number.isFinite(n)) return "--"; |
| | | return n.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: digits, |
| | | maximumFractionDigits: digits, |
| | | const handleExport = () => { |
| | | const headers = [ |
| | | timeColumnLabel.value, |
| | | "产åç±»å«", |
| | | "ç产订å", |
| | | "ç¨é", |
| | | "åä½", |
| | | "ææ¬(å
)", |
| | | ]; |
| | | const lines = tableData.value.map(row => |
| | | [ |
| | | row.timeLabel, |
| | | row.category, |
| | | row.orderNo, |
| | | row.totalQuantity.toFixed(2), |
| | | row.unit || "", |
| | | row.totalCost.toFixed(2), |
| | | ].join(",") |
| | | ); |
| | | const csv = [headers.join(","), ...lines].join("\n"); |
| | | const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" }); |
| | | const url = URL.createObjectURL(blob); |
| | | const link = document.createElement("a"); |
| | | link.href = url; |
| | | link.download = `çäº§ææ¬æ±æ»_${statisticsType.value}_${Date.now()}.csv`; |
| | | link.click(); |
| | | URL.revokeObjectURL(url); |
| | | ElMessage.success("å¯¼åºæå"); |
| | | }; |
| | | |
| | | const formatMoney = v => { |
| | | const n = Number.parseFloat(v); |
| | | const value = Number.isFinite(n) ? n : 0; |
| | | return value.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | }; |
| | | |
| | | const formatNumber = (v, digits = 2) => { |
| | | const n = Number.parseFloat(v); |
| | | if (!Number.isFinite(n)) return "--"; |
| | | return n.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: digits, |
| | | maximumFractionDigits: digits, |
| | | }); |
| | | }; |
| | | |
| | | watch(tableData, () => { |
| | | const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size)); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }); |
| | | }; |
| | | |
| | | watch(tableData, () => { |
| | | const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size)); |
| | | if (page.current > maxPage) page.current = maxPage; |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .production-cost-page { |
| | | --lux-bg: #f6f7fb; |
| | | --lux-card: rgba(255, 255, 255, 0.86); |
| | | --lux-border: rgba(15, 23, 42, 0.08); |
| | | --lux-text: rgba(15, 23, 42, 0.92); |
| | | --lux-subtle: rgba(15, 23, 42, 0.58); |
| | | --lux-muted: rgba(15, 23, 42, 0.38); |
| | | --lux-primary: #2f6fed; |
| | | --lux-success: #16a34a; |
| | | --lux-warning: #f59e0b; |
| | | --lux-danger: #ef4444; |
| | | --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); |
| | | --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06); |
| | | --lux-radius: 14px; |
| | | .production-cost-page { |
| | | --lux-bg: #f6f7fb; |
| | | --lux-card: rgba(255, 255, 255, 0.86); |
| | | --lux-border: rgba(15, 23, 42, 0.08); |
| | | --lux-text: rgba(15, 23, 42, 0.92); |
| | | --lux-subtle: rgba(15, 23, 42, 0.58); |
| | | --lux-muted: rgba(15, 23, 42, 0.38); |
| | | --lux-primary: #2f6fed; |
| | | --lux-success: #16a34a; |
| | | --lux-warning: #f59e0b; |
| | | --lux-danger: #ef4444; |
| | | --lux-shadow: 0 18px 50px rgba(15, 23, 42, 0.08); |
| | | --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06); |
| | | --lux-radius: 14px; |
| | | |
| | | padding: 18px 22px 24px; |
| | | background: radial-gradient( |
| | | 1200px 420px at 20% 0%, |
| | | rgba(47, 111, 237, 0.1), |
| | | transparent 55% |
| | | ), |
| | | linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%); |
| | | } |
| | | padding: 18px 22px 24px; |
| | | background: radial-gradient( |
| | | 1200px 420px at 20% 0%, |
| | | rgba(47, 111, 237, 0.1), |
| | | transparent 55% |
| | | ), |
| | | linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%); |
| | | } |
| | | |
| | | .filter-card, |
| | | .panel-card, |
| | | .table-card { |
| | | border-radius: var(--lux-radius); |
| | | border-color: var(--lux-border); |
| | | background: var(--lux-card); |
| | | box-shadow: var(--lux-shadow-soft); |
| | | } |
| | | .filter-card, |
| | | .panel-card, |
| | | .table-card { |
| | | border-radius: var(--lux-radius); |
| | | border-color: var(--lux-border); |
| | | background: var(--lux-card); |
| | | box-shadow: var(--lux-shadow-soft); |
| | | } |
| | | |
| | | .filter-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | .filter-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .panel-card, |
| | | .summary-row { |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .filter-layout { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .filter-form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px 14px; |
| | | } |
| | | |
| | | .filter-form :deep(.el-form-item) { |
| | | margin: 0; |
| | | } |
| | | |
| | | .filter-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .card-head, |
| | | .panel-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .card-head-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .card-head-right { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .card-icon { |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .card-title { |
| | | font-weight: 760; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .subtle { |
| | | color: var(--lux-subtle); |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .kpi-strip { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | .kpi-item { |
| | | padding: 12px 14px; |
| | | border-radius: 12px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | } |
| | | |
| | | .kpi-total { |
| | | background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | |
| | | .kpi-raw { |
| | | background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | |
| | | .kpi-avg { |
| | | background: linear-gradient(135deg, rgba(99, 102, 241, 0.14), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | |
| | | .kpi-order { |
| | | background: linear-gradient(135deg, rgba(100, 116, 139, 0.1), rgba(255, 255, 255, 0.86)); |
| | | } |
| | | |
| | | .kpi-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .kpi-value { |
| | | font-size: 22px; |
| | | margin-top: 6px; |
| | | font-weight: 780; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .price-value { |
| | | font-weight: 700; |
| | | color: var(--lux-success); |
| | | } |
| | | |
| | | .cost-value { |
| | | font-weight: 700; |
| | | color: var(--lux-danger); |
| | | } |
| | | |
| | | .no-wrap-money { |
| | | display: inline-block; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .drawer-head { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .meta-item { |
| | | padding: 10px 12px; |
| | | border-radius: 10px; |
| | | border: 1px solid var(--lux-border); |
| | | background: rgba(15, 23, 42, 0.03); |
| | | display: grid; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .meta-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .meta-value { |
| | | color: var(--lux-text); |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .material-type-tag { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 46px; |
| | | height: 24px; |
| | | padding: 0 8px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .material-type-tag.is-raw { |
| | | color: #15803d; |
| | | background: rgba(22, 163, 74, 0.12); |
| | | } |
| | | |
| | | .material-type-tag.is-aux { |
| | | color: #b45309; |
| | | background: rgba(245, 158, 11, 0.16); |
| | | } |
| | | |
| | | .quantity-value { |
| | | font-weight: 700; |
| | | color: var(--lux-text); |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .quantity-cell { |
| | | display: inline-flex; |
| | | align-items: baseline; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .quantity-unit { |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .drawer-foot { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 12px; |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px dashed var(--lux-border); |
| | | } |
| | | |
| | | .foot-item { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 7px 16px; |
| | | border-radius: 999px; |
| | | border: 1px solid var(--lux-border); |
| | | background: #fff; |
| | | } |
| | | |
| | | .foot-label { |
| | | color: var(--lux-subtle); |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .foot-value { |
| | | color: var(--lux-text); |
| | | font-weight: 700; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .foot-item.total { |
| | | border-color: rgba(47, 111, 237, 0.26); |
| | | background: rgba(47, 111, 237, 0.08); |
| | | } |
| | | |
| | | .foot-item.total .foot-value { |
| | | color: #1e3a8a; |
| | | font-size: 18px; |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .pagination-container { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-top: 12px; |
| | | } |
| | | |
| | | .strong { |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .w-260 { |
| | | width: 260px; |
| | | } |
| | | |
| | | .w-180 { |
| | | width: 180px; |
| | | } |
| | | |
| | | ::deep(.lux-table) { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | ::deep(.lux-table th.el-table__cell) { |
| | | background: rgba(15, 23, 42, 0.03); |
| | | } |
| | | |
| | | ::deep(.lux-table .el-table__row:hover > td.el-table__cell) { |
| | | background-color: rgba(47, 111, 237, 0.06) !important; |
| | | } |
| | | |
| | | @media (max-width: 1100px) { |
| | | .kpi-strip { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | .panel-card, |
| | | .summary-row { |
| | | margin-bottom: 14px; |
| | | } |
| | | |
| | | .filter-layout { |
| | | flex-direction: column; |
| | | display: flex; |
| | | align-items: flex-start; |
| | | justify-content: space-between; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .filter-form { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 10px 14px; |
| | | } |
| | | |
| | | .filter-form :deep(.el-form-item) { |
| | | margin: 0; |
| | | } |
| | | |
| | | .filter-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .card-head, |
| | | .panel-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .card-head-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .card-head-right { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .card-icon { |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .card-title { |
| | | font-weight: 760; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .subtle { |
| | | color: var(--lux-subtle); |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .kpi-strip { |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | .kpi-item { |
| | | padding: 12px 14px; |
| | | border-radius: 12px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | } |
| | | |
| | | .kpi-total { |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(47, 111, 237, 0.1), |
| | | rgba(255, 255, 255, 0.86) |
| | | ); |
| | | } |
| | | |
| | | .kpi-raw { |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(22, 163, 74, 0.1), |
| | | rgba(255, 255, 255, 0.86) |
| | | ); |
| | | } |
| | | |
| | | .kpi-avg { |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(99, 102, 241, 0.14), |
| | | rgba(255, 255, 255, 0.86) |
| | | ); |
| | | } |
| | | |
| | | .kpi-order { |
| | | background: linear-gradient( |
| | | 135deg, |
| | | rgba(100, 116, 139, 0.1), |
| | | rgba(255, 255, 255, 0.86) |
| | | ); |
| | | } |
| | | |
| | | .kpi-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .kpi-value { |
| | | font-size: 22px; |
| | | margin-top: 6px; |
| | | font-weight: 780; |
| | | color: var(--lux-text); |
| | | } |
| | | |
| | | .price-value { |
| | | font-weight: 700; |
| | | color: var(--lux-success); |
| | | } |
| | | |
| | | .cost-value { |
| | | font-weight: 700; |
| | | color: var(--lux-danger); |
| | | } |
| | | |
| | | .no-wrap-money { |
| | | display: inline-block; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .drawer-head { |
| | | grid-template-columns: 1fr; |
| | | display: grid; |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .meta-item { |
| | | padding: 10px 12px; |
| | | border-radius: 10px; |
| | | border: 1px solid var(--lux-border); |
| | | background: rgba(15, 23, 42, 0.03); |
| | | display: grid; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .meta-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .meta-value { |
| | | color: var(--lux-text); |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .material-type-tag { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 46px; |
| | | height: 24px; |
| | | padding: 0 8px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .material-type-tag.is-raw { |
| | | color: #15803d; |
| | | background: rgba(22, 163, 74, 0.12); |
| | | } |
| | | |
| | | .material-type-tag.is-aux { |
| | | color: #b45309; |
| | | background: rgba(245, 158, 11, 0.16); |
| | | } |
| | | |
| | | .quantity-value { |
| | | font-weight: 700; |
| | | color: var(--lux-text); |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .quantity-cell { |
| | | display: inline-flex; |
| | | align-items: baseline; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .quantity-unit { |
| | | color: var(--lux-subtle); |
| | | } |
| | | |
| | | .drawer-foot { |
| | | justify-content: flex-start; |
| | | flex-wrap: wrap; |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 12px; |
| | | margin-top: 12px; |
| | | padding-top: 12px; |
| | | border-top: 1px dashed var(--lux-border); |
| | | } |
| | | } |
| | | |
| | | .foot-item { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 7px 16px; |
| | | border-radius: 999px; |
| | | border: 1px solid var(--lux-border); |
| | | background: #fff; |
| | | } |
| | | |
| | | .foot-label { |
| | | color: var(--lux-subtle); |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .foot-value { |
| | | color: var(--lux-text); |
| | | font-weight: 700; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .foot-item.total { |
| | | border-color: rgba(47, 111, 237, 0.26); |
| | | background: rgba(47, 111, 237, 0.08); |
| | | } |
| | | |
| | | .foot-item.total .foot-value { |
| | | color: #1e3a8a; |
| | | font-size: 18px; |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .pagination-container { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-top: 12px; |
| | | } |
| | | |
| | | .strong { |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .w-260 { |
| | | width: 260px; |
| | | } |
| | | |
| | | .w-180 { |
| | | width: 180px; |
| | | } |
| | | |
| | | ::deep(.lux-table) { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | ::deep(.lux-table th.el-table__cell) { |
| | | background: rgba(15, 23, 42, 0.03); |
| | | } |
| | | |
| | | ::deep(.lux-table .el-table__row:hover > td.el-table__cell) { |
| | | background-color: rgba(47, 111, 237, 0.06) !important; |
| | | } |
| | | |
| | | @media (max-width: 1100px) { |
| | | .kpi-strip { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .filter-layout { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .drawer-head { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .drawer-foot { |
| | | justify-content: flex-start; |
| | | flex-wrap: wrap; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <template #completeQuantity="{ row }"> |
| | | {{ row.completeQuantity || '-' }}<span style="color:rgb(42, 169, 146)"> æ¹</span> |
| | | </template> |
| | | <template #strength="{ row }"> |
| | | <el-tag v-if="row.strength" |
| | | :type="row.strength === 'A3.5' ? 'primary' : 'warning'">{{ row.strength }}</el-tag> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <el-dialog v-model="bindRouteDialogVisible" |
| | |
| | | label: "强度", |
| | | prop: "strength", |
| | | width: "120px", |
| | | dataType: "tag", |
| | | dataType: "slot", |
| | | slot: "strength", |
| | | // formatData: val => (val ? val : ""), |
| | | }, |
| | | { |
| | | label: "ç©æç¼ç ", |
| | |
| | | <div class="detail-section" |
| | | v-if="detailData.productionProductRouteItemDtoList && detailData.productionProductRouteItemDtoList.length > 0"> |
| | | <h3 class="section-title">å·¥åºä¿¡æ¯</h3> |
| | | <div v-for="(process, index) in detailData.productionProductRouteItemDtoList" |
| | | <div v-for="(process) in detailData.productionProductRouteItemDtoList" |
| | | :key="process.id" |
| | | class="process-item"> |
| | | <div class="process-header"> |
| | |
| | | // å è½½ç产订åå表 |
| | | const loadOrders = () => { |
| | | orderLoading.value = true; |
| | | productOrderListPage({ pageNum: 1, pageSize: 100 }) |
| | | productOrderListPage({ pageNum: -1, pageSize: -1 }) |
| | | .then(res => { |
| | | orderList.value = res.data.records || []; |
| | | }) |
| | |
| | | mergeForm.totalAssignedQuantity = |
| | | (Number(row.volume) - Number(row.assignedQuantity)).toFixed(4) || 0; |
| | | mergeForm.planCompleteTime = row.planCompleteTime || ""; |
| | | mergeForm.productMaterialId = row.productMaterialId || ""; |
| | | mergeForm.strength = row.strength || ""; |
| | | sumAssignedQuantity.value = mergeForm.totalAssignedQuantity; |
| | | // æå¼å¼¹çª |
| | |
| | | totalAssignedQuantity: 0, |
| | | planCompleteTime: "", |
| | | strength: "", |
| | | productMaterialId: "", |
| | | }); |
| | | |
| | | // 导å
¥ç¸å
³ |
| | |
| | | router.push({ |
| | | path: "/productionPlan/trackProgress", |
| | | query: { |
| | | row: encodeURIComponent(JSON.stringify(row)), |
| | | applyNo: encodeURIComponent(row.applyNo), |
| | | }, |
| | | }); |
| | | }; |
| | |
| | | mergeForm.height = firstRow.height || 0; |
| | | mergeForm.totalAssignedQuantity = totalAssignedQuantity; |
| | | mergeForm.planCompleteTime = firstRow.planCompleteTime || ""; |
| | | mergeForm.productMaterialId = firstRow.productMaterialId || ""; |
| | | mergeForm.strength = firstStrength; |
| | | mergeForm.ids = selectedRows.value.map(row => row.id); |
| | | |
| | |
| | | ElMessage.error("ç å产åç强度为å¿
填项"); |
| | | return; |
| | | } |
| | | if (mergeForm.productName != "ç å") { |
| | | mergeForm.strength = ""; |
| | | } |
| | | console.log(sumAssignedQuantity.value, "sumAssignedQuantity"); |
| | | // 计ç®å½åéä¸è¡çæ»æ¹æ° |
| | | const totalVolume = selectedRows.value.reduce((sum, row) => { |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <PageHeader content="ç产计å追踪è¿åº¦"> |
| | | <PageHeader v-if="applyNo" |
| | | content="ç产计å追踪è¿åº¦"> |
| | | </PageHeader> |
| | | <el-card style="height:82vh;overflow:auto;"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>ç³è¯·åç¼å· - {{ rowData.applyNo || '' }}</span> |
| | | <el-form :inline="true" |
| | | :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-form-item> |
| | | </el-form> |
| | | </div> |
| | | </template> |
| | | <!-- åºç¡ä¿¡æ¯ --> |
| | |
| | | <el-descriptions :column="3" |
| | | border> |
| | | <el-descriptions-item label="订åç¼å·">{{ item.orderNo || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="订åç¶æ"> |
| | | <!-- <el-descriptions-item label="订åç¶æ"> |
| | | <el-tag :type="getStatusType(item.status)">{{ getStatusText(item.status) }}</el-tag> |
| | | </el-descriptions-item> |
| | | </el-descriptions-item> --> |
| | | <el-descriptions-item label="å¼å§æ¥æ">{{ item.startTime || '-' }}</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="宿è¿åº¦"> |
| | | <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 |
| | |
| | | <el-link v-if="$index!=0" |
| | | @click="handleClickStep(row)" |
| | | type="primary">{{ row.step }}</el-link> |
| | | <span v-else |
| | | @click="handleClickStep(row)">{{ row.step }}</span> |
| | | <span v-else>{{ row.step }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" |
| | | <!-- <el-table-column prop="status" |
| | | label="ç¶æ" |
| | | align="center"> |
| | | <template #default="scope"> |
| | |
| | | {{ scope.row.status === 'completed' ? '已宿' : scope.row.status === 'processing' ? 'è¿è¡ä¸' : 'å¾
å¼å§' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column> --> |
| | | <el-table-column prop="quantity" |
| | | label="æ°é" |
| | | align="center" /> |
| | | <el-table-columnstep prop="startTime" |
| | | label="æ¶é´" |
| | | align="center" /> |
| | | <el-table-column prop="startTime" |
| | | label="æ¶é´" |
| | | align="center" /> |
| | | <el-table-column prop="startTime1" |
| | | label="å²ä½äººå" |
| | | align="center" /> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <!-- <div class="progress-section"> |
| | | <h3 class="section-title">è¿åº¦ä¿¡æ¯</h3> |
| | | <div class="progress-item"> |
| | | <div class="progress-label">宿è¿åº¦ï¼</div> |
| | | <div class="progress-content"> |
| | | <el-progress :percentage="trackProgressForm.completionRate" |
| | | :color="customColors" |
| | | :status="trackProgressForm.completionRate === 100 ? 'success' : ''" /> |
| | | </div> |
| | | </div> |
| | | <div class="progress-item"> |
| | | <div class="progress-label">è¿åº¦è¯¦æ
ï¼</div> |
| | | <div class="progress-content"> |
| | | <el-table :data="trackProgressForm.progressDetails" |
| | | border |
| | | style="width: auto; height: 300px"> |
| | | <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 |
| | | @click="handleClickStep(row)">{{ 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> |
| | | </div> |
| | | </div> |
| | | </div> --> |
| | | </div> |
| | | </el-card> |
| | | <!-- ç产æ¥å·¥è¯¦æ
å¼¹çª --> |
| | | <el-dialog v-model="detailDialogVisible" |
| | | :title="'ç产æ¥å·¥è¯¦æ
'" |
| | | width="1000px" |
| | | :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> |
| | | </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> |
| | | <!-- åæ°ä¿¡æ¯ --> |
| | | <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 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> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="detailDialogVisible = false">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { ref, reactive, onMounted } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import { useRouter, useRoute } from "vue-router"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const router = useRouter(); |
| | | const route = useRoute(); |
| | |
| | | progressDetails: [], |
| | | remark: "", |
| | | }); |
| | | |
| | | // æç´¢è¡¨å |
| | | const searchForm = reactive({ |
| | | applyNo: "", |
| | | }); |
| | | |
| | | // ç产æ¥å·¥è¯¦æ
å¼¹çª |
| | | const detailDialogVisible = ref(false); |
| | | const detailData = ref({}); |
| | | const baseUrl = import.meta.env.VITE_APP_BASE_API; |
| | | |
| | | // è·åç¶æç±»å |
| | | const getStatusType = status => { |
| | |
| | | router.push("/productionPlan/productionPlan"); |
| | | }; |
| | | |
| | | // å¤çæç´¢ |
| | | const handleSearch = () => { |
| | | const applyNo = searchForm.applyNo.trim(); |
| | | if (!applyNo) { |
| | | 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(); |
| | | }; |
| | | |
| | | // çææ¨¡æè®¢åæ°æ® |
| | | const generateOrderList = () => { |
| | | return [ |
| | |
| | | status: 1, |
| | | quantity: 233.28, |
| | | completeQuantity: 14, |
| | | remainingQuantity: 149.28, |
| | | completionRate: 6, |
| | | startTime: "2026-03-25", |
| | | }, |
| | |
| | | status: 2, |
| | | quantity: 150.5, |
| | | completeQuantity: 100, |
| | | remainingQuantity: 50.5, |
| | | completionRate: 67, |
| | | startTime: "2026-03-20", |
| | | }, |
| | |
| | | status: 0, |
| | | quantity: 80.0, |
| | | completeQuantity: 0, |
| | | remainingQuantity: 80.0, |
| | | completionRate: 0, |
| | | startTime: "2026-03-30", |
| | | }, |
| | | ]; |
| | | }; |
| | | |
| | | // å¤çç¹å»æ¥éª¤æ¥ç详æ
|
| | | 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; |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¶é´ |
| | | const formatTime = time => { |
| | | return time ? dayjs(time).format("YYYY-MM-DD HH:mm:ss") : "-"; |
| | | }; |
| | | |
| | | // æ ¼å¼åæä»¶å表 |
| | | const formatFileList = fileList => { |
| | | return fileList.map(file => ({ |
| | | name: file.fileName, |
| | | url: baseUrl + file.fileUrl, |
| | | size: file.fileSize, |
| | | })); |
| | | }; |
| | | |
| | | // è·åBOMå表 |
| | | const getBomList = paramList => { |
| | | return paramList.filter(item => item.bomId); |
| | | }; |
| | | |
| | | // è·ååæ°å表 |
| | | const getParamList = paramList => { |
| | | return paramList.filter(item => !item.bomId); |
| | | }; |
| | | |
| | | // æsourceSortåç»åæ° |
| | | const getParamGroups = paramList => { |
| | | const params = getParamList(paramList); |
| | | const groups = {}; |
| | | |
| | | params.forEach(param => { |
| | | const sort = param.sourceSort || 1; |
| | | if (!groups[sort]) { |
| | | groups[sort] = []; |
| | | } |
| | | // 计ç®ç»æ |
| | | let result = "åæ ¼"; |
| | | let standardText = ""; |
| | | if (param.valueMode === 1) { |
| | | // å弿¯è¾ |
| | | if (param.standardValue !== null && param.standardValue !== undefined) { |
| | | standardText = param.standardValue; |
| | | if (param.paramValue !== param.standardValue) { |
| | | result = "ä¸åæ ¼"; |
| | | } |
| | | } else { |
| | | standardText = "-"; |
| | | result = "åæ ¼"; |
| | | } |
| | | } else if (param.valueMode === 2) { |
| | | // åºé´æ¯è¾ |
| | | if (param.minValue !== null || param.maxValue !== null) { |
| | | standardText = |
| | | (param.minValue ? param.minValue : "-â") + |
| | | "~" + |
| | | (param.maxValue ? param.maxValue : "+â"); |
| | | if ( |
| | | param.paramValue < param.minValue || |
| | | param.paramValue > param.maxValue |
| | | ) { |
| | | result = "ä¸åæ ¼"; |
| | | } |
| | | } else { |
| | | standardText = "-"; |
| | | result = "åæ ¼"; |
| | | } |
| | | } else { |
| | | // é»è®¤æ
åµ |
| | | standardText = "-"; |
| | | result = "åæ ¼"; |
| | | } |
| | | groups[sort].push({ |
| | | ...param, |
| | | standardText, |
| | | result, |
| | | }); |
| | | }); |
| | | |
| | | // 转æ¢ä¸ºæ°ç»æ ¼å¼ |
| | | return Object.entries(groups).map(([key, items]) => ({ |
| | | sourceSort: key, |
| | | items, |
| | | })); |
| | | }; |
| | | |
| | | // 为ä¸åæ ¼çè¡æ·»å æ ·å¼ |
| | | const rowClassName = ({ row }) => { |
| | | return row.result === "ä¸åæ ¼" ? "warning-row" : ""; |
| | | }; |
| | | |
| | | const applyNo = ref(null); |
| | | // 页é¢å è½½æ¶è·åæ°æ® |
| | | onMounted(() => { |
| | | // ä»è·¯ç±åæ°ä¸è·åæ°æ® |
| | | const data = route.query.row |
| | | ? JSON.parse(decodeURIComponent(route.query.row)) |
| | | applyNo.value = route.query.applyNo |
| | | ? decodeURIComponent(route.query.applyNo) |
| | | : null; |
| | | if (data) { |
| | | // èµå¼ç»rowData |
| | | Object.assign(rowData, data); |
| | | // èµå¼ç»è¡¨åæ°æ® |
| | | trackProgressForm.materialCode = data.materialCode; |
| | | trackProgressForm.currentStatus = data.status; |
| | | trackProgressForm.progressDetails = generateProgressDetails(data.status); |
| | | trackProgressForm.completionRate = calculateCompletionRate( |
| | | trackProgressForm.progressDetails |
| | | ); |
| | | trackProgressForm.remark = ""; |
| | | } |
| | | 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(); |
| | | }); |
| | |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 0 10px; |
| | | } |
| | | |
| | | .search-form { |
| | | width: 100%; |
| | | } |
| | | |
| | | .search-form .el-form-item { |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .action-buttons { |
| | |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08); |
| | | flex: 1; |
| | | transition: all 0.3s ease; |
| | | width: 100%; |
| | | } |
| | | |
| | | .progress-section:hover { |
| | |
| | | border-radius: 12px; |
| | | padding: 2px 10px; |
| | | } |
| | | |
| | | /* å¼¹çªæ ·å¼ */ |
| | | .detail-container { |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | padding: 0 16px; |
| | | } |
| | | |
| | | .process-item { |
| | | margin-bottom: 24px; |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #ebeef5; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .process-header { |
| | | margin-bottom: 20px; |
| | | padding-bottom: 12px; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .process-title { |
| | | font-size: 15px; |
| | | font-weight: 600; |
| | | margin-bottom: 12px; |
| | | color: #1a1a1a; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .process-title::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 4px; |
| | | height: 16px; |
| | | background-color: #409eff; |
| | | margin-right: 8px; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .process-info { |
| | | display: flex; |
| | | gap: 20px; |
| | | font-size: 13px; |
| | | color: #606266; |
| | | } |
| | | |
| | | .process-label { |
| | | padding: 4px 12px; |
| | | background-color: #ecf5ff; |
| | | border-radius: 4px; |
| | | color: #409eff; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .process-details { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .param-section { |
| | | margin-bottom: 20px; |
| | | background-color: #f9f9f9; |
| | | border-radius: 6px; |
| | | padding: 16px; |
| | | border: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .param-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | margin-bottom: 14px; |
| | | color: #1a1a1a; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #e8e8e8; |
| | | } |
| | | |
| | | .file-section { |
| | | margin-top: 20px; |
| | | background-color: #f9f9f9; |
| | | border-radius: 6px; |
| | | padding: 16px; |
| | | border: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .file-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | margin-bottom: 14px; |
| | | color: #1a1a1a; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #e8e8e8; |
| | | } |
| | | |
| | | .file-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .file-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | background-color: #ffffff; |
| | | border: 1px solid #e8e8e8; |
| | | border-radius: 6px; |
| | | padding: 10px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .file-item:hover { |
| | | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
| | | border-color: #409eff; |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .file-info { |
| | | width: 100%; |
| | | text-align: center; |
| | | } |
| | | |
| | | .file-name { |
| | | font-size: 12px; |
| | | color: #606266; |
| | | word-break: break-all; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .param-group { |
| | | margin-bottom: 16px; |
| | | padding: 14px; |
| | | background-color: #ffffff; |
| | | border-radius: 6px; |
| | | border: 1px solid #e8e8e8; |
| | | } |
| | | |
| | | .group-header { |
| | | margin-bottom: 12px; |
| | | padding-bottom: 8px; |
| | | border-bottom: 1px solid #f0f2f5; |
| | | } |
| | | |
| | | .num1 { |
| | | color: #1107cc; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .num2 { |
| | | color: #0fcf25; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .num3 { |
| | | color: #d31818; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .group-title { |
| | | font-size: 14px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | .param-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .param-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 8px 0; |
| | | border-bottom: 1px solid #f5f7fa; |
| | | } |
| | | |
| | | .param-item:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .param-label { |
| | | font-size: 13px; |
| | | color: #606266; |
| | | min-width: 100px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .param-value { |
| | | font-size: 13px; |
| | | color: #1a1a1a; |
| | | font-weight: 600; |
| | | flex: 1; |
| | | } |
| | | |
| | | .param-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | background-color: #f0f2f5; |
| | | padding: 2px 6px; |
| | | border-radius: 3px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: center; |
| | | padding: 20px; |
| | | border-top: 1px solid #ebeef5; |
| | | } |
| | | |
| | | .dialog-footer .el-button { |
| | | min-width: 100px; |
| | | padding: 8px 20px; |
| | | } |
| | | |
| | | /* èªå®ä¹å¯¹è¯æ¡æ ·å¼ */ |
| | | :deep(.custom-dialog) { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.custom-dialog .el-dialog__header) { |
| | | background-color: #f5f7fa; |
| | | padding: 20px; |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | :deep(.custom-dialog .el-dialog__title) { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #1a1a1a; |
| | | } |
| | | |
| | | :deep(.custom-dialog .el-dialog__body) { |
| | | padding: 20px; |
| | | } |
| | | |
| | | /* è¡¨æ ¼æ ·å¼ä¼å */ |
| | | :deep(.el-table) { |
| | | border-radius: 6px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #f5f7fa; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff !important; |
| | | } |
| | | |
| | | /* æè¿°åè¡¨æ ·å¼ä¼å */ |
| | | :deep(.el-descriptions) { |
| | | border-radius: 6px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-descriptions__label) { |
| | | font-weight: 500; |
| | | color: #606266; |
| | | } |
| | | |
| | | :deep(.el-descriptions__content) { |
| | | color: #1a1a1a; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | /* ä¸åæ ¼è¡æ ·å¼ */ |
| | | :deep(.el-table .warning-row) { |
| | | background-color: #fef0f0 !important; |
| | | } |
| | | |
| | | .detail-card { |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | } |
| | | |
| | | .card-header { |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | </style> |
| | |
| | | <div class="center-metric-unit">å®¶</div> |
| | | </div> |
| | | <div class="center-metric m4"> |
| | | <div class="center-metric-label">æ»éå®åº</div> |
| | | <div class="center-metric-value">{{ totalSalesAreaCount }}</div> |
| | | <div class="center-metric-unit">åº</div> |
| | | <div class="center-metric-label">é宿¹æ°</div> |
| | | <div class="center-metric-value">{{ totalSalesAreaCount.toFixed(0) }}</div> |
| | | <div class="center-metric-unit">æ¹</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- å·¦ä¸ï¼äº§åç±»åéé --> |
| | | <div class="bi-panel bi-panel-bottom-left"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="ééæ°æ®-æååæ" /> |
| | | title="ééæ°æ®ç»è®¡" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item" |
| | | :class="{ active: blockTimeDimension === 'year' }" |
| | |
| | | <table class="scroll-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>æå</th> |
| | | <th>åºå·</th> |
| | | <th>产åç±»å</th> |
| | | <th>å¹´æ</th> |
| | | <th>éå®åº</th> |
| | |
| | | <!-- å³ä¸ï¼éå®åºåéé --> |
| | | <div class="bi-panel bi-panel-bottom-right"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="éå®é¢æ°æ®-æååæ" /> |
| | | title="éå®é¢æ°æ®ç»è®¡" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item" |
| | | :class="{ active: boardTimeDimension === 'year' }" |
| | |
| | | <table class="scroll-table"> |
| | | <thead> |
| | | <tr> |
| | | <th>æå</th> |
| | | <th>åºå·</th> |
| | | <th>å¹´æ</th> |
| | | <th>éå®åº</th> |
| | | <th>éå®é¢ï¼ä¸å
ï¼</th> |
| | |
| | | } |
| | | |
| | | /* .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); |
| | | } |
| | |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" |
| | | :inline="true"> |
| | | <el-form-item label="æ¶é´ç»´åº¦:"> |
| | | <el-select v-model="searchForm.timeDimension" |
| | | placeholder="è¯·éæ©æ¶é´ç»´åº¦" |
| | | style="width: 120px;" |
| | | @change="handleQuery"> |
| | | <el-option label="年度" |
| | | value="year" /> |
| | | <el-option label="æåº¦" |
| | | value="month" /> |
| | | </el-select> |
| | | <el-form-item label="ç»è®¡ç»´åº¦:"> |
| | | <el-radio-group v-model="statisticsType" |
| | | @change="handleTypeChange"> |
| | | <el-radio-button label="day">ææ¥ç»è®¡</el-radio-button> |
| | | <el-radio-button label="month">ææç»è®¡</el-radio-button> |
| | | <el-radio-button label="year">æå¹´ç»è®¡</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item label="年份:"> |
| | | <el-select v-model="searchForm.year" |
| | | placeholder="è¯·éæ©å¹´ä»½" |
| | | style="width: 120px;" |
| | | @change="handleQuery"> |
| | | <el-option v-for="year in years" |
| | | :key="year" |
| | | :label="year + 'å¹´'" |
| | | :value="year" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æä»½:" |
| | | v-if="searchForm.timeDimension === 'month'"> |
| | | <el-select v-model="searchForm.month" |
| | | placeholder="è¯·éæ©æä»½" |
| | | style="width: 120px;" |
| | | @change="handleQuery"> |
| | | <el-option v-for="month in 12" |
| | | :key="month" |
| | | :label="month + 'æ'" |
| | | :value="month" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è½èç±»å:"> |
| | | <!-- <el-form-item label="è½èç±»å:"> |
| | | <el-select v-model="searchForm.energyType" |
| | | placeholder="å
¨é¨" |
| | | clearable |
| | | style="width: 140px;" |
| | | style="width: 120px;" |
| | | @change="handleQuery"> |
| | | <el-option label="å
¨é¨" |
| | | value="å
¨é¨" /> |
| | |
| | | value="çµ" /> |
| | | <el-option label="è¸æ±½" |
| | | value="è¸æ±½" /> |
| | | </el-select> |
| | | </el-form-item> --> |
| | | <el-form-item label="æ¶é´èå´:"> |
| | | <el-date-picker v-if="statisticsType === 'day'" |
| | | v-model="searchForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 240px;" |
| | | @change="handleQuery" /> |
| | | <el-date-picker v-else-if="statisticsType === 'month'" |
| | | v-model="searchForm.monthRange" |
| | | type="monthrange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æä»½" |
| | | end-placeholder="ç»ææä»½" |
| | | value-format="YYYY-MM" |
| | | style="width: 240px;" |
| | | @change="handleQuery" /> |
| | | <el-select v-else |
| | | v-model="searchForm.year" |
| | | placeholder="鿩年份" |
| | | style="width: 140px;" |
| | | @change="handleQuery"> |
| | | <el-option v-for="year in yearOptions" |
| | | :key="year" |
| | | :label="year + 'å¹´'" |
| | | :value="year" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | |
| | | </el-icon> |
| | | è½èåèæ°æ® |
| | | </h2> |
| | | <el-table :data="tableData" |
| | | <el-table :data="tableValue" |
| | | v-loading="tableLoading" |
| | | border> |
| | | <el-table-column prop="energyType" |
| | | label="è½è" |
| | | width="100" |
| | | <el-table-column prop="meterReadingDate" |
| | | label="æ¥æ" |
| | | align="center" /> |
| | | <!-- <el-table-column prop="type" |
| | | label="ç±»å" |
| | | align="center" |
| | | width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="scope.row.type === 'ç产' ? 'primary' : 'success'"> |
| | | {{ scope.row.type }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> --> |
| | | <el-table-column prop="energyTyep" |
| | | label="è½èç±»å" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getEnergyTypeType(scope.row.energyType)"> |
| | | {{ scope.row.energyType }} |
| | | <el-tag :type="getEnergyTypeType(scope.row.type)"> |
| | | {{ scope.row.type }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unit" |
| | | label="åä½" |
| | | width="120" |
| | | align="center" /> |
| | | <el-table-column label="æåº¦æ°æ®" |
| | | v-if="searchForm.timeDimension === 'month'"> |
| | | <el-table-column prop="monthlyUnitConsumption" |
| | | label="æåº¦ç´¯è®¡åè" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.monthlyUnitConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="monthlyConsumption" |
| | | label="æåº¦ç´¯è®¡ç¨é/æåº¦ç´¯è®¡äº§é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.monthlyConsumption }}/{{ scope.row.monthlyProduction }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="consumption" |
| | | label="ç¨é" |
| | | align="right" /> |
| | | <el-table-column prop="cost" |
| | | label="ææ¬" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">Â¥{{ scope.row.cost }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="å¹´åº¦æ°æ®" |
| | | v-if="searchForm.timeDimension === 'year'"> |
| | | <el-table-column prop="annualUnitConsumption" |
| | | label="年度累计åè" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.annualUnitConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="annualConsumption" |
| | | label="年度累计ç¨é/年度累计产é" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.annualConsumption }}/{{ scope.row.annualProduction }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitConsumption" |
| | | label="åè" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.unitConsumption }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | |
| | | import { ElMessage } from "element-plus"; |
| | | import { TrendCharts, List } from "@element-plus/icons-vue"; |
| | | import * as echarts from "echarts"; |
| | | import { energyConsumptionDetailStatistics } from "@/api/energyManagement/energyType"; |
| | | |
| | | // ç»è®¡ç»´åº¦ |
| | | const statisticsType = ref("day"); |
| | | |
| | | // æç´¢è¡¨å |
| | | const searchForm = reactive({ |
| | | timeDimension: "year", |
| | | year: new Date().getFullYear(), |
| | | month: new Date().getMonth() + 1, |
| | | energyType: "", |
| | | dateRange: null, |
| | | monthRange: null, |
| | | year: new Date().getFullYear(), |
| | | }); |
| | | |
| | | // çæå¹´ä»½é项 |
| | | const years = []; |
| | | // çæå¹´ä»½éé¡¹ï¼æè¿7å¹´ï¼ |
| | | const yearOptions = []; |
| | | const currentYear = new Date().getFullYear(); |
| | | for (let i = currentYear - 5; i <= currentYear; i++) { |
| | | years.push(i); |
| | | for (let i = currentYear - 6; i <= currentYear; i++) { |
| | | yearOptions.push(i); |
| | | } |
| | | |
| | | // å¤çç»è®¡ç»´åº¦åå |
| | | const handleTypeChange = () => { |
| | | // éç½®æ¶é´éæ© |
| | | if (statisticsType.value === "day") { |
| | | // 设置é»è®¤æ¥æèå´ä¸ºæè¿30天 |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setDate(start.getDate() - 29); |
| | | searchForm.dateRange = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(start.getDate()).padStart(2, "0")}`, |
| | | `${end.getFullYear()}-${String(end.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(end.getDate()).padStart(2, "0")}`, |
| | | ]; |
| | | } else if (statisticsType.value === "month") { |
| | | // 设置é»è®¤æä»½èå´ä¸ºæè¿6个æ |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setMonth(start.getMonth() - 5); |
| | | searchForm.monthRange = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}`, |
| | | `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}`, |
| | | ]; |
| | | } else { |
| | | searchForm.year = currentYear; |
| | | } |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = ref([]); |
| | |
| | | const updateChart = () => { |
| | | const data = tableData.value; |
| | | let xAxisData = []; |
| | | let seriesDataKey = "monthlyData"; |
| | | let seriesDataMap = item => item.unitConsumption; |
| | | |
| | | // æ ¹æ®æ¶é´ç»´åº¦å夿°æ® |
| | | if (searchForm.timeDimension === "year") { |
| | | // æ ¹æ®ç»è®¡ç»´åº¦å夿°æ® |
| | | if (statisticsType.value === "year") { |
| | | // 年度模å¼ï¼12个æ |
| | | xAxisData = [ |
| | | "1æ", |
| | |
| | | "11æ", |
| | | "12æ", |
| | | ]; |
| | | } else if (statisticsType.value === "month") { |
| | | // æåº¦æ¨¡å¼ï¼æ ¹æ®éæ©çæä»½èå´ |
| | | if (searchForm.monthRange && searchForm.monthRange.length === 2) { |
| | | const startMonth = searchForm.monthRange[0]; |
| | | const endMonth = searchForm.monthRange[1]; |
| | | const [startYear, startMonthNum] = startMonth.split("-"); |
| | | const [endYear, endMonthNum] = endMonth.split("-"); |
| | | |
| | | xAxisData = []; |
| | | let currentYear = parseInt(startYear); |
| | | let currentMonth = parseInt(startMonthNum); |
| | | const endYearInt = parseInt(endYear); |
| | | const endMonthInt = parseInt(endMonthNum); |
| | | |
| | | while ( |
| | | currentYear < endYearInt || |
| | | (currentYear === endYearInt && currentMonth <= endMonthInt) |
| | | ) { |
| | | xAxisData.push( |
| | | `${currentYear}-${String(currentMonth).padStart(2, "0")}` |
| | | ); |
| | | currentMonth++; |
| | | if (currentMonth > 12) { |
| | | currentMonth = 1; |
| | | currentYear++; |
| | | } |
| | | } |
| | | } else { |
| | | // é»è®¤æ¾ç¤ºæè¿6个æ |
| | | xAxisData = []; |
| | | const end = new Date(); |
| | | for (let i = 5; i >= 0; i--) { |
| | | const date = new Date(); |
| | | date.setMonth(date.getMonth() - i); |
| | | xAxisData.push( |
| | | `${date.getFullYear()}-${String(date.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}` |
| | | ); |
| | | } |
| | | } |
| | | } else { |
| | | // æåº¦æ¨¡å¼ï¼è¯¥æçæ¯ä¸å¤© |
| | | const year = searchForm.year; |
| | | const month = searchForm.month; |
| | | const daysInMonth = new Date(year, month, 0).getDate(); |
| | | xAxisData = Array.from({ length: daysInMonth }, (_, i) => `${i + 1}æ¥`); |
| | | seriesDataKey = "dailyData"; |
| | | // ææ¥ç»è®¡ï¼æ ¹æ®éæ©çæ¥æèå´ |
| | | if (searchForm.dateRange && searchForm.dateRange.length === 2) { |
| | | const startDate = searchForm.dateRange[0]; |
| | | const endDate = searchForm.dateRange[1]; |
| | | |
| | | xAxisData = []; |
| | | let currentDate = new Date(startDate); |
| | | const end = new Date(endDate); |
| | | |
| | | while (currentDate <= end) { |
| | | xAxisData.push( |
| | | `${currentDate.getFullYear()}-${String( |
| | | currentDate.getMonth() + 1 |
| | | ).padStart(2, "0")}-${String(currentDate.getDate()).padStart(2, "0")}` |
| | | ); |
| | | currentDate.setDate(currentDate.getDate() + 1); |
| | | } |
| | | } else { |
| | | // é»è®¤æ¾ç¤ºæè¿30天 |
| | | xAxisData = []; |
| | | const end = new Date(); |
| | | for (let i = 29; i >= 0; i--) { |
| | | const date = new Date(); |
| | | date.setDate(date.getDate() - i); |
| | | xAxisData.push( |
| | | `${date.getFullYear()}-${String(date.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(date.getDate()).padStart(2, "0")}` |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | |
| | | // åå¤å¾è¡¨æ°æ® |
| | | const series = []; |
| | | const energyTypes = ["æ°´", "çµ", "è¸æ±½"]; |
| | | |
| | | energyTypes.forEach(type => { |
| | | const typeData = data.find(item => item.energyType === type); |
| | | if (typeData && typeData[seriesDataKey]) { |
| | | series.push({ |
| | | name: type, |
| | | type: "line", |
| | | data: typeData[seriesDataKey].map(seriesDataMap), |
| | | smooth: true, |
| | | symbol: "circle", |
| | | symbolSize: 8, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: |
| | | getEnergyTypeType(type) === "primary" |
| | | ? "#409EFF" |
| | | : getEnergyTypeType(type) === "warning" |
| | | ? "#E6A23C" |
| | | : "#67C23A", |
| | | }, |
| | | }); |
| | | // æåææå¯ä¸çmeterReadingDateå¹¶æåº |
| | | const allDates = [...new Set(data.map(item => item.meterReadingDate))].sort( |
| | | (a, b) => { |
| | | return new Date(a) - new Date(b); |
| | | } |
| | | }); |
| | | ); |
| | | // 使ç¨å®é
çmeterReadingDateä½ä¸ºæ¨ªè½´æ°æ® |
| | | xAxisData = allDates; |
| | | |
| | | const option = { |
| | | tooltip: { |
| | |
| | | textStyle: { color: "#303133" }, |
| | | }, |
| | | legend: { |
| | | data: energyTypes, |
| | | data: ["åè"], |
| | | top: 0, |
| | | right: 10, |
| | | textStyle: { color: "#606266" }, |
| | |
| | | data: xAxisData, |
| | | axisLabel: { |
| | | color: "#606266", |
| | | rotate: searchForm.timeDimension === "month" ? 45 : 0, |
| | | rotate: statisticsType.value === "month" ? 45 : 0, |
| | | }, |
| | | axisLine: { lineStyle: { color: "#ebeef5" } }, |
| | | splitLine: { show: false }, |
| | |
| | | axisLine: { show: false }, |
| | | splitLine: { lineStyle: { color: "#f0f2f5" } }, |
| | | }, |
| | | series: series, |
| | | series: { |
| | | name: "åè", |
| | | type: "line", |
| | | data: data.map(item => item.totalCost), |
| | | smooth: true, |
| | | symbol: "circle", |
| | | symbolSize: 8, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | }; |
| | | |
| | | consumptionChartInstance.setOption(option); |
| | | }; |
| | | const tableValue = ref([]); |
| | | |
| | | // æ¥è¯¢ |
| | | const handleQuery = () => { |
| | | tableLoading.value = true; |
| | | |
| | | // æ¨¡ææ¥å£è°ç¨ |
| | | setTimeout(() => { |
| | | generateMockData(); |
| | | tableLoading.value = false; |
| | | updateChart(); |
| | | }, 500); |
| | | const params = { |
| | | type: "", |
| | | state: |
| | | statisticsType.value === "year" |
| | | ? "å¹´" |
| | | : statisticsType.value === "month" |
| | | ? "æ" |
| | | : "æ¥", |
| | | }; |
| | | |
| | | if (searchForm.energyType && searchForm.energyType !== "å
¨é¨") { |
| | | params.type = searchForm.energyType; |
| | | } |
| | | |
| | | if (statisticsType.value === "year") { |
| | | params.startDate = searchForm.year + "-01-01"; |
| | | params.endDate = searchForm.year + "-12-31"; |
| | | // 计ç®å¤©æ° |
| | | const start = new Date(params.startDate); |
| | | const end = new Date(params.endDate); |
| | | params.days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; |
| | | } else if (statisticsType.value === "month" && searchForm.monthRange) { |
| | | const [startMonth, endMonth] = searchForm.monthRange; |
| | | const [startYearStr, startMonthStr] = startMonth.split("-"); |
| | | const [endYearStr, endMonthStr] = endMonth.split("-"); |
| | | |
| | | params.startDate = `${startYearStr}-${startMonthStr}-01`; |
| | | |
| | | const endYear = Number(endYearStr); |
| | | const endMonthNum = Number(endMonthStr); |
| | | const lastDay = new Date(endYear, endMonthNum, 0).getDate(); |
| | | params.endDate = `${endYearStr}-${endMonthStr}-${String(lastDay).padStart( |
| | | 2, |
| | | "0" |
| | | )}`; |
| | | |
| | | // 计ç®å¤©æ° |
| | | const start = new Date(params.startDate); |
| | | const end = new Date(params.endDate); |
| | | params.days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; |
| | | } else if (statisticsType.value === "day" && searchForm.dateRange) { |
| | | params.startDate = searchForm.dateRange[0]; |
| | | params.endDate = searchForm.dateRange[1]; |
| | | // 计ç®å¤©æ° |
| | | const start = new Date(params.startDate); |
| | | const end = new Date(params.endDate); |
| | | params.days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)) + 1; |
| | | } |
| | | |
| | | energyConsumptionDetailStatistics(params) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | const data = res.data; |
| | | tableData.value = data.energyCostDtos || []; |
| | | tableValue.value = []; |
| | | tableData.value.forEach(item => { |
| | | tableValue.value.push({ |
| | | meterReadingDate: item.meterReadingDate, |
| | | consumption: item.waterConsumption, |
| | | cost: item.waterCost, |
| | | type: "æ°´", |
| | | }); |
| | | tableValue.value.push({ |
| | | consumption: item.electricityConsumption, |
| | | cost: item.electricityCost, |
| | | meterReadingDate: item.meterReadingDate, |
| | | type: "çµ", |
| | | }); |
| | | tableValue.value.push({ |
| | | consumption: item.gasConsumption, |
| | | cost: item.gasCost, |
| | | meterReadingDate: item.meterReadingDate, |
| | | type: "è¸æ±½", |
| | | }); |
| | | }); |
| | | updateChart(); |
| | | } else { |
| | | ElMessage.error(res.message || "è·åæ°æ®å¤±è´¥"); |
| | | tableData.value = []; |
| | | } |
| | | }) |
| | | .catch(err => { |
| | | console.error("è·åæ°æ®å¼å¸¸ï¼", err); |
| | | ElMessage.error("ç³»ç»å¼å¸¸ï¼è·åæ°æ®å¤±è´¥"); |
| | | tableData.value = []; |
| | | }) |
| | | .finally(() => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // éç½® |
| | | const handleReset = () => { |
| | | searchForm.timeDimension = "year"; |
| | | searchForm.year = new Date().getFullYear(); |
| | | searchForm.month = new Date().getMonth() + 1; |
| | | // éç½®æç´¢è¡¨å |
| | | searchForm.energyType = ""; |
| | | if (statisticsType.value === "day") { |
| | | // 设置é»è®¤æ¥æèå´ä¸ºæè¿30天 |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setDate(start.getDate() - 29); |
| | | searchForm.dateRange = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(start.getDate()).padStart(2, "0")}`, |
| | | `${end.getFullYear()}-${String(end.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(end.getDate()).padStart(2, "0")}`, |
| | | ]; |
| | | } else if (statisticsType.value === "month") { |
| | | // 设置é»è®¤æä»½èå´ä¸ºæè¿6个æ |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setMonth(start.getMonth() - 5); |
| | | searchForm.monthRange = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}`, |
| | | `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}`, |
| | | ]; |
| | | } else { |
| | | searchForm.year = new Date().getFullYear(); |
| | | } |
| | | handleQuery(); |
| | | }; |
| | | |
| | |
| | | ElMessage.success("æ¥è¡¨å¯¼åºæå"); |
| | | }; |
| | | |
| | | // çæåæ°æ® |
| | | const generateMockData = () => { |
| | | const energyTypes = [ |
| | | { |
| | | energyType: "æ°´", |
| | | unit: "å¨/ç«æ¹ç±³", |
| | | monthlyUnitConsumption: (Math.random() * 0.5 + 0.8).toFixed(4), |
| | | monthlyConsumption: Math.floor(Math.random() * 5000 + 10000), |
| | | monthlyProduction: Math.floor(Math.random() * 10000 + 20000), |
| | | annualUnitConsumption: (Math.random() * 0.3 + 0.9).toFixed(4), |
| | | annualConsumption: Math.floor(Math.random() * 60000 + 120000), |
| | | annualProduction: Math.floor(Math.random() * 120000 + 240000), |
| | | monthlyData: generateMonthlyData(0.8, 1.3), |
| | | dailyData: generateDailyData(0.7, 1.4), |
| | | }, |
| | | { |
| | | energyType: "çµ", |
| | | unit: "度/ç«æ¹ç±³", |
| | | monthlyUnitConsumption: (Math.random() * 2 + 5).toFixed(4), |
| | | monthlyConsumption: Math.floor(Math.random() * 50000 + 100000), |
| | | monthlyProduction: Math.floor(Math.random() * 10000 + 20000), |
| | | annualUnitConsumption: (Math.random() * 1.5 + 5.5).toFixed(4), |
| | | annualConsumption: Math.floor(Math.random() * 600000 + 1200000), |
| | | annualProduction: Math.floor(Math.random() * 120000 + 240000), |
| | | monthlyData: generateMonthlyData(5, 7), |
| | | dailyData: generateDailyData(4.5, 7.5), |
| | | }, |
| | | { |
| | | energyType: "è¸æ±½", |
| | | unit: "å¨/ç«æ¹ç±³", |
| | | monthlyUnitConsumption: (Math.random() * 0.3 + 0.5).toFixed(4), |
| | | monthlyConsumption: Math.floor(Math.random() * 3000 + 6000), |
| | | monthlyProduction: Math.floor(Math.random() * 10000 + 20000), |
| | | annualUnitConsumption: (Math.random() * 0.2 + 0.55).toFixed(4), |
| | | annualConsumption: Math.floor(Math.random() * 36000 + 72000), |
| | | annualProduction: Math.floor(Math.random() * 120000 + 240000), |
| | | monthlyData: generateMonthlyData(0.5, 0.8), |
| | | dailyData: generateDailyData(0.4, 0.9), |
| | | }, |
| | | ]; |
| | | |
| | | if (searchForm.energyType && searchForm.energyType !== "å
¨é¨") { |
| | | tableData.value = energyTypes.filter( |
| | | item => item.energyType === searchForm.energyType |
| | | ); |
| | | } else { |
| | | tableData.value = energyTypes; |
| | | } |
| | | }; |
| | | |
| | | // çææåº¦æ°æ® |
| | | const generateMonthlyData = (min, max) => { |
| | | const data = []; |
| | | for (let i = 1; i <= 12; i++) { |
| | | data.push({ |
| | | month: i, |
| | | unitConsumption: (Math.random() * (max - min) + min).toFixed(4), |
| | | }); |
| | | } |
| | | return data; |
| | | }; |
| | | |
| | | // çææ¯æ¥æ°æ® |
| | | const generateDailyData = (min, max) => { |
| | | const year = searchForm.year; |
| | | const month = searchForm.month; |
| | | const daysInMonth = new Date(year, month, 0).getDate(); |
| | | const data = []; |
| | | for (let i = 1; i <= daysInMonth; i++) { |
| | | data.push({ |
| | | day: i, |
| | | unitConsumption: (Math.random() * (max - min) + min).toFixed(4), |
| | | }); |
| | | } |
| | | return data; |
| | | }; |
| | | |
| | | // çªå£å¤§å°ååæ¶éæ°æ¸²æå¾è¡¨ |
| | | const handleResize = () => { |
| | | consumptionChartInstance && consumptionChartInstance.resize(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | // åå§åæ¶é´èå´ |
| | | if (statisticsType.value === "day") { |
| | | // 设置é»è®¤æ¥æèå´ä¸ºæè¿30天 |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setDate(start.getDate() - 29); |
| | | searchForm.dateRange = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(start.getDate()).padStart(2, "0")}`, |
| | | `${end.getFullYear()}-${String(end.getMonth() + 1).padStart( |
| | | 2, |
| | | "0" |
| | | )}-${String(end.getDate()).padStart(2, "0")}`, |
| | | ]; |
| | | } else if (statisticsType.value === "month") { |
| | | // 设置é»è®¤æä»½èå´ä¸ºæè¿6个æ |
| | | const end = new Date(); |
| | | const start = new Date(); |
| | | start.setMonth(start.getMonth() - 5); |
| | | searchForm.monthRange = [ |
| | | `${start.getFullYear()}-${String(start.getMonth() + 1).padStart(2, "0")}`, |
| | | `${end.getFullYear()}-${String(end.getMonth() + 1).padStart(2, "0")}`, |
| | | ]; |
| | | } |
| | | handleQuery(); |
| | | initChart(); |
| | | window.addEventListener("resize", handleResize); |