| | |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | | // ç产æ¥å·¥-ç¼è¾ |
| | | export function productionRecordEditSubmit(data) { |
| | | return request({ |
| | | url: "/productionRecord/edit", |
| | | method: "post", |
| | | data: data, |
| | | }); |
| | | } |
| | |
| | | data: query, |
| | | }); |
| | | } |
| | | // ç产æ¥å·¥-å é¤ |
| | | export function productionReportDelete(query) { |
| | | |
| | | |
| | | // ç产æ¥å·¥-å页æ¥è¯¢ |
| | | export function productionReportListPage(query) { |
| | | return request({ |
| | | url: "/productionProductMain/delete", |
| | | method: "delete", |
| | | data: query, |
| | | url: "/productionProductMain/listPage", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | // ç产æ¥å·¥-详æ
|
| | | export function productionReportDetail(id) { |
| | | return request({ |
| | | url: "/productionRecord/detail/" + id, |
| | | method: "get", |
| | | }); |
| | | } |
| | | |
| | | // ç产æ¥å·¥-å é¤ |
| | | export function productionReportDelete(id) { |
| | | return request({ |
| | | url: `/productionRecord/`+id, |
| | | method: "delete", |
| | | }); |
| | | } |
| | |
| | | prop="paramFormat"> |
| | | <el-input v-model="formData.paramFormat" |
| | | placeholder="请è¾å
¥å弿 ¼å¼" /> |
| | | <!-- <el-select v-model="formData.paramFormat" |
| | | placeholder="è¯·éæ©å弿¨¡å¼"> |
| | | <el-option label="#.00000" |
| | | value="#.00000" /> |
| | | <el-option label="#.0000" |
| | | value="#.0000" /> |
| | | <el-option label="#.000" |
| | | value="#.000" /> |
| | | <el-option label="#.00" |
| | | value="#.00" /> |
| | | </el-select> --> |
| | | </el-form-item> |
| | | <el-form-item label="䏿åå
¸" |
| | | v-else-if="formData.paramType == '3'" |
| | |
| | | // const isProductTypeEdit = ref(false); |
| | | const handleParamTypeChange = () => { |
| | | if (formData.paramType === "1") { |
| | | formData.paramFormat = "#.0000"; |
| | | formData.paramFormat = "#.00000"; |
| | | } else if (formData.paramType === "4") { |
| | | formData.paramFormat = "YYYY-MM-DD HH:mm:ss"; |
| | | } else { |
| | |
| | | row.valueMode !== undefined ? String(row.valueMode) : "1"; |
| | | formData.unit = row.unit || ""; |
| | | formData.remark = row.remark || ""; |
| | | formData.paramFormat = row.paramFormat || ""; |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // åæç产ç»è®¡åè |
| | | <template> |
| | | <div class="monthly-suc-page"> |
| | | <el-card shadow="never" class="query-card"> |
| | | <el-form :inline="true" :model="queryForm"> |
| | | <el-form-item label="æ¥è¯¢æä»½"> |
| | | <el-date-picker |
| | | v-model="queryForm.month" |
| | | type="month" |
| | | value-format="YYYY-MM" |
| | | placeholder="éæ©æä»½" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </el-form-item> |
| | | <el-form-item class="toolbar-right"> |
| | | <el-button type="success" @click="handleImport">导å
¥</el-button> |
| | | <el-button type="warning" @click="handleExport">导åº</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <el-card shadow="never" class="table-card"> |
| | | <el-table |
| | | :data="tableRows" |
| | | border |
| | | class="suc-table single-table" |
| | | row-key="itemName" |
| | | :row-class-name="getRowClassName" |
| | | max-height="520" |
| | | > |
| | | <el-table-column prop="itemName" label="项ç®" min-width="120" align="center" fixed="left" /> |
| | | |
| | | <el-table-column prop="a35" label="3.5" min-width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-input-number |
| | | v-if="!row.isYield" |
| | | v-model="row.a35" |
| | | :controls="false" |
| | | :precision="2" |
| | | :min="0" |
| | | size="small" |
| | | class="yellow-input" |
| | | @change="recalculateRow(row)" |
| | | /> |
| | | <span v-else class="yield-cell">{{ formatNumber(row.a35, 0) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitA35" label="åè" min-width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.isYield">-</span> |
| | | <span v-else>{{ formatNumber(row.unitA35, 4) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column prop="a50" label="5.0" min-width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-input-number |
| | | v-if="!row.isYield" |
| | | v-model="row.a50" |
| | | :controls="false" |
| | | :precision="2" |
| | | :min="0" |
| | | size="small" |
| | | class="yellow-input" |
| | | @change="recalculateRow(row)" |
| | | /> |
| | | <span v-else class="yield-cell">{{ formatNumber(row.a50, 0) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitA50" label="åè" min-width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.isYield">-</span> |
| | | <span v-else>{{ formatNumber(row.unitA50, 4) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column prop="board" label="æ¿æ" min-width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-input-number |
| | | v-if="!row.isYield" |
| | | v-model="row.board" |
| | | :controls="false" |
| | | :precision="2" |
| | | :min="0" |
| | | size="small" |
| | | class="yellow-input" |
| | | @change="recalculateRow(row)" |
| | | /> |
| | | <span v-else class="yield-cell">{{ formatNumber(row.board, 0) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="unitBoard" label="åè" min-width="110" align="center"> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.isYield">-</span> |
| | | <span v-else>{{ formatNumber(row.unitBoard, 4) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column prop="actualUsage" :label="actualUsageLabel" min-width="140" align="center"> |
| | | <template #default="{ row }"> |
| | | <span class="yield-cell" v-if="row.isYield">{{ formatNumber(row.actualUsage) }}</span> |
| | | <span v-else>{{ formatNumber(row.actualUsage) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="blockSubtotal" label="ç åå计" min-width="120" align="center"> |
| | | <template #default="{ row }"> |
| | | <span class="yield-cell" v-if="row.isYield">{{ formatNumber(row.blockSubtotal) }}</span> |
| | | <span v-else>{{ formatNumber(row.blockSubtotal) }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, reactive, ref } from "vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | |
| | | const getCurrentMonth = () => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = String(now.getMonth() + 1).padStart(2, "0"); |
| | | return `${year}-${month}`; |
| | | }; |
| | | |
| | | const queryForm = reactive({ |
| | | month: getCurrentMonth(), |
| | | }); |
| | | |
| | | const yieldRow = reactive({ |
| | | itemName: "产é", |
| | | isYield: true, |
| | | a35: 1000, |
| | | a50: 900, |
| | | board: 780, |
| | | actualUsage: 0, |
| | | blockSubtotal: 0, |
| | | }); |
| | | |
| | | const materialRows = ref( |
| | | [ |
| | | "ç²ç
¤ç°", |
| | | "æ°´æ³¥", |
| | | "ç³ç°", |
| | | "éç²", |
| | | "ç³è", |
| | | "è±æ¨¡å", |
| | | "æå
带", |
| | | "å·æ¤ä¸", |
| | | "æ°§åé", |
| | | "塿£", |
| | | "é²è
å", |
| | | ].map((name, idx) => ({ |
| | | itemName: name, |
| | | isYield: false, |
| | | a35: Number((15 + idx * 0.5).toFixed(2)), |
| | | a50: Number((13 + idx * 0.45).toFixed(2)), |
| | | board: Number((10 + idx * 0.4).toFixed(2)), |
| | | unitA35: 0, |
| | | unitA50: 0, |
| | | unitBoard: 0, |
| | | actualUsage: 0, |
| | | blockSubtotal: 0, |
| | | })) |
| | | ); |
| | | |
| | | const monthTitle = computed(() => { |
| | | const monthText = String(queryForm.month || ""); |
| | | if (!monthText.includes("-")) return ""; |
| | | return `${Number(monthText.split("-")[1])}æ`; |
| | | }); |
| | | |
| | | const actualUsageLabel = computed(() => `${monthTitle.value}å®é
ç¨é`); |
| | | |
| | | const tableRows = computed(() => [yieldRow, ...materialRows.value]); |
| | | |
| | | function getRowClassName({ row }) { |
| | | if (row?.isYield) return "is-fixed-yield-row"; |
| | | return ""; |
| | | } |
| | | |
| | | function updateYieldTotals() { |
| | | yieldRow.actualUsage = Number( |
| | | materialRows.value.reduce((sum, row) => sum + Number(row.actualUsage || 0), 0).toFixed(2) |
| | | ); |
| | | yieldRow.blockSubtotal = Number( |
| | | materialRows.value.reduce((sum, row) => sum + Number(row.blockSubtotal || 0), 0).toFixed(2) |
| | | ); |
| | | } |
| | | |
| | | function recalculateRow(row) { |
| | | if (row?.isYield) return; |
| | | |
| | | const a35Yield = Number(yieldRow.a35 || 0); |
| | | const a50Yield = Number(yieldRow.a50 || 0); |
| | | const boardYield = Number(yieldRow.board || 0); |
| | | |
| | | row.unitA35 = a35Yield > 0 ? row.a35 / a35Yield : 0; |
| | | row.unitA50 = a50Yield > 0 ? row.a50 / a50Yield : 0; |
| | | row.unitBoard = boardYield > 0 ? row.board / boardYield : 0; |
| | | |
| | | row.actualUsage = Number(row.a35 || 0) + Number(row.a50 || 0) + Number(row.board || 0); |
| | | row.blockSubtotal = Number(row.a35 || 0) + Number(row.a50 || 0); |
| | | |
| | | updateYieldTotals(); |
| | | } |
| | | |
| | | function initializeRows() { |
| | | materialRows.value.forEach((row) => recalculateRow(row)); |
| | | updateYieldTotals(); |
| | | } |
| | | |
| | | function handleQuery() { |
| | | initializeRows(); |
| | | ElMessage.success(`å·²å è½½ ${queryForm.month} æ°æ®`); |
| | | } |
| | | |
| | | function handleReset() { |
| | | queryForm.month = getCurrentMonth(); |
| | | handleQuery(); |
| | | } |
| | | |
| | | function handleImport() { |
| | | ElMessage.info("请æ¥å
¥å¯¼å
¥æ¥å£æä¸ä¼ ç»ä»¶"); |
| | | } |
| | | |
| | | function handleExport() { |
| | | ElMessage.success(`已触å ${queryForm.month} 导åº`); |
| | | } |
| | | |
| | | function formatNumber(value, fraction = 2) { |
| | | const n = Number(value); |
| | | if (!Number.isFinite(n)) return "-"; |
| | | return n.toLocaleString("zh-CN", { |
| | | minimumFractionDigits: fraction, |
| | | maximumFractionDigits: fraction, |
| | | }); |
| | | } |
| | | |
| | | initializeRows(); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .monthly-suc-page { |
| | | padding: 16px; |
| | | background: #f6f8fa; |
| | | min-height: calc(100vh - 100px); |
| | | } |
| | | |
| | | .query-card, |
| | | .table-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .toolbar-right { |
| | | margin-left: auto !important; |
| | | } |
| | | |
| | | .suc-table :deep(.cell) { |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .yield-cell { |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .single-table :deep(.el-table__body tr.is-fixed-yield-row td) { |
| | | position: sticky; |
| | | top: 0; |
| | | z-index: 6; |
| | | background: #f3f6fb !important; |
| | | font-weight: 800; |
| | | } |
| | | |
| | | .yellow-input :deep(.el-input__wrapper) { |
| | | background: #fff200; |
| | | box-shadow: inset 0 0 0 1px #ddd; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ åææ¬å¯¼å
¥ |
| | | <template> |
| | | <div class="standard-cost-ledger-page"> |
| | | <el-card shadow="never" class="query-card"> |
| | | <el-form :inline="true" :model="queryForm"> |
| | | <el-form-item label="导å
¥æä»½"> |
| | | <el-date-picker |
| | | v-model="queryForm.month" |
| | | type="month" |
| | | value-format="YYYY-MM" |
| | | placeholder="éæ©æä»½" |
| | | @change="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¹æ¬¡å·"> |
| | | <el-select |
| | | v-model="queryForm.batchNo" |
| | | clearable |
| | | filterable |
| | | placeholder="å
¨é¨æ¹æ¬¡" |
| | | style="width: 220px" |
| | | @change="handleQuery" |
| | | > |
| | | <el-option |
| | | v-for="item in batchOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æ¥è¯¢</el-button> |
| | | <el-button @click="handleReset">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </el-card> |
| | | |
| | | <el-card shadow="never" class="ledger-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>æ åææ¬å¯¼å
¥å°è´¦</span> |
| | | <span class="meta">å
± {{ filteredLedgerRows.length }} æ¡å¯¼å
¥è®°å½</span> |
| | | </div> |
| | | </template> |
| | | <el-table |
| | | :data="filteredLedgerRows" |
| | | border |
| | | highlight-current-row |
| | | @current-change="handleLedgerRowChange" |
| | | > |
| | | <el-table-column prop="month" label="æä»½" width="120" /> |
| | | <el-table-column prop="batchNo" label="æ¹æ¬¡å·" width="180" /> |
| | | <el-table-column prop="importTime" label="导å
¥æ¶é´" min-width="180" /> |
| | | <el-table-column prop="importUser" label="导å
¥äºº" width="120" /> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="220" show-overflow-tooltip /> |
| | | </el-table> |
| | | </el-card> |
| | | |
| | | <el-card shadow="never" class="matrix-card"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>æ åææ¬æç»ï¼{{ activeLedgerRow?.month || "-" }}ï¼</span> |
| | | <span class="meta">æ¹æ¬¡ï¼{{ activeLedgerRow?.batchNo || "-" }}</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="matrixRows" border class="matrix-table" :row-class-name="getRowClassName"> |
| | | <el-table-column prop="itemName" label="项ç®åç§°" min-width="180" /> |
| | | <el-table-column |
| | | v-for="column in productColumns" |
| | | :key="column.key" |
| | | :prop="column.key" |
| | | :label="column.label" |
| | | min-width="130" |
| | | align="right" |
| | | > |
| | | <template #default="{ row }"> |
| | | {{ formatNumber(row[column.key]) }} |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-card> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, ref } from "vue"; |
| | | |
| | | const getCurrentMonth = () => { |
| | | const now = new Date(); |
| | | const year = now.getFullYear(); |
| | | const month = String(now.getMonth() + 1).padStart(2, "0"); |
| | | return `${year}-${month}`; |
| | | }; |
| | | |
| | | const queryForm = ref({ |
| | | month: getCurrentMonth(), |
| | | batchNo: "", |
| | | }); |
| | | |
| | | const ledgerRows = ref([ |
| | | { |
| | | id: 1, |
| | | month: "2026-01", |
| | | batchNo: "SC-202601-001", |
| | | importTime: "2026-01-05 09:18:22", |
| | | importUser: "çä¼è®¡", |
| | | remark: "æåæ åææ¬å¯¼å
¥", |
| | | matrix: buildMatrixData(), |
| | | }, |
| | | { |
| | | id: 2, |
| | | month: "2026-02", |
| | | batchNo: "SC-202602-001", |
| | | importTime: "2026-02-03 10:08:16", |
| | | importUser: "æä¼è®¡", |
| | | remark: "æäº§åæ´æ°äººå·¥ä¸è´¹ç¨å£å¾", |
| | | matrix: buildMatrixData({ |
| | | yieldA35: 1050, |
| | | yieldA50: 920, |
| | | yieldBoard: 840, |
| | | directMaterialA35: 12.4, |
| | | directMaterialA50: 11.9, |
| | | directMaterialBoard: 9.2, |
| | | }), |
| | | }, |
| | | { |
| | | id: 3, |
| | | month: "2026-03", |
| | | batchNo: "SC-202603-001", |
| | | importTime: "2026-03-04 14:32:09", |
| | | importUser: "çä¼è®¡", |
| | | remark: "ææå¯¼å
¥æ åææ¬", |
| | | matrix: buildMatrixData({ |
| | | yieldA35: 1100, |
| | | yieldA50: 960, |
| | | yieldBoard: 900, |
| | | directMaterialA35: 12.9, |
| | | directMaterialA50: 12.1, |
| | | directMaterialBoard: 9.6, |
| | | }), |
| | | }, |
| | | ]); |
| | | |
| | | const activeLedgerId = ref(ledgerRows.value[0]?.id || null); |
| | | |
| | | const batchOptions = computed(() => { |
| | | return ledgerRows.value.map((row) => ({ |
| | | label: `${row.batchNo}ï¼${row.month}ï¼`, |
| | | value: row.batchNo, |
| | | })); |
| | | }); |
| | | |
| | | const filteredLedgerRows = computed(() => { |
| | | return ledgerRows.value.filter((row) => { |
| | | const byMonth = !queryForm.value.month || row.month === queryForm.value.month; |
| | | const byBatch = !queryForm.value.batchNo || row.batchNo === queryForm.value.batchNo; |
| | | return byMonth && byBatch; |
| | | }); |
| | | }); |
| | | |
| | | const activeLedgerRow = computed(() => { |
| | | const list = filteredLedgerRows.value; |
| | | const target = list.find((row) => row.id === activeLedgerId.value); |
| | | return target || list[0] || null; |
| | | }); |
| | | |
| | | const matrixRows = computed(() => activeLedgerRow.value?.matrix || []); |
| | | |
| | | const productColumns = [ |
| | | { key: "a35", label: "å æ°å-A3.5" }, |
| | | { key: "a50", label: "å æ°å-A5.0" }, |
| | | { key: "board", label: "æ¿æ" }, |
| | | { key: "total", label: "综å" }, |
| | | ]; |
| | | |
| | | function buildMatrixData(override = {}) { |
| | | const yieldA35 = override.yieldA35 ?? 980; |
| | | const yieldA50 = override.yieldA50 ?? 860; |
| | | const yieldBoard = override.yieldBoard ?? 800; |
| | | const directMaterialA35 = override.directMaterialA35 ?? 12.6; |
| | | const directMaterialA50 = override.directMaterialA50 ?? 11.7; |
| | | const directMaterialBoard = override.directMaterialBoard ?? 9.1; |
| | | const manufacturingA35 = 3.2; |
| | | const manufacturingA50 = 3.5; |
| | | const manufacturingBoard = 2.8; |
| | | const mgmtA35 = 1.4; |
| | | const mgmtA50 = 1.2; |
| | | const mgmtBoard = 1.0; |
| | | const salesA35 = 0.9; |
| | | const salesA50 = 0.8; |
| | | const salesBoard = 0.7; |
| | | const financeA35 = 0.4; |
| | | const financeA50 = 0.3; |
| | | const financeBoard = 0.2; |
| | | const offSeasonA35 = 1.1; |
| | | const offSeasonA50 = 1.0; |
| | | const offSeasonBoard = 0.8; |
| | | |
| | | const subtotalA35 = directMaterialA35 + manufacturingA35; |
| | | const subtotalA50 = directMaterialA50 + manufacturingA50; |
| | | const subtotalBoard = directMaterialBoard + manufacturingBoard; |
| | | |
| | | const totalA35 = subtotalA35 + mgmtA35 + salesA35 + financeA35 + offSeasonA35; |
| | | const totalA50 = subtotalA50 + mgmtA50 + salesA50 + financeA50 + offSeasonA50; |
| | | const totalBoard = subtotalBoard + mgmtBoard + salesBoard + financeBoard + offSeasonBoard; |
| | | |
| | | return [ |
| | | { |
| | | itemName: "产é", |
| | | a35: yieldA35, |
| | | a50: yieldA50, |
| | | board: yieldBoard, |
| | | total: yieldA35 + yieldA50 + yieldBoard, |
| | | }, |
| | | { |
| | | itemName: "ç´æ¥ææåæ¹ææ¬", |
| | | a35: directMaterialA35, |
| | | a50: directMaterialA50, |
| | | board: directMaterialBoard, |
| | | total: (directMaterialA35 + directMaterialA50 + directMaterialBoard) / 3, |
| | | }, |
| | | { |
| | | itemName: "å¶é è´¹ç¨", |
| | | a35: manufacturingA35, |
| | | a50: manufacturingA50, |
| | | board: manufacturingBoard, |
| | | total: (manufacturingA35 + manufacturingA50 + manufacturingBoard) / 3, |
| | | }, |
| | | { |
| | | itemName: "çäº§ææ¬å°è®¡", |
| | | a35: subtotalA35, |
| | | a50: subtotalA50, |
| | | board: subtotalBoard, |
| | | total: (subtotalA35 + subtotalA50 + subtotalBoard) / 3, |
| | | isSubtotal: true, |
| | | }, |
| | | { |
| | | itemName: "管çè´¹ç¨", |
| | | a35: mgmtA35, |
| | | a50: mgmtA50, |
| | | board: mgmtBoard, |
| | | total: (mgmtA35 + mgmtA50 + mgmtBoard) / 3, |
| | | }, |
| | | { |
| | | itemName: "éå®è´¹ç¨", |
| | | a35: salesA35, |
| | | a50: salesA50, |
| | | board: salesBoard, |
| | | total: (salesA35 + salesA50 + salesBoard) / 3, |
| | | }, |
| | | { |
| | | itemName: "è´¢å¡è´¹ç¨", |
| | | a35: financeA35, |
| | | a50: financeA50, |
| | | board: financeBoard, |
| | | total: (financeA35 + financeA50 + financeBoard) / 3, |
| | | }, |
| | | { |
| | | itemName: "æ·¡å£è´¹ç¨-çäº§ææ¬", |
| | | a35: offSeasonA35, |
| | | a50: offSeasonA50, |
| | | board: offSeasonBoard, |
| | | total: (offSeasonA35 + offSeasonA50 + offSeasonBoard) / 3, |
| | | }, |
| | | { |
| | | itemName: "å计", |
| | | a35: totalA35, |
| | | a50: totalA50, |
| | | board: totalBoard, |
| | | total: (totalA35 + totalA50 + totalBoard) / 3, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | function handleQuery() { |
| | | if (filteredLedgerRows.value.length > 0) { |
| | | activeLedgerId.value = filteredLedgerRows.value[0].id; |
| | | return; |
| | | } |
| | | activeLedgerId.value = null; |
| | | } |
| | | |
| | | function handleReset() { |
| | | queryForm.value.month = getCurrentMonth(); |
| | | queryForm.value.batchNo = ""; |
| | | handleQuery(); |
| | | } |
| | | |
| | | function handleLedgerRowChange(row) { |
| | | if (!row) return; |
| | | activeLedgerId.value = row.id; |
| | | } |
| | | |
| | | function getRowClassName({ row }) { |
| | | return row.isSubtotal ? "is-subtotal-row" : ""; |
| | | } |
| | | |
| | | function formatNumber(value) { |
| | | if (typeof value !== "number") return "-"; |
| | | if (Number.isInteger(value)) return value.toLocaleString("zh-CN"); |
| | | return value.toLocaleString("zh-CN", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .standard-cost-ledger-page { |
| | | padding: 16px; |
| | | background: #f6f8fa; |
| | | min-height: calc(100vh - 100px); |
| | | } |
| | | |
| | | .query-card, |
| | | .ledger-card, |
| | | .matrix-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .meta { |
| | | font-size: 13px; |
| | | color: #909399; |
| | | font-weight: 400; |
| | | } |
| | | |
| | | .matrix-table :deep(.is-subtotal-row td) { |
| | | background: #fff200 !important; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="dashboard"> |
| | | <!-- 顶鍿¨ªå两æ --> |
| | | <div class="dashboard-top"> |
| | | <!-- å·¦ï¼ä¼ä¸ä¿¡æ¯+ä¸å¤§æ°æ®å¡çï¼ä¸ä¸æåï¼ --> |
| | | <div class="top-left"> |
| | | <div class="company-info"> |
| | | <!-- é¡¶é¨é®åæ¡ --> |
| | | <div class="welcome-banner"> |
| | | <div class="welcome-title"> |
| | | <span class="welcome-user">{{ userStore.roleName || 'ç³»ç»ç®¡çå' }}</span> |
| | | <span> æ¨å¥½ï¼ç¥æ¨å¼å¿æ¯ä¸å¤©</span> |
| | | </div> |
| | | <div class="welcome-time">ç»å½äº: {{ userStore.currentLoginTime }}</div> |
| | | </div> |
| | | |
| | | <!-- ç¨æ·ä¿¡æ¯å¡ç --> |
| | | <div class="user-card"> |
| | | <img :src="userStore.avatar" class="avatar" alt="" /> |
| | | <div class="user-card-main"> |
| | | <div class="user-name">{{ userStore.name }}</div> |
| | | <div class="user-role">{{ userStore.roleName }}</div> |
| | | <div class="user-meta"> |
| | | <span>{{ userStore.phoneNumber || '123456789' }}</span> |
| | | <span class="sep">|</span> |
| | | <span>{{ userStore.deptName || 'ç»ç»æ¶æ' }}</span> |
| | | <span class="sep">|</span> |
| | | <span>{{ userStore.postName || 'å²ä½å' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="home-page"> |
| | | <div class="top-bar"> |
| | | <div class="user-box"> |
| | | <img :src="userStore.avatar" class="avatar" alt="" /> |
| | | <div> |
| | | <div class="hello">{{ userStore.roleName || "ç³»ç»ç®¡çå" }}ï¼ä½ 好</div> |
| | | <div class="sub">ç»å½æ¶é´ï¼{{ userStore.currentLoginTime }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="data-cards"> |
| | | <div class="data-card sales"> |
| | | <div class="data-title">é宿°æ®</div> |
| | | <div class="data-num"> |
| | | <div> |
| | | <div class="data-desc">æ¬æéå®é¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthSaleMoney }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="data-desc">æªå¼ç¥¨éé¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthSaleHaveMoney }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> |
| | | <div class="data-card purchase"> |
| | | <div class="data-title">éè´æ°æ®</div> |
| | | <div class="data-num"> |
| | | <div> |
| | | <div class="data-desc">æ¬æéè´é¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthPurchaseMoney }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="data-desc">å¾
仿¬¾éé¢/å
</div> |
| | | <div class="data-value">{{ businessInfo.monthPurchaseHaveMoney }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="data-card inventory"> |
| | | <div class="data-title">åºåæ°æ®</div> |
| | | <div class="data-num"> |
| | | <div> |
| | | <div class="data-desc">å½ååºåæ»é/ä»¶</div> |
| | | <div class="data-value">{{ businessInfo.inventoryNum }}</div> |
| | | </div> |
| | | <div> |
| | | <div class="data-desc">仿¥å
¥åº/ä»¶</div> |
| | | <div class="data-value">{{ businessInfo.todayInventoryNum }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- å³ï¼å¾
åäºé¡¹ --> |
| | | <div class="todo-panel"> |
| | | <div class="section-title">å¾
åäºé¡¹</div> |
| | | <ul class="todo-list" v-if="todoList.length > 0"> |
| | | <li v-for="item in todoList" :key="item.id"> |
| | | <div style="display: flex;flex-direction: column;justify-content: space-between;width: 100%;gap: 20px"> |
| | | <div style="display: flex;justify-content: space-between;align-items: center;"> |
| | | <div class="todo-title">å¾
åç¼å·ï¼{{ item.approveId }}</div> |
| | | <div class="todo-division">é¨é¨ï¼{{ item.approveDeptName }}</div> |
| | | <div class="todo-time">{{ item.approveTime }}</div> |
| | | </div> |
| | | <div class="todo-division">å¾
åäºç±ï¼{{ item.approveReason }}</div> |
| | | </div> |
| | | </li> |
| | | </ul> |
| | | <div v-else style="text-align: center"> |
| | | ææ æ°æ® |
| | | </div> |
| | | <div class="top-actions"> |
| | | <span class="refresh-time">æ°æ®æ´æ°æ¶é´ï¼{{ lastUpdatedAt || "-" }}</span> |
| | | <el-button size="small" type="primary" plain @click="refreshDashboardData">å·æ°æ°æ®</el-button> |
| | | <el-button size="small" plain @click="configDialogVisible = true">é¦é¡µé
ç½®</el-button> |
| | | </div> |
| | | </div> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel process-panel"> |
| | | <div class="process-panel__header"> |
| | | <div class="section-title">å·¥åºæ°æ®ç产ç»è®¡æç»</div> |
| | | <div style="display: flex; gap: 10px; align-items: center;"> |
| | | <el-button type="primary" size="small" plain icon="Filter" @click="openProcessDialog">鿩工åº</el-button> |
| | | <el-button type="info" size="small" plain icon="Refresh" @click="resetProcessFilter">éç½®</el-button> |
| | | <el-radio-group v-model="processRange" size="small" @change="refreshProcessStats"> |
| | | <el-radio-button :value="1">æ¥</el-radio-button> |
| | | <el-radio-button :value="2">å¨</el-radio-button> |
| | | <el-radio-button :value="3">æ</el-radio-button> |
| | | |
| | | <div class="content-grid"> |
| | | <div class="left-col"> |
| | | <section class="section-card"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">å¿«æ·æä½</div> |
| | | <el-button size="small" type="primary" link @click="openShortcutDialog">æ·»å å¿«æ·å
¥å£</el-button> |
| | | </div> |
| | | <div class="quick-grid"> |
| | | <el-button |
| | | v-for="item in shortcuts" |
| | | :key="`${item.label}-${item.path}`" |
| | | :type="item.invalid ? 'danger' : 'default'" |
| | | @click="goTo(item.path)" |
| | | > |
| | | {{ item.label }} |
| | | </el-button> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card"> |
| | | <div class="section-title">éç¹å¾
å</div> |
| | | <div class="todo-row" v-for="todo in todos" :key="todo.title"> |
| | | <el-tag size="small" :type="todo.type">{{ todo.level }}</el-tag> |
| | | <span>{{ todo.title }}</span> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card"> |
| | | <div class="section-title">ç»è¥å
³æ³¨</div> |
| | | <div class="focus-row" v-for="item in businessFocus" :key="item.name"> |
| | | <span class="focus-name">{{ item.name }}</span> |
| | | <span class="focus-value">{{ item.value }}</span> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card flex-fill-card"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">仿¥å¾
å¤ç</div> |
| | | <el-radio-group v-model="pendingFilter" size="small"> |
| | | <el-radio-button label="all">å
¨é¨</el-radio-button> |
| | | <el-radio-button label="mine">æç</el-radio-button> |
| | | <el-radio-button label="high">é«ä¼å
</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="process-panel__body"> |
| | | <div class="process-panel__chart"> |
| | | <Echarts :chartStyle="{ width: '100%', height: '100%' }" :grid="processGrid" :series="processSeries" |
| | | :tooltip="processTooltip" :xAxis="processXAxis" :yAxis="processYAxis" style="height: 100%" |
| | | @click="handleChartClick" /> |
| | | <div class="task-row" v-for="task in filteredPendingTasks" :key="task.id"> |
| | | <div class="task-left"> |
| | | <el-tag size="small" :type="task.type">{{ task.level }}</el-tag> |
| | | <span class="task-title">{{ task.title }}</span> |
| | | </div> |
| | | <el-button link type="primary" @click="goTo(task.path)">å»å¤ç</el-button> |
| | | </div> |
| | | <el-empty v-if="filteredPendingTasks.length === 0" description="ææ å¾
å¤çäºé¡¹" :image-size="80" /> |
| | | </section> |
| | | </div> |
| | | |
| | | <div class="process-panel__aside"> |
| | | <div class="process-legend"> |
| | | <div class="process-legend__item"> |
| | | <span class="dot dot-blue"></span><span>æå
¥é</span> |
| | | <div class="right-col"> |
| | | <section class="section-card" v-if="isSectionVisible('trendCards')"> |
| | | <div class="section-title">æè¿7天å
³é®ææ è¶å¿</div> |
| | | <div class="trend-cards"> |
| | | <div class="trend-card clickable" v-for="card in recentTrendCards" :key="card.key" @click="handleTrendCardClick(card)"> |
| | | <div class="trend-head"> |
| | | <span class="trend-label">{{ card.label }}</span> |
| | | <span class="trend-rate" :class="trendClass(card.change)"> |
| | | {{ card.change > 0 ? "+" : "" }}{{ card.change.toFixed(1) }}% |
| | | </span> |
| | | </div> |
| | | <div class="process-legend__item"> |
| | | <span class="dot dot-yellow"></span><span>æ¥åºé</span> |
| | | </div> |
| | | <div class="process-legend__item"> |
| | | <span class="dot dot-teal"></span><span>产åºé</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="process-card process-card--name">{{ processAside.processName }}</div> |
| | | |
| | | <div class="process-card"> |
| | | <div class="process-card__label">ç´¯è®¡æ»æå
¥</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalInput) }} |
| | | </div> |
| | | </div> |
| | | <div class="process-card"> |
| | | <div class="process-card__label">ç´¯è®¡æ»æ¥åº</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalScrap) }} |
| | | </div> |
| | | </div> |
| | | <div class="process-card"> |
| | | <div class="process-card__label">累计æ»äº§åº</div> |
| | | <div class="process-card__value">{{ formatAmount(processAside.totalOutput) }} |
| | | <div class="trend-value">{{ card.latest }} {{ card.unit }}</div> |
| | | <div class="sparkline"> |
| | | <span |
| | | v-for="(v, idx) in card.values" |
| | | :key="`${card.key}-${idx}`" |
| | | class="sparkline-bar" |
| | | :style="{ height: `${calcBarHeight(v, card.values)}%` }" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="section-card" v-if="isSectionVisible('planTrend')"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">计åä¸ç产è¶å¿</div> |
| | | <el-radio-group v-model="chartRangePlan" size="small" @change="loadPlanTrend"> |
| | | <el-radio-button :label="1">æ¥</el-radio-button> |
| | | <el-radio-button :label="2">å¨</el-radio-button> |
| | | <el-radio-button :label="3">æ</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <Echarts |
| | | :chartStyle="chartStyle" |
| | | :legend="planLegend" |
| | | :grid="grid" |
| | | :tooltip="lineTooltip" |
| | | :xAxis="planXAxis" |
| | | :yAxis="valueYAxis" |
| | | :series="planSeries" |
| | | style="height: 300px" |
| | | /> |
| | | </section> |
| | | |
| | | <div class="row-two" v-if="isSectionVisible('qualityChart') || isSectionVisible('costChart')"> |
| | | <section class="section-card" v-if="isSectionVisible('qualityChart')"> |
| | | <div class="section-title-row"> |
| | | <div class="section-title">è´¨æ£å¼å¸¸åå¸</div> |
| | | <el-radio-group v-model="chartRangeQuality" size="small" @change="loadQualityData"> |
| | | <el-radio-button :label="1">å¨</el-radio-button> |
| | | <el-radio-button :label="2">æ</el-radio-button> |
| | | <el-radio-button :label="3">å£åº¦</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <Echarts |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | :tooltip="barTooltip" |
| | | :xAxis="qualityXAxis" |
| | | :yAxis="valueYAxis" |
| | | :series="qualitySeries" |
| | | style="height: 260px" |
| | | /> |
| | | </section> |
| | | <section class="section-card" v-if="isSectionVisible('costChart')"> |
| | | <div class="section-title">è½è䏿æ¬ç»æ</div> |
| | | <Echarts |
| | | :chartStyle="chartStyle" |
| | | :legend="costLegend" |
| | | :tooltip="pieTooltip" |
| | | :series="costSeries" |
| | | style="height: 260px" |
| | | /> |
| | | </section> |
| | | </div> |
| | | |
| | | <section class="section-card" v-if="isSectionVisible('warningCenter')"> |
| | | <div class="section-title">å¼å¸¸é¢è¦ä¸å¿</div> |
| | | <div class="warning-row" v-for="item in warningList" :key="item.id"> |
| | | <div class="warning-left"> |
| | | <el-tag size="small" :type="item.levelType">{{ item.levelText }}</el-tag> |
| | | <span class="warning-title">{{ item.title }}</span> |
| | | </div> |
| | | <el-button link type="danger" @click="goTo(item.path)">å¤ç</el-button> |
| | | </div> |
| | | <el-empty v-if="warningList.length === 0" description="ææ å¼å¸¸é¢è¦" :image-size="80" /> |
| | | </section> |
| | | |
| | | <section class="section-card mini-table-wrap" v-if="isSectionVisible('planTable')"> |
| | | <div class="section-title">çäº§è®¡åæ§è¡æç»</div> |
| | | <el-table :data="planTable" size="small" stripe> |
| | | <el-table-column prop="planNo" label="计ååå·" min-width="150" /> |
| | | <el-table-column prop="product" label="产å" min-width="120" /> |
| | | <el-table-column prop="qty" label="计åé" min-width="90" /> |
| | | <el-table-column prop="issued" label="å·²ä¸å" min-width="90" /> |
| | | <el-table-column prop="status" label="ç¶æ" min-width="100" /> |
| | | <el-table-column label="æä½" min-width="120"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="primary" @click="goTo(routePathMap.plan)">æ¥ç</el-button> |
| | | <el-button |
| | | v-if="row.status !== '已宿'" |
| | | link |
| | | type="success" |
| | | @click="goTo(routePathMap.dispatch)" |
| | | > |
| | | ä¸å |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </section> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- å·¥åºéæ©å¼¹çª --> |
| | | <el-dialog v-model="processDialogVisible" title="鿩工åº" width="500px" append-to-body> |
| | | <div class="process-selection-wrapper"> |
| | | <el-checkbox-group v-model="tempProcessIds"> |
| | | <div class="process-grid"> |
| | | <el-checkbox v-for="item in processOptions" :key="item.id" :label="item.id" border> |
| | | {{ item.name }} |
| | | </el-checkbox> |
| | | </div> |
| | | </el-checkbox-group> |
| | | <el-dialog v-model="shortcutDialogVisible" title="æ·»å å¿«æ·å
¥å£ï¼æå¤6个ï¼" width="680px"> |
| | | <div class="shortcut-form-row"> |
| | | <el-tree-select |
| | | v-model="selectedPagePath" |
| | | placeholder="è¯·éæ©é¡µé¢ï¼ç®å½ä¸å¯éï¼" |
| | | filterable |
| | | clearable |
| | | check-strictly |
| | | node-key="value" |
| | | :data="menuTreeOptions" |
| | | :props="{ label: 'label', value: 'value', children: 'children', disabled: 'disabled' }" |
| | | style="grid-column: span 2" |
| | | /> |
| | | <el-button type="success" @click="addShortcutBySelect">éæ©æ·»å </el-button> |
| | | </div> |
| | | <el-table :data="shortcuts" size="small" border> |
| | | <el-table-column prop="label" label="åç§°" min-width="220" /> |
| | | <el-table-column label="ç¶æ" min-width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag size="small" :type="row.invalid ? 'danger' : 'success'">{{ row.invalid ? "æ æ" : "ææ" }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" min-width="90" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button link type="danger" @click="removeShortcut($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="configDialogVisible" title="é¦é¡µæ¨¡åé
ç½®" width="520px"> |
| | | <el-checkbox-group v-model="enabledSectionKeys" class="config-check-group"> |
| | | <el-checkbox v-for="item in sectionConfigOptions" :key="item.key" :label="item.key"> |
| | | {{ item.label }} |
| | | </el-checkbox> |
| | | </el-checkbox-group> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="processDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="handleProcessDialogConfirm">确认</el-button> |
| | | </span> |
| | | <el-button @click="configDialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="saveSectionConfig">ä¿å</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- ä¸é¨æ¨ªå两æ --> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel"> |
| | | <div class="section-title">客æ·ååéé¢åæ</div> |
| | | <div class="contract-summary"> |
| | | <div class="contract-info"> |
| | | <img src="../assets/images/khtitle.png" alt="" style="width: 42px" /> |
| | | <div class="contract-card"> |
| | | <div class="contract-name">æ»ååéé¢(å
)</div> |
| | | <div class="contract-meta"> |
| | | <div class="main-amount">{{ sum }}</div> |
| | | <div>å¨åæ¯: <span class="up">{{ yny }}% </span> æ¥ç¯æ¯: <span class="up">{{ chain }}% </span></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div |
| | | style="display: flex;align-items: center;gap: 20px;justify-content: space-evenly;height: 180px;margin-top: 20px"> |
| | | <div> |
| | | <Echarts ref="chart" :legend="pieLegend" :chartStyle="chartStylePie" :series="materialPieSeries" |
| | | :tooltip="pieTooltip"></Echarts> |
| | | </div> |
| | | <ul class="contract-list"> |
| | | <li v-for="item in materialPieSeries[0].data" :key="item.name"> |
| | | <div style="display: flex;align-items: center;justify-content: space-between;width: 100%"> |
| | | <div class="line" :style="{ color: item.itemStyle.color }">â{{ item.name }}</div> |
| | | <div style="width: 70px">{{ item.rate }}%</div> |
| | | <div>ï¿¥{{ item.value }}</div> |
| | | </div> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | </div> |
| | | <div class="main-panel"> |
| | | <div style="display: flex;justify-content: space-between;"> |
| | | <div class="section-title">åºæ¶åºä»ç»è®¡</div> |
| | | <!-- <el-radio-group v-model="radio1" size="large" @change="statisticsReceivable">--> |
| | | <!-- <el-radio-button label="æå¨" :value="1" />--> |
| | | <!-- <el-radio-button label="ææ" :value="2" />--> |
| | | <!-- <el-radio-button label="æå£åº¦" :value="3" />--> |
| | | <!-- </el-radio-group>--> |
| | | </div> |
| | | <Echarts ref="chart" :color="barColors2" :chartStyle="chartStyle" :grid="grid" :series="barSeries" |
| | | :tooltip="tooltip" :xAxis="xAxis" :yAxis="yAxis" style="height: 260px"></Echarts> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- åºé¨æ¨ªå两æ --> |
| | | <div class="dashboard-row"> |
| | | <div class="main-panel"> |
| | | <div style="display: flex;justify-content: space-between;align-items: center;margin-bottom: 10px;"> |
| | | <div class="section-title" style="margin-bottom: 0;">è´¨éç»è®¡</div> |
| | | <el-radio-group v-model="qualityRange" size="small" @change="qualityStatisticsInfo"> |
| | | <el-radio-button :value="1">å¨</el-radio-button> |
| | | <el-radio-button :value="2">æ</el-radio-button> |
| | | <el-radio-button :value="3">å£åº¦</el-radio-button> |
| | | </el-radio-group> |
| | | </div> |
| | | <div class="quality-cards"> |
| | | <div class="quality-card one">åææå·²æ£æµæ° <span>{{ qualityStatisticsObject.supplierNum }}ä»¶</span></div> |
| | | <div class="quality-card two">è¿ç¨æ£éªæ°é <span>{{ qualityStatisticsObject.processNum }}ä»¶</span></div> |
| | | <div class="quality-card three">åºåå·²æ£æ°é <span>{{ qualityStatisticsObject.factoryNum }}ä»¶</span></div> |
| | | </div> |
| | | <Echarts ref="chart" :chartStyle="chartStyle" :grid="grid" :legend="barLegend" :series="barSeries1" |
| | | :tooltip="tooltip" :xAxis="xAxis1" :yAxis="yAxis1" style="height: 260px"></Echarts> |
| | | </div> |
| | | |
| | | <div class="main-panel"> |
| | | <div class="section-title">忬¾ä¸å¼ç¥¨åæ</div> |
| | | <Echarts ref="invoiceChart" :chartStyle="chartStyle" :grid="grid" :legend="lineLegend" :series="lineSeries" |
| | | :tooltip="tooltipLine" :xAxis="xAxis2" :yAxis="yAxis2" style="height: 270px;" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, computed, reactive } from 'vue' |
| | | import { computed, onMounted, reactive, ref } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import { ElMessage } from "element-plus"; |
| | | import Echarts from "@/components/Echarts/echarts.vue"; |
| | | import * as echarts from 'echarts'; |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import usePermissionStore from "@/store/modules/permission"; |
| | | import { |
| | | analysisCustomerContractAmounts, getAmountHalfYear, |
| | | getBusiness, |
| | | expenseCompositionAnalysis, |
| | | getProgressStatistics, |
| | | homeTodos, |
| | | orderCount, |
| | | processDataProductionStatistics, |
| | | statisticsReceivablePayable, |
| | | qualityInspectionStatistics |
| | | qualityInspectionStatistics, |
| | | nonComplianceWarning, |
| | | } from "@/api/viewIndex.js"; |
| | | import { list } from '@/api/productionManagement/productionProcess'; |
| | | |
| | | const router = useRouter(); |
| | | const userStore = useUserStore(); |
| | | const permissionStore = usePermissionStore(); |
| | | |
| | | const userStore = useUserStore() |
| | | const SHORTCUT_STORAGE_KEY = "home-shortcuts-v1"; |
| | | |
| | | const processOptions = ref([]) |
| | | const selectedProcessIds = ref([]) |
| | | const tempProcessIds = ref([]) |
| | | const processDialogVisible = ref(false) |
| | | const activeProcessIndex = ref(0) |
| | | const defaultShortcuts = [ |
| | | { label: "主ç产计å", path: "/productionManagement/productionPlan" }, |
| | | { label: "ç产订å", path: "/productionManagement/productionOrder" }, |
| | | { label: "ç产æ¥å·¥", path: "/productionManagement/productionReporting" }, |
| | | { label: "è¿ç¨æ£", path: "/qualityManagement/processInspection" }, |
| | | { label: "ç产è½è", path: "/energyManagement/productionEnergyConsumption" }, |
| | | { label: "çäº§ææ¬", path: "/costAccounting/productionCostAccounting" }, |
| | | { label: "æ åvså®é
", path: "/costAccounting/stdVsActCostAnalysis" }, |
| | | { label: "å³çåæ", path: "/reportAnalysis/dataDashboard" }, |
| | | ]; |
| | | |
| | | const businessInfo = ref({ |
| | | inventoryNum: 0, |
| | | monthPurchaseHaveMoney: 0, |
| | | monthPurchaseMoney: 0, |
| | | monthSaleHaveMoney: 0, |
| | | monthSaleMoney: 0, |
| | | todayInventoryNum: 0, |
| | | }) |
| | | const qualityStatisticsObject = ref({ |
| | | supplierNum: 0, |
| | | processNum: 0, |
| | | factoryNum: 0, |
| | | }) |
| | | const sum = ref(0) |
| | | const yny = ref(0) |
| | | const chain = ref(0) |
| | | const isRouteValid = (path) => { |
| | | if (!path || !path.startsWith("/")) return false; |
| | | const resolved = router.resolve(path); |
| | | return resolved.matched && resolved.matched.length > 0; |
| | | }; |
| | | |
| | | const pieLegend = reactive({ |
| | | show: false, |
| | | }) |
| | | const barSeries = ref([ |
| | | { |
| | | type: 'bar', |
| | | data: [], |
| | | label: { |
| | | show: true, |
| | | const withValidFlag = (list) => |
| | | list.map((item) => ({ |
| | | ...item, |
| | | invalid: !isRouteValid(item.path), |
| | | })); |
| | | |
| | | const pageOptions = router |
| | | .getRoutes() |
| | | .filter((route) => { |
| | | const hasTitle = Boolean(route.meta?.title); |
| | | const validPath = route.path && route.path.startsWith("/") && !route.path.includes(":"); |
| | | const isVisibleMenu = !route.meta?.hidden && route.path !== "/index"; |
| | | const notSpecial = |
| | | !route.path.includes("redirect") && |
| | | route.path !== "/login" && |
| | | route.path !== "/register" && |
| | | route.path !== "/401" && |
| | | !route.path.includes(":pathMatch"); |
| | | return hasTitle && validPath && isVisibleMenu && notSpecial; |
| | | }) |
| | | .map((route) => ({ |
| | | title: route.meta.title, |
| | | path: route.path, |
| | | })) |
| | | .sort((a, b) => a.path.localeCompare(b.path)); |
| | | |
| | | const normalizePath = (path) => String(path || "").replace(/\/+/g, "/"); |
| | | const joinPath = (parentPath, childPath) => { |
| | | if (!childPath) return normalizePath(parentPath || "/"); |
| | | if (String(childPath).startsWith("/")) return normalizePath(childPath); |
| | | return normalizePath(`${parentPath || ""}/${childPath}`); |
| | | }; |
| | | |
| | | const buildMenuTreeOptions = (routes = [], parentPath = "") => { |
| | | const result = []; |
| | | routes.forEach((route) => { |
| | | if (!route || route.hidden) return; |
| | | const fullPath = joinPath(parentPath, route.path); |
| | | const children = buildMenuTreeOptions(route.children || [], fullPath); |
| | | const title = route.meta?.title; |
| | | if (!title && children.length > 0) { |
| | | result.push(...children); |
| | | return; |
| | | } |
| | | }, |
| | | ]) |
| | | if (!title) return; |
| | | result.push({ |
| | | label: title, |
| | | value: fullPath, |
| | | disabled: children.length > 0, |
| | | children, |
| | | }); |
| | | }); |
| | | return result; |
| | | }; |
| | | |
| | | const barSeries1 = ref([ |
| | | { |
| | | name: 'åææä¸åæ ¼æ°', |
| | | type: 'bar', |
| | | barGap: 0, |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | data: [] |
| | | }, |
| | | { |
| | | name: 'è¿ç¨ä¸åæ ¼æ°', |
| | | type: 'bar', |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | data: [] |
| | | }, |
| | | { |
| | | name: 'åºåä¸åæ ¼æ°', |
| | | type: 'bar', |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | data: [] |
| | | }, |
| | | ]) |
| | | const chartStyle = { |
| | | width: '100%', |
| | | height: '100%' // 设置å¾è¡¨å®¹å¨çé«åº¦ |
| | | } |
| | | const chartStylePie = { |
| | | width: '140%', |
| | | height: '140%' // 设置å¾è¡¨å®¹å¨çé«åº¦ |
| | | } |
| | | const grid = { |
| | | left: '3%', |
| | | right: '4%', |
| | | bottom: '3%', |
| | | containLabel: true |
| | | } |
| | | const barLegend = { |
| | | show: true, |
| | | data: ['åææä¸åæ ¼æ°', 'è¿ç¨ä¸åæ ¼æ°', 'åºåä¸åæ ¼æ°'] |
| | | } |
| | | const barLegend1 = { |
| | | show: true, |
| | | data: ['é¢ä»è´¦æ¬¾', 'åºä»è´¦æ¬¾', '颿¶è´¦æ¬¾', 'åºæ¶è´¦æ¬¾'] |
| | | } |
| | | const lineLegend = { |
| | | show: true, |
| | | data: ['å¼ç¥¨', '忬¾'] |
| | | } |
| | | const tooltip = { |
| | | trigger: 'axis', |
| | | axisPointer: { |
| | | type: 'shadow' |
| | | } |
| | | } |
| | | const xAxis = [{ |
| | | type: 'value', |
| | | }] |
| | | const xAxis1 = ref([{ |
| | | type: 'category', |
| | | axisTick: { show: false }, |
| | | data: [] |
| | | }]) |
| | | const yAxis = [{ |
| | | type: 'category', |
| | | data: ['åºä»è´¦æ¬¾', 'åºæ¶è´¦æ¬¾',] |
| | | }] |
| | | const yAxis1 = [{ |
| | | type: 'value' |
| | | }] |
| | | const pieTooltip = reactive({ |
| | | trigger: 'item', |
| | | formatter: function (params) { |
| | | // å¨æçææç¤ºä¿¡æ¯ï¼åºäºæ°æ®é¡¹ç name 屿§ |
| | | const description = params.name === 'æ¬æåæ¬¾éé¢' ? 'æ¬æåæ¬¾éé¢' : 'åºæ¶æ¬¾éé¢'; |
| | | return `${description} ${formatNumber(params.value)}å
${params.percent}%`; |
| | | }, |
| | | position: 'right' |
| | | }) |
| | | const materialPieSeries = ref([ |
| | | { |
| | | type: 'pie', |
| | | radius: ['66%', '90%'], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderColor: '#fff', |
| | | borderWidth: 2 |
| | | }, |
| | | label: { |
| | | show: false |
| | | }, |
| | | data: [] |
| | | } |
| | | ]) |
| | | const lineSeries = ref([ |
| | | { |
| | | type: 'line', |
| | | data: [], |
| | | label: { |
| | | show: true |
| | | }, |
| | | showSymbol: true, // æ¾ç¤ºåç¹ |
| | | }, |
| | | ]) |
| | | const tooltipLine = { |
| | | trigger: 'axis', |
| | | } |
| | | const yAxis2 = ref([ |
| | | { |
| | | type: 'value', |
| | | } |
| | | ]) |
| | | const xAxis2 = ref([ |
| | | { |
| | | type: 'category', |
| | | data: [], |
| | | axisLabel: { |
| | | interval: 0, |
| | | formatter: function (value) { |
| | | return value.replace(/~/g, '\n'); |
| | | }, |
| | | } |
| | | } |
| | | ]) |
| | | const menuTreeOptions = computed(() => buildMenuTreeOptions(permissionStore.sidebarRouters || [])); |
| | | const selectableMenuMap = computed(() => { |
| | | const map = new Map(); |
| | | const walk = (list = []) => { |
| | | list.forEach((item) => { |
| | | if (!item.disabled) map.set(item.value, item.label); |
| | | if (item.children?.length) walk(item.children); |
| | | }); |
| | | }; |
| | | walk(menuTreeOptions.value); |
| | | return map; |
| | | }); |
| | | |
| | | // å¾
åäºé¡¹ |
| | | const todoList = ref([]) |
| | | const radio1 = ref(1) |
| | | const qualityRange = ref(1) |
| | | const keywordMap = { |
| | | "主ç产计å": ["ç产计å", "productionPlan"], |
| | | "ç产订å": ["ç产订å", "productionOrder"], |
| | | "ç产æ¥å·¥": ["æ¥å·¥", "productionReporting"], |
| | | "è¿ç¨æ£": ["è¿ç¨æ£", "processInspection"], |
| | | "ç产è½è": ["ç产è½è", "productionEnergyConsumption"], |
| | | "çäº§ææ¬": ["çäº§ææ¬", "productionCostAccounting"], |
| | | "æ åvså®é
": ["æ å", "å®é
", "stdVsActCostAnalysis"], |
| | | "å³çåæ": ["å³ç", "çæ¿", "dataDashboard"], |
| | | }; |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const barChart = ref(null) |
| | | const lineChart = ref(null) |
| | | const barColors2 = ['#5181DB', '#D369E0', '#F2CA6D', '#60CCA8'] |
| | | const findRouteByKeywords = (keywords = []) => { |
| | | const lowerKeywords = keywords.map((k) => String(k).toLowerCase()); |
| | | return pageOptions.find((item) => { |
| | | const title = String(item.title || "").toLowerCase(); |
| | | const path = String(item.path || "").toLowerCase(); |
| | | return lowerKeywords.some((k) => title.includes(k) || path.includes(k)); |
| | | }); |
| | | }; |
| | | |
| | | // éæºé¢è²çæå½æ° |
| | | const getRandomColor = () => { |
| | | return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0'); |
| | | } |
| | | const getPathByKeywords = (keywords = []) => findRouteByKeywords(keywords)?.path || ""; |
| | | |
| | | onMounted(() => { |
| | | getBusinessData() |
| | | analysisCustomer() |
| | | todoInfoS() |
| | | statisticsReceivable() |
| | | qualityStatisticsInfo() |
| | | getAmountHalfYearNum() |
| | | getProcessList() |
| | | }) |
| | | // æ°æ®ç»è®¡ |
| | | const getBusinessData = () => { |
| | | getBusiness().then((res) => { |
| | | businessInfo.value = { ...res.data } |
| | | }) |
| | | } |
| | | // ååéé¢ |
| | | const analysisCustomer = () => { |
| | | analysisCustomerContractAmounts().then((res) => { |
| | | sum.value = res.data.sum |
| | | yny.value = res.data.yny |
| | | chain.value = res.data.chain |
| | | // 为æ¯ä¸ªæ°æ®é¡¹åé
éæºé¢è² |
| | | materialPieSeries.value[0].data = res.data.item.map(item => ({ |
| | | ...item, |
| | | itemStyle: { color: getRandomColor() } |
| | | })) |
| | | }) |
| | | } |
| | | // å¾
åäºé¡¹ |
| | | const todoInfoS = () => { |
| | | homeTodos().then((res) => { |
| | | todoList.value = res.data |
| | | }) |
| | | } |
| | | // è·åå·¥åºå表 |
| | | const getProcessList = () => { |
| | | list().then(res => { |
| | | processOptions.value = res.data |
| | | }) |
| | | } |
| | | |
| | | const openProcessDialog = () => { |
| | | tempProcessIds.value = [...selectedProcessIds.value] |
| | | processDialogVisible.value = true |
| | | } |
| | | |
| | | const handleProcessDialogConfirm = () => { |
| | | selectedProcessIds.value = [...tempProcessIds.value] |
| | | processDialogVisible.value = false |
| | | refreshProcessStats() |
| | | } |
| | | |
| | | const resetProcessFilter = () => { |
| | | selectedProcessIds.value = [] |
| | | tempProcessIds.value = [] |
| | | refreshProcessStats() |
| | | } |
| | | |
| | | const handleChartClick = (params) => { |
| | | if (params && params.dataIndex !== undefined) { |
| | | activeProcessIndex.value = params.dataIndex |
| | | } |
| | | } |
| | | // åºä»åºæ¶ç»è®¡ |
| | | const statisticsReceivable = () => { |
| | | statisticsReceivablePayable({ type: radio1.value }).then((res) => { |
| | | barSeries.value[0].data = [ |
| | | // { value: res.data.prepayMoney, itemStyle: { color: barColors2[0] } }, |
| | | { value: res.data.payableMoney, itemStyle: { color: barColors2[0] } }, |
| | | // { value: res.data.advanceMoney, itemStyle: { color: barColors2[2] } }, |
| | | { value: res.data.receivableMoney, itemStyle: { color: barColors2[1] } } |
| | | ] |
| | | }) |
| | | } |
| | | // è´¨æ£ç»è®¡ |
| | | const qualityStatisticsInfo = () => { |
| | | qualityInspectionStatistics({ type: qualityRange.value }).then((res) => { |
| | | xAxis1.value[0].data = [] |
| | | barSeries1.value[0].data = [] |
| | | barSeries1.value[1].data = [] |
| | | barSeries1.value[2].data = [] |
| | | res.data.item.forEach(item => { |
| | | xAxis1.value[0].data.push(item.date) |
| | | barSeries1.value[0].data.push(item.supplierNum) |
| | | barSeries1.value[1].data.push(item.processNum) |
| | | barSeries1.value[2].data.push(item.factoryNum) |
| | | const getRecommendedShortcuts = () => { |
| | | const list = defaultShortcuts |
| | | .map((item) => { |
| | | const matched = findRouteByKeywords(keywordMap[item.label] || [item.label]); |
| | | return matched ? { label: item.label, path: matched.path } : null; |
| | | }) |
| | | qualityStatisticsObject.value.supplierNum = res.data.supplierNum |
| | | qualityStatisticsObject.value.processNum = res.data.processNum |
| | | qualityStatisticsObject.value.factoryNum = res.data.factoryNum |
| | | }) |
| | | } |
| | | const getAmountHalfYearNum = async () => { |
| | | const res = await getAmountHalfYear() |
| | | console.log(res) |
| | | const monthName = [] |
| | | const receiptAmount = [] |
| | | const invoiceAmount = [] |
| | | res.data.forEach(item => { |
| | | monthName.push(item.month) |
| | | receiptAmount.push(item.receiptAmount) |
| | | invoiceAmount.push(item.invoiceAmount) |
| | | }) |
| | | // æ£ç¡®ååºå¼èµå¼ï¼å建æ°ç xAxis å series 对象 |
| | | xAxis2.value[0].data = monthName |
| | | xAxis2.value[0].data = monthName.map(item => item.replace(/~/g, '\n~')); |
| | | lineSeries.value = [ |
| | | { |
| | | name: 'å¼ç¥¨', |
| | | type: 'line', |
| | | data: invoiceAmount, |
| | | stack: 'Total', |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { |
| | | offset: 0, |
| | | color: 'rgba(131, 207, 255, 1)' |
| | | }, |
| | | { |
| | | offset: 1, |
| | | color: 'rgba(186, 228, 255, 1)' |
| | | } |
| | | ]) |
| | | }, |
| | | itemStyle: { |
| | | color: '#2D99FF', |
| | | borderColor: '#2D99FF' |
| | | }, |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | lineStyle: { |
| | | width: 0 |
| | | }, |
| | | showSymbol: true, |
| | | }, |
| | | { |
| | | name: '忬¾', |
| | | type: 'line', |
| | | data: receiptAmount, |
| | | stack: 'Total', |
| | | lineStyle: { |
| | | width: 0 |
| | | }, |
| | | itemStyle: { |
| | | color: '#83CFFF', |
| | | borderColor: '#83CFFF' |
| | | }, |
| | | showSymbol: true, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { |
| | | offset: 0, |
| | | color: 'rgba(54, 153, 255, 1)' |
| | | }, |
| | | { |
| | | offset: 1, |
| | | color: 'rgba(89, 169, 254, 1)' |
| | | } |
| | | ]) |
| | | }, |
| | | emphasis: { |
| | | focus: 'series' |
| | | }, |
| | | } |
| | | ] |
| | | } |
| | | .filter(Boolean); |
| | | return list.length > 0 ? list : defaultShortcuts; |
| | | }; |
| | | |
| | | // å·¥åºæ°æ®ç产ç»è®¡æç»ï¼åæ°æ® + å¾è¡¨ï¼ |
| | | const processRange = ref(1) |
| | | const processChartData = ref([]) |
| | | const tryRepairSavedShortcut = (item) => { |
| | | const matched = findRouteByKeywords(keywordMap[item.label] || [item.label]); |
| | | if (matched) return { label: item.label, path: matched.path }; |
| | | return item; |
| | | }; |
| | | |
| | | const processXAxis = ref([ |
| | | const getSavedShortcuts = () => { |
| | | const recommended = getRecommendedShortcuts(); |
| | | try { |
| | | const saved = localStorage.getItem(SHORTCUT_STORAGE_KEY); |
| | | if (!saved) return recommended; |
| | | const parsed = JSON.parse(saved); |
| | | if (!Array.isArray(parsed) || parsed.length === 0) return recommended; |
| | | return parsed.map((item) => tryRepairSavedShortcut(item)); |
| | | } catch (error) { |
| | | return recommended; |
| | | } |
| | | }; |
| | | |
| | | const shortcuts = reactive(withValidFlag(getSavedShortcuts().slice(0, 6))); |
| | | const shortcutDialogVisible = ref(false); |
| | | const configDialogVisible = ref(false); |
| | | const selectedPagePath = ref(""); |
| | | const lastUpdatedAt = ref(""); |
| | | const pendingFilter = ref("all"); |
| | | const chartRangePlan = ref(3); |
| | | const chartRangeQuality = ref(2); |
| | | |
| | | const routePathMap = { |
| | | plan: getPathByKeywords(["ç产计å", "productionPlan"]), |
| | | order: getPathByKeywords(["ç产订å", "productionOrder"]), |
| | | processInspection: getPathByKeywords(["è¿ç¨æ£", "processInspection"]), |
| | | meter: getPathByKeywords(["æè¡¨", "meterCollection", "è½è"]), |
| | | dispatch: getPathByKeywords(["ç产è°åº¦", "productionDispatching"]), |
| | | }; |
| | | |
| | | const persistShortcuts = () => { |
| | | localStorage.setItem( |
| | | SHORTCUT_STORAGE_KEY, |
| | | JSON.stringify(shortcuts.slice(0, 6).map(({ label, path }) => ({ label, path }))) |
| | | ); |
| | | }; |
| | | |
| | | const todos = reactive([]); |
| | | |
| | | const businessFocus = reactive([ |
| | | { name: "çäº§è®¢åæ»æ°", value: "-" }, |
| | | { name: "å·²å®æè®¢åæ°", value: "-" }, |
| | | { name: "æªå®æè®¢åæ°", value: "-" }, |
| | | { name: "é¨åå®æè®¢åæ°", value: "-" }, |
| | | { name: "è´¨æ£æ»æ°", value: "-" }, |
| | | { name: "è¿ç¨æ£æ»æ°", value: "-" }, |
| | | ]); |
| | | |
| | | const pendingTasks = reactive([]); |
| | | const warningList = reactive([]); |
| | | const SECTION_CONFIG_KEY = "home-sections-v1"; |
| | | const sectionConfigOptions = [ |
| | | { key: "trendCards", label: "æè¿7天è¶å¿å¡" }, |
| | | { key: "planTrend", label: "计åä¸ç产è¶å¿å¾" }, |
| | | { key: "qualityChart", label: "è´¨æ£å¼å¸¸åå¸å¾" }, |
| | | { key: "costChart", label: "è½è䏿æ¬ç»æå¾" }, |
| | | { key: "warningCenter", label: "å¼å¸¸é¢è¦ä¸å¿" }, |
| | | { key: "planTable", label: "çäº§è®¡åæ§è¡æç»è¡¨" }, |
| | | ]; |
| | | const enabledSectionKeys = ref(sectionConfigOptions.map((i) => i.key)); |
| | | |
| | | const chartStyle = { width: "100%", height: "100%" }; |
| | | const grid = { left: "3%", right: "4%", bottom: "3%", containLabel: true }; |
| | | const lineTooltip = { trigger: "axis" }; |
| | | const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } }; |
| | | const pieTooltip = { trigger: "item" }; |
| | | |
| | | const valueYAxis = [{ type: "value" }]; |
| | | const planXAxis = [{ type: "category", data: [] }]; |
| | | const qualityXAxis = [{ type: "category", data: [] }]; |
| | | |
| | | const planLegend = { show: true, data: ["计åé", "ä¸åé", "宿é"] }; |
| | | const costLegend = { |
| | | show: true, |
| | | orient: "vertical", |
| | | right: 10, |
| | | top: "center", |
| | | data: ["è½èææ¬", "çäº§ææ¬", "è´¨éæå¤±ææ¬", "å
¶ä»ææ¬"], |
| | | }; |
| | | |
| | | const planSeries = reactive([ |
| | | { name: "计åé", type: "line", smooth: true, data: [] }, |
| | | { name: "ä¸åé", type: "line", smooth: true, data: [] }, |
| | | { name: "宿é", type: "line", smooth: true, data: [] }, |
| | | ]); |
| | | |
| | | const qualitySeries = reactive([ |
| | | { |
| | | nameTextStyle: { color: 'rgba(0,0,0,0.35)', fontSize: 12 }, |
| | | axisLabel: { color: 'rgba(0,0,0,0.35)' }, |
| | | splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)', type: 'dashed' } }, |
| | | }, |
| | | ]) |
| | | |
| | | const processYAxis = ref([ |
| | | { |
| | | type: 'category', |
| | | axisTick: { show: false }, |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: 'rgba(0,0,0,0.45)' }, |
| | | name: "å¼å¸¸æ°", |
| | | type: "bar", |
| | | barWidth: 26, |
| | | itemStyle: { color: "#e67e22", borderRadius: [6, 6, 0, 0] }, |
| | | data: [], |
| | | }, |
| | | ]) |
| | | ]); |
| | | |
| | | const processGrid = reactive({ left: 0, right: 100, top: 30, bottom: 20, containLabel: true }) |
| | | |
| | | const processTooltip = reactive({ |
| | | trigger: 'axis', |
| | | axisPointer: { type: 'shadow' }, |
| | | formatter: (params) => { |
| | | const name = params?.[0]?.name ?? '' |
| | | const list = Array.isArray(params) ? params : [] |
| | | const lines = list |
| | | .map((p) => { |
| | | const colorBox = `<span style="display:inline-block;margin-right:6px;border-radius:2px;width:10px;height:10px;background:${p.color}"></span>` |
| | | return `${colorBox}${p.seriesName} <b style="float:right;">${Number(p.value || 0).toFixed(2)}</b>` |
| | | }) |
| | | .join('<br/>') |
| | | return `<div style="min-width:140px;"><div style="font-weight:700;margin-bottom:6px;">${name}</div>${lines}</div>` |
| | | const costSeries = reactive([ |
| | | { |
| | | type: "pie", |
| | | radius: ["45%", "68%"], |
| | | center: ["35%", "50%"], |
| | | label: { formatter: "{b}: {d}%" }, |
| | | data: [], |
| | | }, |
| | | }) |
| | | ]); |
| | | |
| | | const processSeries = computed(() => { |
| | | const input = processChartData.value.map((i) => i.input) |
| | | const scrap = processChartData.value.map((i) => i.scrap) |
| | | const output = processChartData.value.map((i) => i.output) |
| | | const planTable = reactive([]); |
| | | const recentTrendCards = reactive([ |
| | | { key: "planIssued", label: "计åä¸åé", unit: "å", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | { key: "qualityRaw", label: "æ¥ææ£æ°é", unit: "æ¡", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | { key: "qualityProcess", label: "è¿ç¨æ£æ°é", unit: "æ¡", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | { key: "qualityFactory", label: "æåæ£æ°é", unit: "æ¡", values: [0, 0, 0, 0, 0, 0, 0], latest: 0, change: 0 }, |
| | | ]); |
| | | |
| | | return [ |
| | | { |
| | | name: 'æå
¥é', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#1E5BFF', borderRadius: [6, 0, 0, 6] }, |
| | | data: input, |
| | | }, |
| | | { |
| | | name: 'æ¥åºé', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#F7B500' }, |
| | | data: scrap, |
| | | }, |
| | | { |
| | | name: '产åºé', |
| | | type: 'bar', |
| | | stack: 'total', |
| | | barWidth: 22, |
| | | itemStyle: { color: '#19C6C6', borderRadius: [0, 6, 6, 0] }, |
| | | data: output, |
| | | }, |
| | | ] |
| | | }) |
| | | const toNumber = (value) => { |
| | | const num = Number(value); |
| | | return Number.isFinite(num) ? num : 0; |
| | | }; |
| | | |
| | | const processAside = computed(() => { |
| | | const list = processChartData.value |
| | | const item = list[activeProcessIndex.value] || {} |
| | | return { |
| | | processName: item.name || 'ææ æ°æ®', |
| | | totalInput: item.input || 0, |
| | | totalScrap: item.scrap || 0, |
| | | totalOutput: item.output || 0, |
| | | const pickFirstNumber = (obj, keys = []) => { |
| | | for (const key of keys) { |
| | | if (obj && obj[key] !== undefined && obj[key] !== null) return toNumber(obj[key]); |
| | | } |
| | | }) |
| | | return 0; |
| | | }; |
| | | |
| | | const formatAmount = (n) => { |
| | | const num = Number(n || 0) |
| | | return num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) |
| | | } |
| | | const updateArray = (target, list) => { |
| | | target.splice(0, target.length, ...list); |
| | | }; |
| | | |
| | | const refreshProcessStats = () => { |
| | | processDataProductionStatistics({ |
| | | type: processRange.value, |
| | | processIds: selectedProcessIds.value.length > 0 ? selectedProcessIds.value.join(',') : null |
| | | }).then(res => { |
| | | processChartData.value = res.data.map(item => ({ |
| | | name: item.processName, |
| | | input: item.totalInput, |
| | | scrap: item.totalScrap, |
| | | output: item.totalOutput |
| | | })) |
| | | processYAxis.value[0].data = processChartData.value.map((i) => i.name) |
| | | activeProcessIndex.value = 0 |
| | | }) |
| | | } |
| | | const toFixedOne = (num) => Number(num || 0).toFixed(1); |
| | | |
| | | const normalizeSeven = (list = []) => { |
| | | const nums = list.map((i) => toNumber(i)); |
| | | if (nums.length >= 7) return nums.slice(-7); |
| | | return [...Array(7 - nums.length).fill(0), ...nums]; |
| | | }; |
| | | |
| | | const calcTrend = (list = []) => { |
| | | if (!Array.isArray(list) || list.length === 0) return { latest: 0, change: 0 }; |
| | | const first = toNumber(list[0]); |
| | | const latest = toNumber(list[list.length - 1]); |
| | | if (first === 0) return { latest, change: latest > 0 ? 100 : 0 }; |
| | | return { latest, change: ((latest - first) / first) * 100 }; |
| | | }; |
| | | |
| | | const setTrendCard = (key, values) => { |
| | | const target = recentTrendCards.find((i) => i.key === key); |
| | | if (!target) return; |
| | | const series = normalizeSeven(values); |
| | | const { latest, change } = calcTrend(series); |
| | | target.values = series; |
| | | target.latest = latest; |
| | | target.change = Number(toFixedOne(change)); |
| | | }; |
| | | |
| | | const trendClass = (change) => (change > 0 ? "up" : change < 0 ? "down" : "flat"); |
| | | |
| | | const calcBarHeight = (value, list) => { |
| | | const max = Math.max(...list, 1); |
| | | return Math.max(18, Math.round((toNumber(value) / max) * 100)); |
| | | }; |
| | | |
| | | const filteredPendingTasks = computed(() => { |
| | | if (pendingFilter.value === "high") return pendingTasks.filter((i) => i.level === "é«"); |
| | | if (pendingFilter.value === "mine") { |
| | | const currentUserName = String(userStore?.name || "").toLowerCase(); |
| | | const currentUserId = String(userStore?.userId || ""); |
| | | return pendingTasks.filter((i) => { |
| | | const ownerName = String(i.ownerName || "").toLowerCase(); |
| | | const ownerId = String(i.ownerId || ""); |
| | | return (currentUserName && ownerName && ownerName.includes(currentUserName)) || (currentUserId && ownerId === currentUserId); |
| | | }); |
| | | } |
| | | return pendingTasks; |
| | | }); |
| | | |
| | | const isSectionVisible = (key) => enabledSectionKeys.value.includes(key); |
| | | |
| | | const goTo = (path) => { |
| | | if (!isRouteValid(path)) { |
| | | ElMessage.warning("å½åèåæªé
ç½®è¯¥é¡µé¢ææ è®¿é®æé"); |
| | | return; |
| | | } |
| | | router.push(path); |
| | | }; |
| | | |
| | | const handleTrendCardClick = (card) => { |
| | | const mapping = { |
| | | planIssued: routePathMap.plan || routePathMap.order, |
| | | qualityRaw: routePathMap.processInspection, |
| | | qualityProcess: routePathMap.processInspection, |
| | | qualityFactory: routePathMap.processInspection, |
| | | }; |
| | | const target = mapping[card.key]; |
| | | if (!target) { |
| | | ElMessage.warning("æªé
ç½®å¯è·³è½¬é¡µé¢"); |
| | | return; |
| | | } |
| | | const query = |
| | | card.key === "planIssued" |
| | | ? { dateType: String(chartRangePlan.value), source: "homeTrend" } |
| | | : { dateType: String(chartRangeQuality.value), source: "homeTrend" }; |
| | | router.push({ path: target, query }); |
| | | }; |
| | | |
| | | const openShortcutDialog = () => { |
| | | shortcutDialogVisible.value = true; |
| | | }; |
| | | |
| | | const addShortcutBySelect = () => { |
| | | if (shortcuts.length >= 6) { |
| | | ElMessage.warning("å¿«æ·å
¥å£æå¤åªè½æ·»å 6个"); |
| | | return; |
| | | } |
| | | if (!selectedPagePath.value) { |
| | | ElMessage.warning("请å
鿩页é¢"); |
| | | return; |
| | | } |
| | | if (shortcuts.some((item) => item.path === selectedPagePath.value)) { |
| | | ElMessage.warning("该快æ·å
¥å£å·²åå¨"); |
| | | return; |
| | | } |
| | | const label = selectableMenuMap.value.get(selectedPagePath.value); |
| | | if (!label) { |
| | | ElMessage.warning("è¯·éæ©å¯æ·»å ç页é¢ï¼ç®å½èç¹ä¸å¯é"); |
| | | return; |
| | | } |
| | | shortcuts.push({ |
| | | label, |
| | | path: selectedPagePath.value, |
| | | invalid: !isRouteValid(selectedPagePath.value), |
| | | }); |
| | | persistShortcuts(); |
| | | selectedPagePath.value = ""; |
| | | }; |
| | | |
| | | const removeShortcut = (index) => { |
| | | shortcuts.splice(index, 1); |
| | | persistShortcuts(); |
| | | ElMessage.success("å·²å é¤å¿«æ·å
¥å£"); |
| | | }; |
| | | |
| | | const loadHomeTodos = async () => { |
| | | try { |
| | | const res = await homeTodos(); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.slice(0, 4).map((item, idx) => { |
| | | const text = item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | const levelType = idx === 0 ? "danger" : idx <= 2 ? "warning" : "success"; |
| | | const level = idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½"; |
| | | return { level, title: text, type: levelType }; |
| | | }); |
| | | updateArray(todos, mapped); |
| | | const pendingMapped = list.slice(0, 4).map((item, idx) => { |
| | | const title = item?.approveReason || item?.approveTypeName || `å¾
å¤çäºé¡¹ ${idx + 1}`; |
| | | const path = inferTodoPath(item); |
| | | return { |
| | | id: item?.id || `${idx}-${title}`, |
| | | title, |
| | | level: idx === 0 ? "é«" : idx <= 2 ? "ä¸" : "ä½", |
| | | type: idx === 0 ? "danger" : idx <= 2 ? "warning" : "success", |
| | | path, |
| | | ownerId: item?.approveUserId || item?.userId || "", |
| | | ownerName: item?.approveUserName || item?.userName || "", |
| | | }; |
| | | }); |
| | | updateArray(pendingTasks, pendingMapped); |
| | | } catch (error) { |
| | | console.error("homeTodosæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const loadOrderAndProgress = async () => { |
| | | try { |
| | | const [orderRes, progressRes] = await Promise.allSettled([orderCount(), getProgressStatistics()]); |
| | | |
| | | if (orderRes.status === "fulfilled") { |
| | | const items = Array.isArray(orderRes.value?.data) ? orderRes.value.data : []; |
| | | const byName = Object.fromEntries( |
| | | items.map((i) => [String(i?.name || "").replace(/\s/g, ""), i?.value]) |
| | | ); |
| | | businessFocus[0].value = `${pickFirstNumber(byName, ["çäº§è®¢åæ°", "çäº§è®¢åæ»æ°", "æ»è®¢åæ°"]) || 0} å`; |
| | | businessFocus[1].value = `${pickFirstNumber(byName, ["å·²å®æè®¢åæ°"]) || 0} å`; |
| | | businessFocus[2].value = `${pickFirstNumber(byName, ["å¾
çäº§è®¢åæ°", "æªå®æè®¢åæ°"]) || 0} å`; |
| | | businessFocus[3].value = `${pickFirstNumber(byName, ["é¨åå®æè®¢åæ°"]) || 0} å`; |
| | | } |
| | | |
| | | if (progressRes.status === "fulfilled") { |
| | | const p = progressRes.value?.data || {}; |
| | | const detail = Array.isArray(p.completedOrderDetails) ? p.completedOrderDetails : []; |
| | | const rows = detail.slice(0, 6).map((item, index) => { |
| | | const qty = pickFirstNumber(item, ["quantity", "planQuantity"]); |
| | | const done = pickFirstNumber(item, ["completeQuantity", "completedQuantity"]); |
| | | return { |
| | | planNo: item.npsNo || item.productionPlanNo || `NO-${index + 1}`, |
| | | product: item.productCategory || item.productName || "-", |
| | | qty, |
| | | issued: done, |
| | | status: qty > 0 && done >= qty ? "已宿" : done > 0 ? "æ§è¡ä¸" : "å¾
ä¸å", |
| | | }; |
| | | }); |
| | | updateArray(planTable, rows); |
| | | setTrendCard( |
| | | "planIssued", |
| | | detail.slice(-7).map((i) => pickFirstNumber(i, ["completeQuantity", "completedQuantity", "issueNum"])) |
| | | ); |
| | | |
| | | } |
| | | } catch (error) { |
| | | console.error("orderCount/getProgressStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const inferTodoPath = (todo) => { |
| | | const text = `${todo?.approveTypeName || ""}${todo?.approveReason || ""}`.toLowerCase(); |
| | | if (text.includes("计å")) return routePathMap.plan || routePathMap.order; |
| | | if (text.includes("订å")) return routePathMap.order || routePathMap.plan; |
| | | if (text.includes("è¿ç¨æ£") || text.includes("è´¨æ£")) return routePathMap.processInspection || routePathMap.plan; |
| | | if (text.includes("è½è") || text.includes("æè¡¨")) return routePathMap.meter || routePathMap.plan; |
| | | return routePathMap.plan || routePathMap.order || ""; |
| | | }; |
| | | |
| | | const loadPlanTrend = async () => { |
| | | try { |
| | | const res = await processDataProductionStatistics({ type: chartRangePlan.value }); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | planXAxis[0].data = list.map((i, index) => i.processName || `å·¥åº${index + 1}`); |
| | | planSeries[0].data = list.map((i) => pickFirstNumber(i, ["totalInput", "input", "planNum"])); |
| | | planSeries[1].data = list.map((i) => pickFirstNumber(i, ["totalOutput", "output", "issueNum"])); |
| | | planSeries[2].data = list.map((i) => pickFirstNumber(i, ["totalScrap", "scrap", "completeNum"])); |
| | | } catch (error) { |
| | | console.error("processDataProductionStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const loadQualityData = async () => { |
| | | try { |
| | | const res = await qualityInspectionStatistics({ type: chartRangeQuality.value }); |
| | | const data = res?.data || {}; |
| | | const items = Array.isArray(data.item) ? data.item : []; |
| | | if (items.length > 0) { |
| | | qualityXAxis[0].data = items.map((i) => i.date || i.name || "-"); |
| | | qualitySeries[0].data = items.map((i) => |
| | | pickFirstNumber(i, ["supplierNum", "processNum", "factoryNum", "totalNum"]) |
| | | ); |
| | | setTrendCard("qualityRaw", items.map((i) => pickFirstNumber(i, ["supplierNum"]))); |
| | | setTrendCard("qualityProcess", items.map((i) => pickFirstNumber(i, ["processNum"]))); |
| | | setTrendCard("qualityFactory", items.map((i) => pickFirstNumber(i, ["factoryNum"]))); |
| | | } else { |
| | | qualityXAxis[0].data = ["æ¥ææ£", "è¿ç¨æ£", "æåæ£"]; |
| | | qualitySeries[0].data = [ |
| | | pickFirstNumber(data, ["supplierNum"]), |
| | | pickFirstNumber(data, ["processNum"]), |
| | | pickFirstNumber(data, ["factoryNum"]), |
| | | ]; |
| | | setTrendCard("qualityRaw", [pickFirstNumber(data, ["supplierNum"])]); |
| | | setTrendCard("qualityProcess", [pickFirstNumber(data, ["processNum"])]); |
| | | setTrendCard("qualityFactory", [pickFirstNumber(data, ["factoryNum"])]); |
| | | } |
| | | businessFocus[4].value = `${pickFirstNumber(data, ["supplierNum", "totalNum"])} æ¡`; |
| | | businessFocus[5].value = `${pickFirstNumber(data, ["processNum"])} æ¡`; |
| | | } catch (error) { |
| | | console.error("qualityInspectionStatisticsæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const loadWarningCenter = async () => { |
| | | try { |
| | | const res = await nonComplianceWarning(); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.slice(0, 6).map((item, idx) => { |
| | | const levelNum = toNumber(item.level ?? item.warningLevel ?? 2); |
| | | const levelType = levelNum >= 3 ? "danger" : levelNum === 2 ? "warning" : "info"; |
| | | const levelText = levelNum >= 3 ? "é«" : levelNum === 2 ? "ä¸" : "ä½"; |
| | | const title = item.name || item.title || item.paramName || `å¼å¸¸é¢è¦ ${idx + 1}`; |
| | | const text = `${title}${item.processName || ""}${item.orderNo || ""}`.toLowerCase(); |
| | | const path = text.includes("è´¨æ£") |
| | | ? routePathMap.processInspection |
| | | : text.includes("订å") |
| | | ? routePathMap.order |
| | | : routePathMap.processInspection || routePathMap.order || routePathMap.plan; |
| | | return { id: item.id || `${idx}-${title}`, levelType, levelText, title, path }; |
| | | }); |
| | | updateArray(warningList, mapped); |
| | | } catch (error) { |
| | | console.error("nonComplianceWarningæ¥å£è·å失败:", error); |
| | | updateArray(warningList, []); |
| | | } |
| | | }; |
| | | |
| | | const initSectionConfig = () => { |
| | | try { |
| | | const raw = localStorage.getItem(SECTION_CONFIG_KEY); |
| | | if (!raw) return; |
| | | const parsed = JSON.parse(raw); |
| | | if (Array.isArray(parsed) && parsed.length > 0) { |
| | | enabledSectionKeys.value = parsed.filter((k) => sectionConfigOptions.some((i) => i.key === k)); |
| | | } |
| | | } catch (error) { |
| | | console.error("读åé¦é¡µé
置失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const saveSectionConfig = () => { |
| | | if (enabledSectionKeys.value.length === 0) { |
| | | ElMessage.warning("è³å°ä¿çä¸ä¸ªæ¨¡å"); |
| | | return; |
| | | } |
| | | localStorage.setItem(SECTION_CONFIG_KEY, JSON.stringify(enabledSectionKeys.value)); |
| | | configDialogVisible.value = false; |
| | | ElMessage.success("é¦é¡µé
置已ä¿å"); |
| | | }; |
| | | |
| | | const loadCostComposition = async () => { |
| | | try { |
| | | const res = await expenseCompositionAnalysis({ type: 1 }); |
| | | const list = Array.isArray(res?.data) ? res.data : []; |
| | | const mapped = list.map((i) => ({ |
| | | name: i.name || "æªå½å", |
| | | value: pickFirstNumber(i, ["value", "amount", "cost"]), |
| | | })); |
| | | costSeries[0].data = mapped; |
| | | } catch (error) { |
| | | console.error("expenseCompositionAnalysisæ¥å£è·å失败:", error); |
| | | } |
| | | }; |
| | | |
| | | const refreshDashboardData = () => { |
| | | loadHomeTodos(); |
| | | loadOrderAndProgress(); |
| | | loadPlanTrend(); |
| | | loadQualityData(); |
| | | loadCostComposition(); |
| | | loadWarningCenter(); |
| | | lastUpdatedAt.value = new Date().toLocaleString(); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getBusinessData() |
| | | analysisCustomer() |
| | | todoInfoS() |
| | | statisticsReceivable() |
| | | qualityStatisticsInfo() |
| | | getAmountHalfYearNum() |
| | | refreshProcessStats() |
| | | }) |
| | | initSectionConfig(); |
| | | refreshDashboardData(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .dashboard { |
| | | background: #f5f7fa; |
| | | .home-page { |
| | | min-height: 100vh; |
| | | background: #f5f7fb; |
| | | padding: 20px; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .dashboard-top { |
| | | .top-bar { |
| | | display: flex; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | align-items: flex-start; |
| | | justify-content: space-evenly; |
| | | } |
| | | |
| | | .company-info { |
| | | padding: 0; |
| | | overflow: hidden; |
| | | border-radius: 12px; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 16px; |
| | | background: #fff; |
| | | height: 100%; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .welcome-banner { |
| | | padding: 10px 10px; |
| | | background: linear-gradient(135deg, rgba(229, 240, 255, 0.9), rgba(214, 232, 255, 0.7), rgba(207, 236, 255, 0.9)); |
| | | } |
| | | |
| | | .welcome-title { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #222; |
| | | line-height: 1.3; |
| | | } |
| | | |
| | | .welcome-user { |
| | | margin-right: 6px; |
| | | } |
| | | |
| | | .welcome-time { |
| | | margin-top: 10px; |
| | | font-size: 16px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | } |
| | | |
| | | .user-card { |
| | | .top-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 18px 22px; |
| | | } |
| | | |
| | | .user-card-main { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .user-name { |
| | | font-size: 16px; |
| | | font-weight: bold; |
| | | color: #111; |
| | | letter-spacing: 1px; |
| | | } |
| | | |
| | | .user-role { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | height: 20px; |
| | | padding: 5px 10px; |
| | | background: rgba(245, 246, 248, 1); |
| | | color: #333; |
| | | width: fit-content; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .user-meta { |
| | | .refresh-time { |
| | | font-size: 12px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | color: #7b8794; |
| | | } |
| | | |
| | | .user-meta .sep { |
| | | margin: 0 10px; |
| | | color: rgba(0, 0, 0, 0.25); |
| | | .user-box { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .avatar { |
| | | width: 90px; |
| | | height: 90px; |
| | | width: 54px; |
| | | height: 54px; |
| | | border-radius: 50%; |
| | | object-fit: cover; |
| | | flex: 0 0 auto; |
| | | } |
| | | |
| | | .data-cards { |
| | | width: 50%; |
| | | display: flex; |
| | | gap: 16px; |
| | | justify-content: flex-start; |
| | | background: #ffffff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .data-title { |
| | | font-weight: 700; |
| | | font-size: 26px; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .data-num { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | .data-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 14px 10px 10px 10px; |
| | | min-width: 160px; |
| | | box-shadow: 0 2px 8px #eee; |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 32%; |
| | | height: 140px; |
| | | } |
| | | |
| | | .data-card.sales { |
| | | background-image: url("../assets/images/xioashoushuju.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-card.purchase { |
| | | background-image: url("../assets/images/caigou.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-card.inventory { |
| | | background-image: url("../assets/images/kucun.png"); |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .data-desc { |
| | | font-weight: 500; |
| | | font-size: 13px; |
| | | color: #FFFFFF; |
| | | } |
| | | |
| | | .data-value { |
| | | .hello { |
| | | font-size: 18px; |
| | | font-weight: 500; |
| | | margin: 10px 0; |
| | | color: #FFFFFF; |
| | | font-weight: 700; |
| | | color: #1f2d3d; |
| | | } |
| | | |
| | | .top-left { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | height: 180px; |
| | | width: 20%; |
| | | } |
| | | |
| | | .todo-panel { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | height: 180px; |
| | | width: 30%; |
| | | } |
| | | |
| | | .todo-list { |
| | | height: 100px; |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | font-size: 15px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .todo-list li { |
| | | border-radius: 8px; |
| | | margin-bottom: 12px; |
| | | padding: 8px 20px; |
| | | height: 74px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | background: rgba(225, 227, 250, 0.62); |
| | | } |
| | | |
| | | .todo-title { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | position: relative; |
| | | } |
| | | |
| | | .todo-title::before { |
| | | content: ''; |
| | | /* å¿
éï¼è¡¨ç¤ºè¿éæä¸ä¸ªå
容 */ |
| | | position: absolute; |
| | | left: -10px; |
| | | /* å®ä½å°å·¦ä¾§ */ |
| | | top: 50%; |
| | | /* åç´å±
ä¸ */ |
| | | transform: translateY(-50%); |
| | | /* å¾®è°åç´å±
ä¸ */ |
| | | width: 6px; |
| | | /* åçç´å¾ */ |
| | | height: 6px; |
| | | /* åçç´å¾ */ |
| | | background: #498CEB; |
| | | border-radius: 50%; |
| | | /* 让å
¶åæåå½¢ */ |
| | | } |
| | | |
| | | .todo-division { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | } |
| | | |
| | | .todo-time { |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: #000000; |
| | | } |
| | | |
| | | .todo-meta { |
| | | color: #888; |
| | | .sub { |
| | | margin-top: 4px; |
| | | color: #6b7785; |
| | | font-size: 13px; |
| | | } |
| | | |
| | | .dashboard-row { |
| | | display: flex; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | .content-grid { |
| | | display: grid; |
| | | grid-template-columns: 320px 1fr; |
| | | gap: 16px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | .main-panel { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 20px; |
| | | flex: 1; |
| | | min-width: 0; |
| | | .left-col, |
| | | .right-col { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .section-card { |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | padding: 16px; |
| | | margin-bottom: 16px; |
| | | box-shadow: 0 2px 10px rgba(20, 35, 90, 0.06); |
| | | } |
| | | |
| | | .flex-fill-card { |
| | | flex: 1; |
| | | } |
| | | |
| | | .section-title { |
| | | position: relative; |
| | | font-size: 18px; |
| | | color: #333; |
| | | padding-left: 10px; |
| | | margin-bottom: 10px; |
| | | margin-bottom: 14px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | color: #243447; |
| | | } |
| | | |
| | | .section-title::before { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | content: ''; |
| | | width: 4px; |
| | | height: 18px; |
| | | background-color: #002FA7; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .contract-info { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 20px; |
| | | height: 90px; |
| | | background: rgba(245, 245, 245, 0.59); |
| | | width: 100%; |
| | | border-radius: 10px; |
| | | padding: 10px 30px; |
| | | } |
| | | |
| | | .contract-summary { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 30px; |
| | | } |
| | | |
| | | .contract-card { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .contract-name { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #050505; |
| | | } |
| | | |
| | | .contract-meta { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 100%; |
| | | gap: 80px; |
| | | } |
| | | |
| | | .main-amount { |
| | | font-size: 24px; |
| | | color: rgba(51, 50, 50, 0.85); |
| | | } |
| | | |
| | | .up { |
| | | color: #e57373; |
| | | } |
| | | |
| | | .contract-list { |
| | | margin-top: 16px; |
| | | font-size: 14px; |
| | | color: #666; |
| | | list-style: none; |
| | | padding: 0; |
| | | height: 190px; |
| | | overflow-y: auto; |
| | | width: 460px; |
| | | } |
| | | |
| | | .line { |
| | | position: relative; |
| | | width: 230px; |
| | | } |
| | | |
| | | .line::after { |
| | | content: ''; |
| | | position: absolute; |
| | | right: 2px; |
| | | top: 0; |
| | | bottom: 0; |
| | | width: 1px; |
| | | background-color: #C9C5C5; |
| | | border-radius: 2px; |
| | | } |
| | | |
| | | .contract-list li { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .quality-cards { |
| | | display: flex; |
| | | gap: 12px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .quality-card { |
| | | border-radius: 8px; |
| | | padding: 15px 10px 10px 50px; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: rgba(0, 0, 0, 0.67); |
| | | width: 236px; |
| | | height: 49px; |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .quality-card.one { |
| | | background-image: url("../assets/images/yuancailiao.png"); |
| | | } |
| | | |
| | | .quality-card.two { |
| | | background-image: url("../assets/images/guocheng.png"); |
| | | } |
| | | |
| | | .quality-card.three { |
| | | background-image: url("../assets/images/chuchang.png"); |
| | | |
| | | } |
| | | |
| | | .quality-card span { |
| | | color: #4fc3f7; |
| | | font-weight: bold; |
| | | margin-left: 6px; |
| | | } |
| | | |
| | | .chart { |
| | | width: 100%; |
| | | height: 220px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .process-panel { |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .process-panel__header { |
| | | .section-title-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .process-panel__body { |
| | | display: flex; |
| | | gap: 24px; |
| | | align-items: stretch; |
| | | margin-top: 10px; |
| | | .section-title::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 4px; |
| | | width: 4px; |
| | | height: 16px; |
| | | border-radius: 2px; |
| | | background: #409eff; |
| | | } |
| | | |
| | | .process-panel__chart { |
| | | flex: 1; |
| | | min-width: 0; |
| | | padding: 6px 0; |
| | | } |
| | | |
| | | .process-panel__aside { |
| | | width: 260px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .process-legend { |
| | | display: flex; |
| | | flex-direction: column; |
| | | .quick-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 10px; |
| | | align-items: flex-start; |
| | | padding: 8px 6px; |
| | | } |
| | | |
| | | .process-legend__item { |
| | | .quick-grid :deep(.el-button) { |
| | | margin-left: 0; |
| | | } |
| | | |
| | | .shortcut-form-row { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1.5fr auto; |
| | | gap: 10px; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .todo-row { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 13px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | } |
| | | |
| | | .dot { |
| | | width: 10px; |
| | | height: 10px; |
| | | border-radius: 2px; |
| | | display: inline-block; |
| | | } |
| | | |
| | | .dot-blue { |
| | | background: #1E5BFF; |
| | | } |
| | | |
| | | .dot-yellow { |
| | | background: #F7B500; |
| | | } |
| | | |
| | | .dot-teal { |
| | | background: #19C6C6; |
| | | } |
| | | |
| | | .process-card { |
| | | background: rgba(245, 247, 250, 0.9); |
| | | border-radius: 10px; |
| | | padding: 16px 16px; |
| | | } |
| | | |
| | | .process-card--name { |
| | | background: rgba(235, 242, 255, 1); |
| | | color: #1E5BFF; |
| | | font-weight: 800; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .process-card__label { |
| | | font-size: 13px; |
| | | color: rgba(0, 0, 0, 0.55); |
| | | gap: 10px; |
| | | margin-bottom: 10px; |
| | | font-size: 13px; |
| | | color: #3b4a5b; |
| | | } |
| | | |
| | | .process-card__value { |
| | | font-size: 24px; |
| | | font-weight: 800; |
| | | color: rgba(0, 0, 0, 0.8); |
| | | .focus-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | padding: 8px 0; |
| | | border-bottom: 1px dashed #e8edf5; |
| | | } |
| | | |
| | | .process-card__value .unit { |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | color: rgba(0, 0, 0, 0.45); |
| | | margin-left: 6px; |
| | | .focus-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | @media (max-width: 1200px) { |
| | | .process-panel__body { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .process-panel__aside { |
| | | width: 100%; |
| | | flex-direction: row; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .process-card { |
| | | flex: 1; |
| | | min-width: 220px; |
| | | } |
| | | .focus-name { |
| | | font-size: 13px; |
| | | color: #516174; |
| | | } |
| | | |
| | | .process-selection-wrapper { |
| | | max-height: 400px; |
| | | overflow-y: auto; |
| | | padding: 10px; |
| | | .focus-value { |
| | | font-weight: 700; |
| | | color: #1f2d3d; |
| | | } |
| | | |
| | | .process-grid { |
| | | .task-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 8px 0; |
| | | border-bottom: 1px dashed #e8edf5; |
| | | } |
| | | |
| | | .task-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .task-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .task-title { |
| | | font-size: 13px; |
| | | color: #3d4d5f; |
| | | } |
| | | |
| | | .row-two { |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .trend-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | :deep(.el-checkbox.is-bordered) { |
| | | margin-left: 0 !important; |
| | | width: 100%; |
| | | .trend-card { |
| | | border: 1px solid #e8edf5; |
| | | border-radius: 10px; |
| | | padding: 12px; |
| | | } |
| | | |
| | | .trend-card.clickable { |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | } |
| | | |
| | | .trend-card.clickable:hover { |
| | | border-color: #8eb8ff; |
| | | background: #f6f9ff; |
| | | } |
| | | |
| | | .trend-head { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .trend-label { |
| | | font-size: 13px; |
| | | color: #5f6b7a; |
| | | } |
| | | |
| | | .trend-rate { |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .trend-rate.up { |
| | | color: #67c23a; |
| | | } |
| | | |
| | | .trend-rate.down { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | .trend-rate.flat { |
| | | color: #909399; |
| | | } |
| | | |
| | | .trend-value { |
| | | margin-top: 6px; |
| | | font-size: 20px; |
| | | color: #1f2d3d; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .sparkline { |
| | | margin-top: 10px; |
| | | height: 48px; |
| | | display: flex; |
| | | align-items: flex-end; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .sparkline-bar { |
| | | flex: 1; |
| | | min-height: 6px; |
| | | border-radius: 3px 3px 0 0; |
| | | background: linear-gradient(180deg, #82b1ff 0%, #409eff 100%); |
| | | } |
| | | |
| | | .warning-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | gap: 10px; |
| | | padding: 8px 0; |
| | | border-bottom: 1px dashed #e8edf5; |
| | | } |
| | | |
| | | .warning-row:last-child { |
| | | border-bottom: none; |
| | | } |
| | | |
| | | .warning-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .warning-title { |
| | | font-size: 13px; |
| | | color: #3d4d5f; |
| | | } |
| | | |
| | | .config-check-group { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | gap: 10px 16px; |
| | | } |
| | | |
| | | .mini-table-wrap :deep(.el-table th) { |
| | | background: #f8fbff; |
| | | } |
| | | |
| | | @media (max-width: 1100px) { |
| | | .content-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .row-two { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .trend-cards { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | 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, index) 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> |
| | | <div v-for="(group, sort) in getParamGroups(process.productionProductRouteItemParamDtoList)" |
| | | :key="sort" |
| | | class="param-group"> |
| | | <div class="group-header"> |
| | | <span class="group-title">ç产记å½ç» {{ sort }}</span> |
| | | </div> |
| | | <div class="param-grid"> |
| | | <div v-for="param in group" |
| | | :key="param.id" |
| | | class="param-item"> |
| | | <span class="param-label">{{ param.paramName || '-' }}:</span> |
| | | <span class="param-value">{{ param.paramValue || '-' }}</span> |
| | | <span v-if="param.unit && param.unit !== '/'" |
| | | class="param-unit">{{ param.unit }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- ä¸ä¼ æä»¶ --> |
| | | <div class="file-section" |
| | | v-if="process.fileList && process.fileList.length > 0"> |
| | | <h5 class="file-title">ä¸ä¼ æä»¶</h5> |
| | | <div class="file-grid"> |
| | | <div v-for="file in process.fileList" |
| | | :key="file.id" |
| | | class="file-item"> |
| | | <el-image style="width: 100px; height: 100px" |
| | | v-if="file.fileUrl" |
| | | :src="baseUrl + file.fileUrl" |
| | | :zoom-rate="1.2" |
| | | :max-scale="7" |
| | | :alt="file.fileName" |
| | | :min-scale="0.2" |
| | | :preview-src-list="formatFileList(process.fileList)" |
| | | show-progress |
| | | :initial-index="4" |
| | | fit="cover" /> |
| | | <div class="file-info"> |
| | | <span class="file-name">{{ file.fileName || '-' }}</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const baseUrl = import.meta.env.VITE_APP_BASE_API; |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | data: { |
| | | type: Object, |
| | | default: () => ({}), |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:visible"]); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.visible, |
| | | set: value => emit("update:visible", value), |
| | | }); |
| | | |
| | | const dialogTitle = computed(() => "ç产æ¥å·¥è¯¦æ
"); |
| | | const detailData = ref(props.data); |
| | | |
| | | // æ ¼å¼åæ¶é´ |
| | | 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, |
| | | })); |
| | | }; |
| | | |
| | | // å¤çæä»¶é¢è§ |
| | | const handleFilePreview = file => { |
| | | if (file.fileUrl) { |
| | | window.open(baseUrl + file.fileUrl, "_blank"); |
| | | } else { |
| | | console.log("æä»¶æ²¡æURLï¼æ æ³é¢è§"); |
| | | } |
| | | }; |
| | | |
| | | // è·å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] = []; |
| | | } |
| | | groups[sort].push(param); |
| | | }); |
| | | |
| | | return groups; |
| | | }; |
| | | |
| | | // ç嬿°æ®åå |
| | | watch( |
| | | () => props.data, |
| | | newData => { |
| | | detailData.value = newData; |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .detail-container { |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | padding: 0 16px; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-bottom: 28px; |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | margin-bottom: 16px; |
| | | color: #1a1a1a; |
| | | border-bottom: 2px solid #409eff; |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .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-image { |
| | | width: 100px; |
| | | height: 100px; |
| | | object-fit: cover; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | :deep(.el-image) { |
| | | border-radius: 4px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-image__inner) { |
| | | transition: transform 0.3s ease; |
| | | } |
| | | |
| | | .file-item:hover :deep(.el-image__inner) { |
| | | transform: scale(1.05); |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | .unit { |
| | | font-size: 12px; |
| | | color: #5d5a66; |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | </style> |
| | |
| | | <el-form :model="searchForm" |
| | | :inline="true"> |
| | | <el-form-item label="ç产订åå·:"> |
| | | <el-input v-model="searchForm.orderNo" |
| | | <el-input v-model="searchForm.npsNo" |
| | | placeholder="请è¾å
¥" |
| | | clearable |
| | | style="width: 160px;" |
| | | @keyup.enter="handleQuery" /> |
| | | </el-form-item> |
| | | <el-form-item label="çç»:"> |
| | | <el-select v-model="searchForm.teamName" |
| | | <el-select v-model="searchForm.schedule" |
| | | placeholder="è¯·éæ©" |
| | | clearable |
| | | style="width: 160px;" |
| | |
| | | <el-option label="å¤ç" |
| | | value="å¤ç" /> |
| | | </el-select> |
| | | <!-- <el-input v-model="searchForm.teamName" |
| | | <!-- <el-input v-model="searchForm.schedule" |
| | | placeholder="请è¾å
¥"" |
| | | @keyup.enter="handleQuery" /> --> |
| | | </el-form-item> |
| | |
| | | :isSelection="false" |
| | | @selection-change="handleSelectionChange" |
| | | @pagination="pagination"> |
| | | <template #outputVolume="{ row }"> |
| | | <span style="font-weight: bold;color: #409eff;">{{ row.outputVolume }}</span><span style="margin-left: 5px;color: #909399;">æ¹</span> |
| | | <template #totalQuantity="{ row }"> |
| | | <span style="font-weight: bold;color: #409eff;">{{ row.totalQuantity }}</span><span style="margin-left: 5px;color: #909399;">æ¹</span> |
| | | </template> |
| | | <template #unqualifiedVolume="{ row }"> |
| | | <span style="font-weight: bold;color: #b43434;">{{ row.unqualifiedVolume }}</span><span style="margin-left: 5px;color: #909399;">æ¹</span> |
| | | <template #scrapQty="{ row }"> |
| | | <span style="font-weight: bold;color: #b43434;">{{ row.scrapQty }}</span><span style="margin-left: 5px;color: #909399;">æ¹</span> |
| | | </template> |
| | | <template #completedVolume="{ row }"> |
| | | <span style="font-weight: bold;color: #28e431;">{{ row.completedVolume }}</span><span style="margin-left: 5px;color: #909399;">æ¹</span> |
| | | <template #quantity="{ row }"> |
| | | <span style="font-weight: bold;color: #28e431;">{{ row.quantity }}</span><span style="margin-left: 5px;color: #909399;">æ¹</span> |
| | | </template> |
| | | </PIMTable> |
| | | </div> |
| | | <!-- 详æ
å¼¹çª --> |
| | | <detail-dialog v-model:visible="detailDialogVisible" |
| | | :data="detailData" /> |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | workListPage, |
| | | productionReport, |
| | | productionReportUpdate, |
| | | productionReportDelete, |
| | | productionReportDetail, |
| | | productionReportListPage, |
| | | } from "@/api/productionManagement/productionReporting.js"; |
| | | import PIMTable from "@/components/PIMTable/PIMTable.vue"; |
| | | import DetailDialog from "./detailDialog.vue"; |
| | | |
| | | const router = useRouter(); |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "æ¥å·¥ç¼å·", |
| | | prop: "productNo", |
| | | }, |
| | | { |
| | | label: "ç产订åå·", |
| | | prop: "orderNo", |
| | | prop: "npsNo", |
| | | }, |
| | | { |
| | | label: "çç»", |
| | | prop: "teamName", |
| | | prop: "schedule", |
| | | width: "120px", |
| | | dataType: "tag", |
| | | formatType: params => { |
| | |
| | | { |
| | | label: "产åç¼ç ", |
| | | prop: "materialCode", |
| | | width: "150px", |
| | | }, |
| | | { |
| | | label: "产ååç§°", |
| | | prop: "productName", |
| | | width: "150px", |
| | | }, |
| | | { |
| | | label: "è§æ ¼", |
| | | prop: "specification", |
| | | width: "120px", |
| | | className: "specification-cell", |
| | | prop: "productModelName", |
| | | className: "productModelName-cell", |
| | | }, |
| | | { |
| | | label: "强度", |
| | | prop: "strength", |
| | | dataType: "tag", |
| | | formatType: params => { |
| | | return params == "A3.5" ? "primary" : "warning"; |
| | | }, |
| | | }, |
| | | { |
| | | label: "äº§åºæ¹é", |
| | | prop: "outputVolume", |
| | | width: "120px", |
| | | prop: "totalQuantity", |
| | | width: "100px", |
| | | align: "right", |
| | | dataType: "slot", |
| | | slot: "outputVolume", |
| | | slot: "totalQuantity", |
| | | }, |
| | | { |
| | | label: "ä¸åæ ¼æ¹é", |
| | | prop: "unqualifiedVolume", |
| | | width: "120px", |
| | | prop: "scrapQty", |
| | | width: "100px", |
| | | align: "right", |
| | | dataType: "slot", |
| | | slot: "unqualifiedVolume", |
| | | slot: "scrapQty", |
| | | }, |
| | | { |
| | | label: "宿æ¹é", |
| | | prop: "completedVolume", |
| | | width: "120px", |
| | | prop: "quantity", |
| | | width: "100px", |
| | | align: "right", |
| | | dataType: "slot", |
| | | slot: "completedVolume", |
| | | slot: "quantity", |
| | | }, |
| | | { |
| | | label: "å建人", |
| | | prop: "createBy", |
| | | prop: "postName", |
| | | width: "120px", |
| | | dataType: "tag", |
| | | }, |
| | |
| | | }); |
| | | |
| | | const searchForm = reactive({ |
| | | orderNo: "", |
| | | teamName: "", |
| | | npsNo: "", |
| | | schedule: "", |
| | | productName: "", |
| | | }); |
| | | |
| | | const mockData = [ |
| | | { |
| | | id: 1, |
| | | orderNo: "PO202401001", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC001", |
| | | productName: "æ åç å", |
| | | specification: "600Ã240Ã200", |
| | | outputVolume: 120.5, |
| | | unqualifiedVolume: 2.3, |
| | | completedVolume: 118.2, |
| | | createBy: "å¼ ä¸", |
| | | createTime: "2024-01-15 08:30:00", |
| | | }, |
| | | { |
| | | id: 2, |
| | | orderNo: "PO202401002", |
| | | teamName: "å¤ç", |
| | | materialCode: "PC002", |
| | | productName: "æ åç å", |
| | | specification: "600Ã240Ã200", |
| | | outputVolume: 150.8, |
| | | unqualifiedVolume: 1.5, |
| | | completedVolume: 149.3, |
| | | createBy: "æå", |
| | | createTime: "2024-01-15 09:15:00", |
| | | }, |
| | | { |
| | | id: 3, |
| | | orderNo: "PO202401003", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC003", |
| | | productName: "å æ°ç å", |
| | | specification: "600Ã240Ã250", |
| | | outputVolume: 95.2, |
| | | unqualifiedVolume: 0.8, |
| | | completedVolume: 94.4, |
| | | createBy: "çäº", |
| | | createTime: "2024-01-15 10:00:00", |
| | | }, |
| | | { |
| | | id: 4, |
| | | orderNo: "PO202401004", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC004", |
| | | productName: "æ åç å", |
| | | specification: "600Ã240Ã200", |
| | | outputVolume: 180.6, |
| | | unqualifiedVolume: 3.2, |
| | | completedVolume: 177.4, |
| | | createBy: "èµµå
", |
| | | createTime: "2024-01-15 14:20:00", |
| | | }, |
| | | { |
| | | id: 5, |
| | | orderNo: "PO202401005", |
| | | teamName: "å¤ç", |
| | | materialCode: "PC005", |
| | | productName: "å æ°ç å", |
| | | specification: "600Ã240Ã250", |
| | | outputVolume: 110.3, |
| | | unqualifiedVolume: 1.1, |
| | | completedVolume: 109.2, |
| | | createBy: "åä¸", |
| | | createTime: "2024-01-15 15:45:00", |
| | | }, |
| | | { |
| | | id: 6, |
| | | orderNo: "PO202401006", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC006", |
| | | productName: "æ åç å", |
| | | specification: "600Ã240Ã200", |
| | | outputVolume: 135.7, |
| | | unqualifiedVolume: 2.5, |
| | | completedVolume: 133.2, |
| | | createBy: "å¨å
«", |
| | | createTime: "2024-01-16 08:00:00", |
| | | }, |
| | | { |
| | | id: 7, |
| | | orderNo: "PO202401007", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC007", |
| | | productName: "å æ°ç å", |
| | | specification: "600Ã240Ã250", |
| | | outputVolume: 88.4, |
| | | unqualifiedVolume: 0.6, |
| | | completedVolume: 87.8, |
| | | createBy: "å´ä¹", |
| | | createTime: "2024-01-16 09:30:00", |
| | | }, |
| | | { |
| | | id: 8, |
| | | orderNo: "PO202401008", |
| | | teamName: "å¤ç", |
| | | materialCode: "PC008", |
| | | productName: "æ åç å", |
| | | specification: "600Ã240Ã200", |
| | | outputVolume: 165.2, |
| | | unqualifiedVolume: 2.8, |
| | | completedVolume: 162.4, |
| | | createBy: "éå", |
| | | createTime: "2024-01-16 11:00:00", |
| | | }, |
| | | { |
| | | id: 9, |
| | | orderNo: "PO202401009", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC009", |
| | | productName: "å æ°ç å", |
| | | specification: "600Ã240Ã250", |
| | | outputVolume: 102.5, |
| | | unqualifiedVolume: 1.3, |
| | | completedVolume: 101.2, |
| | | createBy: "é±åä¸", |
| | | createTime: "2024-01-16 13:15:00", |
| | | }, |
| | | { |
| | | id: 10, |
| | | orderNo: "PO202401010", |
| | | teamName: "ç½ç", |
| | | materialCode: "PC010", |
| | | productName: "æ åç å", |
| | | specification: "600Ã240Ã200", |
| | | outputVolume: 142.8, |
| | | unqualifiedVolume: 2.1, |
| | | completedVolume: 140.7, |
| | | createBy: "ååäº", |
| | | createTime: "2024-01-16 15:00:00", |
| | | }, |
| | | ]; |
| | | |
| | | const form = reactive({ |
| | | id: undefined, |
| | | orderId: "", |
| | | orderNo: "", |
| | | teamName: "", |
| | | npsNo: "", |
| | | schedule: "", |
| | | materialCode: "", |
| | | productName: "", |
| | | specification: "", |
| | | outputVolume: 0, |
| | | unqualifiedVolume: 0, |
| | | completedVolume: 0, |
| | | productModelName: "", |
| | | totalQuantity: 0, |
| | | scrapQty: 0, |
| | | quantity: 0, |
| | | processId: "", |
| | | params: {}, |
| | | }); |
| | | |
| | | const selectedRows = ref([]); |
| | | const detailDialogVisible = ref(false); |
| | | const detailData = ref({}); |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | setTimeout(() => { |
| | | productionReportListPage({ |
| | | current: page.current, |
| | | size: page.size, |
| | | ...searchForm, |
| | | }).then(res => { |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | tableLoading.value = false; |
| | | const start = (page.current - 1) * page.size; |
| | | const end = start + page.size; |
| | | tableData.value = mockData.slice(start, end); |
| | | page.total = mockData.length; |
| | | }, 500); |
| | | }); |
| | | }; |
| | | |
| | | const handleQuery = () => { |
| | |
| | | }; |
| | | |
| | | const handleReset = () => { |
| | | searchForm.orderNo = ""; |
| | | searchForm.teamName = ""; |
| | | searchForm.npsNo = ""; |
| | | searchForm.schedule = ""; |
| | | searchForm.productName = ""; |
| | | page.current = 1; |
| | | getList(); |
| | |
| | | |
| | | const handleAdd = () => { |
| | | Object.assign(form, { |
| | | type: "add", |
| | | id: undefined, |
| | | orderId: "", |
| | | orderNo: "", |
| | | teamName: "", |
| | | npsNo: "", |
| | | schedule: "", |
| | | materialCode: "", |
| | | productName: "", |
| | | specification: "", |
| | | outputVolume: 0, |
| | | unqualifiedVolume: 0, |
| | | completedVolume: 0, |
| | | productModelName: "", |
| | | totalQuantity: 0, |
| | | scrapQty: 0, |
| | | quantity: 0, |
| | | processId: "", |
| | | params: {}, |
| | | }); |
| | |
| | | }; |
| | | |
| | | const handleEdit = row => { |
| | | Object.assign(form, { |
| | | id: row.id, |
| | | orderId: row.orderId || "", |
| | | orderNo: row.orderNo, |
| | | teamName: row.teamName, |
| | | materialCode: row.materialCode, |
| | | productName: row.productName, |
| | | specification: row.specification, |
| | | outputVolume: row.outputVolume, |
| | | unqualifiedVolume: row.unqualifiedVolume, |
| | | completedVolume: row.completedVolume, |
| | | createBy: row.createBy || "", |
| | | createTime: row.createTime || new Date(), |
| | | processId: row.processId || "", |
| | | params: row.params || {}, |
| | | }); |
| | | router.push({ |
| | | path: "/productionManagement/ReportingDialog", |
| | | query: { data: JSON.stringify(form) }, |
| | | }); |
| | | // è°ç¨è¯¦æ
æ¥å£è·å宿´æ°æ® |
| | | productionReportDetail(row.id) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | const detailData = res.data; |
| | | // æå»ºç¼è¾è¡¨åæ°æ® |
| | | const editForm = { |
| | | id: row.id, |
| | | type: "edit", |
| | | orderId: detailData.productOrderId || "", |
| | | npsNo: detailData.npsNo || "", |
| | | schedule: detailData.schedule || "", |
| | | materialCode: detailData.materialCode || "", |
| | | productName: detailData.productName || "", |
| | | productModelName: detailData.model || "", |
| | | totalQuantity: detailData.quantity || 0, |
| | | scrapQty: detailData.unqualifiedQuantity || 0, |
| | | quantity: detailData.qualifiedQuantity || 0, |
| | | createBy: detailData.postName || "", |
| | | createTime: detailData.createTime || new Date(), |
| | | processId: "", |
| | | params: {}, |
| | | // ä¼ éå·¥åºä¿¡æ¯ |
| | | productionProductRouteItemDtoList: |
| | | detailData.productionProductRouteItemDtoList || [], |
| | | }; |
| | | router.push({ |
| | | path: "/productionManagement/ReportingDialog", |
| | | query: { data: JSON.stringify(editForm) }, |
| | | }); |
| | | } else { |
| | | ElMessage.error("è·å详æ
失败"); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("è·å详æ
失败"); |
| | | }); |
| | | }; |
| | | |
| | | const handleDetail = row => { |
| | | ElMessage.info("详æ
åè½å¾
å®ç°"); |
| | | productionReportDetail(row.id) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | detailData.value = res.data; |
| | | detailDialogVisible.value = true; |
| | | } else { |
| | | ElMessage.error("è·å详æ
失败"); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | ElMessage.error("è·å详æ
失败"); |
| | | }); |
| | | }; |
| | | |
| | | const handleDelete = row => { |
| | |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | productionReportDelete({ id: row.id }) |
| | | productionReportDelete(row.id) |
| | | .then(() => { |
| | | ElMessage.success("å 餿å"); |
| | | getList(); |
| | |
| | | } |
| | | </style> |
| | | <style lang="scss"> |
| | | .specification-cell { |
| | | .productModelName-cell { |
| | | color: #7a7d81; |
| | | font-style: italic; |
| | | } |
| | |
| | | :label="`${item.productName} ${item.model}`" |
| | | class="form-item"> |
| | | <div class="consumable-input-group"> |
| | | <el-input-number v-model="getProcessInfo(parseInt(activeProcessId)).consumables[item.id]" |
| | | <el-input-number v-model="getProcessInfo(parseInt(activeProcessId)).consumables[item.productModelId]" |
| | | :min="0" |
| | | :model-value="getConsumableValue(parseInt(activeProcessId), item.id)" |
| | | @change="val => getProcessInfo(parseInt(activeProcessId)).consumables[item.id] = val" |
| | | :model-value="getConsumableValue(parseInt(activeProcessId), item.productModelId)" |
| | | @change="val => getProcessInfo(parseInt(activeProcessId)).consumables[item.productModelId] = val" |
| | | class="consumable-input" /> |
| | | <span class="consumable-unit">{{ item.unit }}</span> |
| | | </div> |
| | |
| | | </div> |
| | | <div class="card-body"> |
| | | <div class="param-grid"> |
| | | <el-form-item v-for="param in params" |
| | | <el-form-item v-for="param in params[activeProcessId] || []" |
| | | :key="param.id" |
| | | :label="param.paramName" |
| | | :label-width="120" |
| | |
| | | <template v-if="param.paramType == '1'"> |
| | | <!-- æ°åç±»å --> |
| | | <div class="param-input-group"> |
| | | <!-- :precision="getPrecision(param.paramFormat)" --> |
| | | <el-input-number v-model="form.paramGroups[activeProcessId][index][param.id]" |
| | | controls-position="right" |
| | | :precision="getPrecision(param.paramFormat)" |
| | | :key="param.id" |
| | | class="param-input" /> |
| | | <span v-if="param.unit && param.unit != '/'" |
| | | class="param-unit"> |
| | |
| | | <!-- ææ¬ç±»å --> |
| | | <div class="param-input-group"> |
| | | <el-input v-model="form.paramGroups[activeProcessId][index][param.id]" |
| | | :key="param.id" |
| | | class="param-input" /> |
| | | <span v-if="param.unit && param.unit != '/'" |
| | | class="param-unit"> |
| | |
| | | <div class="param-input-group"> |
| | | <el-select v-model="form.paramGroups[activeProcessId][index][param.id]" |
| | | placeholder="è¯·éæ©" |
| | | :key="param.id" |
| | | class="param-select" |
| | | style="width: 100%"> |
| | | <el-option v-for="option in dictOptions[param.paramFormat] || []" |
| | |
| | | <div class="param-input-group"> |
| | | <el-date-picker :value-format="param.paramFormat" |
| | | :format="param.paramFormat" |
| | | :key="param.id" |
| | | :type="param.paramFormat=='YYYY-MM-DD'?'date':'datetime'" |
| | | v-model="form.paramGroups[activeProcessId][index][param.id]" |
| | | class="param-input" /> |
| | |
| | | <!-- å
¶ä»ç±»å --> |
| | | <div class="param-input-group"> |
| | | <el-input v-model="form.paramGroups[activeProcessId][index][param.id]" |
| | | :key="param.id" |
| | | class="param-input" /> |
| | | <span v-if="param.unit && param.unit != '/'" |
| | | class="param-unit"> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | <!-- åæ°å --> |
| | | <el-table-column v-for="param in params" |
| | | <el-table-column v-for="param in params[activeProcessId] || []" |
| | | :key="param.id" |
| | | :min-width="200"> |
| | | <template #header> |
| | |
| | | <template #default="{ row }"> |
| | | <template v-if="param.paramType == '1'"> |
| | | <!-- æ°åç±»å --> |
| | | <!-- :precision="getPrecision(param.paramFormat)" --> |
| | | <el-input-number v-model="row[param.id]" |
| | | controls-position="right" |
| | | :precision="getPrecision(param.paramFormat)" |
| | | :key="param.id" |
| | | class="table-input" /> |
| | | </template> |
| | | <template v-else-if="param.paramType == '2'"> |
| | | <!-- ææ¬ç±»å --> |
| | | <el-input v-model="row[param.id]" |
| | | :key="param.id" |
| | | class="table-input" /> |
| | | </template> |
| | | <template v-else-if="param.paramType == '3'"> |
| | | <!-- åå
¸ç±»å --> |
| | | <el-select v-model="row[param.id]" |
| | | placeholder="è¯·éæ©" |
| | | :key="param.id" |
| | | class="table-select"> |
| | | <el-option v-for="option in dictOptions[param.paramFormat] || []" |
| | | :key="option.dictValue" |
| | |
| | | <!-- æ¥æç±»å --> |
| | | <el-date-picker :value-format="param.paramFormat" |
| | | :format="param.paramFormat" |
| | | :key="param.id" |
| | | width="100%" |
| | | :type="param.paramFormat=='YYYY-MM-DD'?'date':'datetime'" |
| | | v-model="row[param.id]" |
| | |
| | | import { |
| | | productionRecordAdd, |
| | | productionRecordAddSubmit, |
| | | productionRecordEditSubmit, |
| | | } from "@/api/productionManagement/productProcessRoute.js"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { getInfo } from "@/api/login.js"; |
| | |
| | | const route = useRoute(); |
| | | const data = route.query.data ? JSON.parse(route.query.data) : {}; |
| | | |
| | | const dialogTitle = computed(() => (data.id ? "ç¼è¾æ¥å·¥" : "æ°å¢æ¥å·¥")); |
| | | const dialogTitle = computed(() => |
| | | form.type === "edit" ? "ç¼è¾æ¥å·¥" : "æ°å¢æ¥å·¥" |
| | | ); |
| | | |
| | | const formRef = ref(null); |
| | | const isSubmitting = ref(false); |
| | |
| | | const useTableView = ref(false); // æ§å¶æ¯å¦ä½¿ç¨è¡¨æ ¼è§å¾ |
| | | |
| | | const form = reactive({ |
| | | type: data.type || "add", |
| | | id: data.id || undefined, |
| | | orderId: data.orderId || "", |
| | | orderId: data.productOrderId || "", |
| | | npsNo: data.npsNo || "", |
| | | teamName: data.teamName || "ç½ç", |
| | | teamName: data.schedule || data.teamName || "ç½ç", |
| | | materialCode: data.materialCode || "", |
| | | productName: data.productName || "", |
| | | specification: data.specification || "", |
| | | outputVolume: data.outputVolume || 0, |
| | | unqualifiedVolume: data.unqualifiedVolume || 0, |
| | | completedVolume: data.completedVolume || 0, |
| | | createBy: data.createBy || "å½åç»å½äºº", |
| | | specification: data.productModelName || "", |
| | | outputVolume: data.totalQuantity || data.outputVolume || 0, |
| | | unqualifiedVolume: data.scrapQty || data.unqualifiedQuantity || 0, |
| | | completedVolume: data.quantity || data.completedVolume || 0, |
| | | createBy: data.createBy || data.postName || "å½åç»å½äºº", |
| | | createTime: data.createTime || new Date(), |
| | | paramGroups: data.paramGroups || {}, // å卿¯ä¸ªå·¥åºçåæ°ç» |
| | | processInfo: data.processInfo || {}, // å卿¯ä¸ªå·¥åºçåºæ¬ä¿¡æ¯ |
| | | productionProductRouteItemDtoList: |
| | | data.productionProductRouteItemDtoList || [], // å·¥åºä¿¡æ¯ |
| | | }); |
| | | |
| | | const rules = { |
| | |
| | | processExplained: "", |
| | | files: [], |
| | | consumables: {}, |
| | | delFileIds: [], // åå¨è¦å é¤çæä»¶id |
| | | }; |
| | | } |
| | | return form.processInfo[processId]; |
| | |
| | | }; |
| | | |
| | | // è·åæ¶èåæ°éï¼é»è®¤ä¸º0 |
| | | const getConsumableValue = (processId, itemId) => { |
| | | const getConsumableValue = (processId, productModelId) => { |
| | | const processInfo = getProcessInfo(processId); |
| | | if (!processInfo.consumables[itemId]) { |
| | | processInfo.consumables[itemId] = 0; |
| | | if (!processInfo.consumables[productModelId]) { |
| | | processInfo.consumables[productModelId] = 0; |
| | | } |
| | | return processInfo.consumables[itemId]; |
| | | return processInfo.consumables[productModelId]; |
| | | }; |
| | | |
| | | // å¤çæä»¶é¢è§ |
| | |
| | | const processId = parseInt(activeProcessId.value); |
| | | if (processId) { |
| | | const processInfo = getProcessInfo(processId); |
| | | // è®°å½è¢«å é¤çæä»¶idï¼åªæç¼è¾æ¨¡å¼ä¸çç°ææä»¶æéè¦è®°å½ï¼ |
| | | if (file.uid && !file.tempId) { |
| | | processInfo.delFileIds.push(file.uid); |
| | | } |
| | | processInfo.files = fileList; |
| | | } |
| | | }; |
| | | |
| | | // å¤çæä»¶åæ´ |
| | | const handleFileChange = async (file, fileList) => { |
| | | console.log(file, fileList); |
| | | const formData = new FormData(); |
| | | formData.append("file", file.raw); |
| | | |
| | |
| | | Authorization: `Bearer ${getToken()}`, |
| | | }, |
| | | }); |
| | | console.log(uploadRes); |
| | | if (uploadRes.code === 200) { |
| | | const tempId = uploadRes.data.tempId; |
| | | // å°tempIdåå¨å°fileå¯¹è±¡ä¸ |
| | |
| | | p => p.processId === parseInt(processId) |
| | | ); |
| | | if (process) { |
| | | params.value = process.orderRouteItemParaVos || []; |
| | | params.value[processId] = process.orderRouteItemParaVos || []; |
| | | |
| | | // åå§ååæ°ç» |
| | | if (!form.paramGroups[processId]) { |
| | | form.paramGroups[processId] = []; |
| | | } |
| | | |
| | | // æ£æ¥æ¯å¦æç¼è¾æ°æ® |
| | | if ( |
| | | form.productionProductRouteItemDtoList && |
| | | form.productionProductRouteItemDtoList.length > 0 |
| | | ) { |
| | | const editProcess = form.productionProductRouteItemDtoList.find( |
| | | p => p.processId === parseInt(processId) |
| | | ); |
| | | if (editProcess && editProcess.productionProductRouteItemParamDtoList) { |
| | | // æsourceSortåç»åæ° |
| | | const paramGroups = {}; |
| | | editProcess.productionProductRouteItemParamDtoList.forEach(param => { |
| | | if (!param.bomId) { |
| | | // åªå¤çéBOMåæ° |
| | | const sort = param.sourceSort || 1; |
| | | if (!paramGroups[sort]) { |
| | | paramGroups[sort] = {}; |
| | | } |
| | | paramGroups[sort][param.orderItemParamId] = param.paramValue || ""; |
| | | // 妿æ¯åå
¸ç±»ååæ°ï¼è·ååå
¸æ°æ® |
| | | if (param.paramType == "3" && param.paramFormat) { |
| | | getDictOptions(param.paramFormat); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // 转æ¢ä¸ºæ°ç» |
| | | form.paramGroups[processId] = Object.values(paramGroups); |
| | | |
| | | // åå§åå·¥åºåºæ¬ä¿¡æ¯ |
| | | if (editProcess) { |
| | | const processInfo = getProcessInfo(parseInt(processId)); |
| | | processInfo.postName = editProcess.postName || ""; |
| | | processInfo.equipmentMalfunction = |
| | | editProcess.equipmentMalfunction || ""; |
| | | processInfo.equipmentDisposal = editProcess.equipmentDisposal || ""; |
| | | processInfo.id = editProcess.id || ""; |
| | | processInfo.processExplained = editProcess.processExplained || ""; |
| | | // å¤çæä»¶ |
| | | if (editProcess.fileList) { |
| | | processInfo.files = editProcess.fileList.map(file => ({ |
| | | name: file.fileName, |
| | | url: file.fileUrl, |
| | | uid: file.id, |
| | | })); |
| | | } |
| | | // å¤çBOMä¿¡æ¯ |
| | | if (editProcess.productionProductRouteItemParamDtoList) { |
| | | editProcess.productionProductRouteItemParamDtoList.forEach( |
| | | param => { |
| | | // console.log(form.processStructures[processId], "========"); |
| | | if (param.bomId) { |
| | | // 使ç¨bomIdä½ä¸ºkeyï¼å 为getProcessStructuresè¿åçitem.idæ¯bomId |
| | | form.processStructures[processId].forEach(item => { |
| | | if (item.productModelId == param.productId) { |
| | | processInfo.consumables[param.productId] = |
| | | param.productValue || 0; |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | ); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // å¦ææ²¡æåæ°ç»ï¼æ·»å ä¸ä¸ªé»è®¤åæ°ç» |
| | | if (form.paramGroups[processId].length === 0) { |
| | | const defaultGroup = {}; |
| | | for (const param of params.value) { |
| | | for (const param of params.value[processId]) { |
| | | defaultGroup[param.id] = param.standardValue || ""; |
| | | // 妿æ¯åå
¸ç±»ååæ°ï¼è·ååå
¸æ°æ® |
| | | if (param.paramType == "3" && param.paramFormat) { |
| | |
| | | |
| | | // æå»ºè¯·æ±åæ° |
| | | const order = orderList.value.find(item => item.id === form.orderId); |
| | | console.log(order, "order"); |
| | | const submitParams = { |
| | | productOrderId: form.orderId, |
| | | productId: order ? order.productId : null, |
| | |
| | | const processInfo = getProcessInfo(process.processId); |
| | | const paramGroups = form.paramGroups[process.processId] || []; |
| | | const productionProductRouteItemParamDtoList = []; |
| | | |
| | | // æ·»å åæ°ç» |
| | | paramGroups.forEach((group, index) => { |
| | | Object.entries(group).forEach(([paramId, value]) => { |
| | |
| | | ) |
| | | : null; |
| | | if (param) { |
| | | console.log(param, "param"); |
| | | productionProductRouteItemParamDtoList.push({ |
| | | id: parseInt(paramId), |
| | | standardValue: param.standardValue, |
| | | minValue: param.minValue, |
| | | maxValue: param.maxValue, |
| | | // standardValue: param.standardValue, |
| | | // minValue: param.minValue, |
| | | // maxValue: param.maxValue, |
| | | productId: param.productId, |
| | | productValue: value, |
| | | paramValue: value, |
| | | // productValue: value, |
| | | sourceSort: index + 1, |
| | | unit: param.unit, |
| | | isRequired: param.isRequired, |
| | | // isRequired: param.isRequired, |
| | | }); |
| | | } |
| | | }); |
| | |
| | | |
| | | // æ·»å BOMä¿¡æ¯ |
| | | const structures = getProcessStructures(process.processId); |
| | | console.log(structures, "structures"); |
| | | structures.forEach(structure => { |
| | | const consumableValue = getConsumableValue( |
| | | process.processId, |
| | | structure.id |
| | | structure.productModelId |
| | | ); |
| | | if (consumableValue > 0) { |
| | | productionProductRouteItemParamDtoList.push({ |
| | |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | const fileIds = []; |
| | | processInfo.files.forEach(file => { |
| | | if (file.tempId) { |
| | | fileIds.push(file.tempId); |
| | | } |
| | | }); |
| | | console.log(processInfo, "processInfo"); |
| | | return { |
| | | postName: processInfo.postName, |
| | | id: processInfo.id, |
| | | equipmentMalfunction: processInfo.equipmentMalfunction, |
| | | equipmentDisposal: processInfo.equipmentDisposal, |
| | | processExplained: processInfo.processExplained, |
| | | processId: process.processId, |
| | | delFileIds: [...(processInfo.delFileIds || [])], |
| | | productionProductRouteItemParamDtoList, |
| | | files: processInfo.files.map(file => file.tempId || file.uid), |
| | | // files: processInfo.files.map(file => file.tempId || file.uid), |
| | | files: fileIds, |
| | | }; |
| | | }), |
| | | }; |
| | | console.log(submitParams, "submitParams"); |
| | | isSubmitting.value = false; |
| | | // è°ç¨APIè¿è¡æäº¤ |
| | | productionRecordAddSubmit(submitParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | ElMessage.success(data.id ? "ä¿®æ¹æå" : "æ°å¢æå"); |
| | | router.back(); |
| | | } else { |
| | | ElMessage.error(res.msg || "æäº¤å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | ElMessage.error("æäº¤å¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | console.error("æäº¤é误:", error); |
| | | }) |
| | | .finally(() => { |
| | | isSubmitting.value = false; |
| | | }); |
| | | if (form.type === "edit") { |
| | | submitParams.productMainId = form.id; |
| | | productionRecordEditSubmit(submitParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | ElMessage.success(form.type === "edit" ? "ä¿®æ¹æå" : "æ°å¢æå"); |
| | | router.back(); |
| | | } else { |
| | | ElMessage.error(res.msg || "æäº¤å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | ElMessage.error("æäº¤å¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | console.error("æäº¤é误:", error); |
| | | }) |
| | | .finally(() => { |
| | | isSubmitting.value = false; |
| | | }); |
| | | } else { |
| | | productionRecordAddSubmit(submitParams) |
| | | .then(res => { |
| | | if (res.code === 200) { |
| | | ElMessage.success(form.type === "edit" ? "ä¿®æ¹æå" : "æ°å¢æå"); |
| | | router.back(); |
| | | } else { |
| | | ElMessage.error(res.msg || "æäº¤å¤±è´¥"); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | ElMessage.error("æäº¤å¤±è´¥ï¼è¯·ç¨åéè¯"); |
| | | console.error("æäº¤é误:", error); |
| | | }) |
| | | .finally(() => { |
| | | isSubmitting.value = false; |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | |
| | | loadUsers(); |
| | | getCurrentUser(); |
| | | |
| | | if (data.id) { |
| | | if (form.type === "edit") { |
| | | // ç¼è¾æ¶è®¾ç½®è¡¨åæ°æ® |
| | | Object.assign(form, data); |
| | | // 设置orderId |
| | | orderId.value = data.orderId || ""; |
| | | orderId.value = form.orderId || ""; |
| | | // 妿æè®¢åIDï¼å 载工åºååæ° |
| | | if (data.orderId) { |
| | | if (form.orderId) { |
| | | // 模æéæ©è®¢åçæä½ï¼è§¦åæ°æ®å è½½ |
| | | setTimeout(() => { |
| | | handleOrderChange(data.orderId); |
| | | handleOrderChange(form.orderId); |
| | | }, 100); |
| | | } |
| | | } else { |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | width="800px" |
| | | :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="æ£æµæ¥æ">{{ formatTime(detailData.checkTime) }}</el-descriptions-item> |
| | | <el-descriptions-item label="ä¾åºå">{{ detailData.supplier || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ£éªå"><span style="color: #eb9113;">{{ detailData.checkName || '-' }}</span></el-descriptions-item> |
| | | <el-descriptions-item label="产ååç§°"><span style="color: hsl(210, 100%, 63%);">{{ detailData.productName || '-' }}</span></el-descriptions-item> |
| | | <el-descriptions-item label="è§æ ¼åå·"><span style="">{{ detailData.model || '-' }}</span></el-descriptions-item> |
| | | <el-descriptions-item label="åä½"><span style="">{{ detailData.unit || '-' }}</span></el-descriptions-item> |
| | | <el-descriptions-item label="æ°é">{{ detailData.quantity || 0 }}</el-descriptions-item> |
| | | <el-descriptions-item label="è¯æ ·ç¼å·">{{ detailData.sampleCode || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="车çå·">{{ detailData.licensePlateNumber || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="è¯æ ·ç¶æ"><el-tag>{{ detailData.sampleState || '-' }}</el-tag></el-descriptions-item> |
| | | <el-descriptions-item label="æ£æµæ§è´¨"><el-tag type="info">{{ detailData.inspectNature || '-' }}</el-tag></el-descriptions-item> |
| | | <el-descriptions-item label="ææ éæ©"><el-tag type="warning">{{ detailData.testStandardName || detailData.testStandardId || '-' }}</el-tag></el-descriptions-item> |
| | | <el-descriptions-item label="åæ ·æ¥æ">{{ formatTime(detailData.sampleTime) }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ£æµåä½">{{ detailData.checkCompany || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ£æµç»æ"><el-tag :type="detailData.checkResult == 'åæ ¼' ? 'success' : 'danger'">{{ detailData.checkResult || '-' }}</el-tag></el-descriptions-item> |
| | | <el-descriptions-item label="æäº¤ç¶æ"><el-tag :type="detailData.inspectState ? 'success' : 'danger'">{{ detailData.inspectState ? 'å·²æäº¤' : 'æªæäº¤' }}</el-tag></el-descriptions-item> |
| | | </el-descriptions> |
| | | </div> |
| | | <!-- ææ ä¿¡æ¯ --> |
| | | <div class="detail-section" |
| | | v-if="detailData.qualityInspectParams && detailData.qualityInspectParams.length > 0"> |
| | | <h3 class="section-title">ææ ä¿¡æ¯</h3> |
| | | <el-table :data="detailData.qualityInspectParams" |
| | | style="width: 100%" |
| | | border> |
| | | <el-table-column prop="parameterItem" |
| | | label="ææ " |
| | | min-width="150"></el-table-column> |
| | | <el-table-column prop="unit" |
| | | label="åä½" |
| | | width="100"></el-table-column> |
| | | <el-table-column prop="standardValue" |
| | | label="æ åå¼" |
| | | width="120"></el-table-column> |
| | | <el-table-column prop="controlValue" |
| | | label="å
æ§å¼" |
| | | width="120"></el-table-column> |
| | | <el-table-column prop="testValue" |
| | | label="æ£éªå¼" |
| | | width="120"> |
| | | <template #default="scope"> |
| | | <span style="color: hsl(210, 100%, 63%);">{{ scope.row.testValue || '-' }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">å
³é</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, computed, watch } from "vue"; |
| | | import dayjs from "dayjs"; |
| | | import { |
| | | qualityInspectDetailByProductId, |
| | | getQualityTestStandardParamByTestStandardId, |
| | | } from "@/api/qualityManagement/metricMaintenance.js"; |
| | | import { qualityInspectParamInfo } from "@/api/qualityManagement/qualityInspectParam.js"; |
| | | |
| | | const props = defineProps({ |
| | | visible: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | data: { |
| | | type: Object, |
| | | default: () => ({}), |
| | | }, |
| | | }); |
| | | |
| | | const emit = defineEmits(["update:visible"]); |
| | | |
| | | const dialogVisible = computed({ |
| | | get: () => props.visible, |
| | | set: value => emit("update:visible", value), |
| | | }); |
| | | |
| | | const dialogTitle = computed(() => "åæææ£éªè¯¦æ
"); |
| | | const detailData = ref(props.data); |
| | | const loading = ref(false); |
| | | |
| | | // æ ¼å¼åæ¶é´ |
| | | const formatTime = time => { |
| | | return time ? dayjs(time).format("YYYY-MM-DD HH:mm:ss") : "-"; |
| | | }; |
| | | |
| | | // å è½½ææ éæ©åè¡¨æ ¼æ°æ® |
| | | const loadIndicatorData = async () => { |
| | | if (!detailData.value.productId) return; |
| | | |
| | | loading.value = true; |
| | | try { |
| | | // å è½½ææ éæ©å表 |
| | | const params = { |
| | | productId: detailData.value.productId, |
| | | inspectType: 0, |
| | | }; |
| | | const standardRes = await qualityInspectDetailByProductId(params); |
| | | if (standardRes.data && standardRes.data.length > 0) { |
| | | // æ¥æ¾å½åéæ©çææ åç§° |
| | | const selectedStandard = standardRes.data.find( |
| | | item => item.id == detailData.value.testStandardId |
| | | ); |
| | | if (selectedStandard) { |
| | | detailData.value.testStandardName = |
| | | selectedStandard.standardName || selectedStandard.standardNo; |
| | | } |
| | | } |
| | | |
| | | // å è½½åæ°æ°æ®ï¼ä¸ç¼è¾é¡µé¢ä¿æä¸è´ï¼ |
| | | if (detailData.value.id) { |
| | | getQualityInspectParamList(detailData.value.id); |
| | | } |
| | | } catch (error) { |
| | | console.error("å è½½ææ æ°æ®å¤±è´¥:", error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | const getQualityInspectParamList = id => { |
| | | qualityInspectParamInfo(id).then(res => { |
| | | detailData.value.qualityInspectParams = res.data; |
| | | }); |
| | | }; |
| | | // ç嬿°æ®åå |
| | | watch( |
| | | () => props.data, |
| | | newData => { |
| | | detailData.value = newData; |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | // æ´é²æ¹æ³ç»ç¶ç»ä»¶ |
| | | defineExpose({ |
| | | loadIndicatorData, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .detail-container { |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | padding: 0 16px; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-bottom: 28px; |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | .section-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | margin-bottom: 16px; |
| | | color: #1a1a1a; |
| | | border-bottom: 2px solid #409eff; |
| | | padding-bottom: 10px; |
| | | } |
| | | |
| | | .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-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) { |
| | | border-radius: 6px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #f5f7fa; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff !important; |
| | | } |
| | | |
| | | :deep(.el-table td) { |
| | | color: #303133; |
| | | } |
| | | </style> |
| | |
| | | @close="handleQuery"></FormDia> |
| | | <files-dia ref="filesDia" |
| | | @close="handleQuery"></files-dia> |
| | | <DetailDialog ref="detailDialog" |
| | | v-model:visible="detailDialogVisible" |
| | | :data="detailDialogData" |
| | | @close="handleQuery"></DetailDialog> |
| | | <el-dialog v-model="dialogFormVisible" |
| | | title="ç¼è¾æ£éªå" |
| | | width="30%" |
| | |
| | | } from "vue"; |
| | | import InspectionFormDia from "@/views/qualityManagement/rawMaterialInspection/components/inspectionFormDia.vue"; |
| | | import FormDia from "@/views/qualityManagement/rawMaterialInspection/components/formDia.vue"; |
| | | import DetailDialog from "@/views/qualityManagement/rawMaterialInspection/components/detailDialog.vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { |
| | | downloadQualityInspect, |
| | |
| | | }, |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: row => { |
| | | openDetailDialog(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "éä»¶", |
| | | type: "text", |
| | | clickFun: row => { |
| | |
| | | const formDia = ref(); |
| | | const filesDia = ref(); |
| | | const inspectionFormDia = ref(); |
| | | const detailDialog = ref(); |
| | | const detailDialogVisible = ref(false); |
| | | const detailDialogData = ref({}); |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | | const changeDaterange = value => { |
| | |
| | | }); |
| | | }; |
| | | |
| | | // æå¼è¯¦æ
å¼¹æ¡ |
| | | const openDetailDialog = row => { |
| | | // ç¡®ä¿qualityInspectParamsåæ®µåå¨ |
| | | if (!row.qualityInspectParams) { |
| | | row.qualityInspectParams = []; |
| | | } |
| | | detailDialogData.value = row; |
| | | detailDialogVisible.value = true; |
| | | // æå¼å¼¹çªåå è½½ææ æ°æ® |
| | | setTimeout(() => { |
| | | detailDialog.value?.loadIndicatorData(); |
| | | }, 100); |
| | | }; |
| | | |
| | | // å é¤ |
| | | const handleDelete = () => { |
| | | let ids = []; |
| | |
| | | <template> |
| | | <div class="panel-header"> |
| | | <span class="panel-title">{{ title }}</span> |
| | | <span :class="{'panel-title': !isFullscreen, 'panel-title-fullscreen': isFullscreen}">{{ title }}</span> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | defineProps({ |
| | | title: { |
| | | type: String, |
| | | required: true, |
| | | default: '' |
| | | } |
| | | }) |
| | | defineProps({ |
| | | title: { |
| | | type: String, |
| | | required: true, |
| | | default: "", |
| | | }, |
| | | isFullscreen: { |
| | | type: Boolean, |
| | | default: false, |
| | | }, |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .panel-header { |
| | | background-image: url("@/assets/BI/kehuhetongback@2x.png"); |
| | | background-size: 100% 100%; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | .panel-header { |
| | | background-image: url("@/assets/BI/kehuhetongback@2x.png"); |
| | | background-size: 100% 100%; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | } |
| | | |
| | | .panel-title { |
| | | width: 100%; |
| | | font-weight: 500; |
| | | font-size: 16px; |
| | | color: #D9ECFF; |
| | | padding-left: 46px; |
| | | line-height: 36px; |
| | | } |
| | | .panel-title { |
| | | width: 100%; |
| | | font-weight: 500; |
| | | font-size: 16px; |
| | | color: #d9ecff; |
| | | padding-left: 46px; |
| | | line-height: 36px; |
| | | } |
| | | |
| | | .panel-title-fullscreen { |
| | | font-size: 1.6vh; |
| | | line-height: 4vh; |
| | | padding-left: 4.6vh; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="carousel-cards"> |
| | | <button |
| | | v-if="canScrollLeft" |
| | | class="nav-button nav-button-left" |
| | | @click="scrollLeftFn" |
| | | > |
| | | <img src="@/assets/BI/jiantou.png" alt="å·¦ç®å¤´" /> |
| | | <button v-if="canScrollLeft" |
| | | class="nav-button nav-button-left" |
| | | @click="scrollLeftFn"> |
| | | <img src="@/assets/BI/jiantou.png" |
| | | alt="å·¦ç®å¤´" /> |
| | | </button> |
| | | <div |
| | | class="cards-container" |
| | | :style="{ '--visible-count': visibleCount }" |
| | | ref="cardsContainerRef" |
| | | > |
| | | <div |
| | | v-for="(item, index) in items" |
| | | :key="index" |
| | | class="card-item" |
| | | > |
| | | <div v-if="item.icon" class="card-icon" :style="{ backgroundImage: `url(${item.icon})` }"></div> |
| | | <div class="cards-container" |
| | | :style="{ '--visible-count': visibleCount }" |
| | | ref="cardsContainerRef"> |
| | | <div v-for="(item, index) in items" |
| | | :key="index" |
| | | class="card-item"> |
| | | <div v-if="item.icon" |
| | | class="card-icon" |
| | | :style="{ backgroundImage: `url(${item.icon})` }"></div> |
| | | <div class="card-title"> |
| | | <div class="card-label">{{ item.label }}</div> |
| | | <div class="card-value"> |
| | | <span class="value-number">{{ item.value }}</span> |
| | | <span class="value-unit">{{ item.unit }}</span> |
| | | </div> |
| | | <div v-if="item.rate ?? item.ratio ?? item.percent" class="card-rate"> |
| | | <div v-if="item.rate ?? item.ratio ?? item.percent" |
| | | class="card-rate"> |
| | | <span class="rate-value">{{ item.rate ?? item.ratio ?? item.percent }}%</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <button |
| | | v-if="canScrollRight" |
| | | class="nav-button nav-button-right" |
| | | @click="scrollRightFn" |
| | | > |
| | | <img src="@/assets/BI/jiantou.png" alt="å³ç®å¤´" /> |
| | | <button v-if="canScrollRight" |
| | | class="nav-button nav-button-right" |
| | | @click="scrollRightFn"> |
| | | <img src="@/assets/BI/jiantou.png" |
| | | alt="å³ç®å¤´" /> |
| | | </button> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, onBeforeUnmount, nextTick, watch, computed } from 'vue' |
| | | import { |
| | | ref, |
| | | onMounted, |
| | | onBeforeUnmount, |
| | | nextTick, |
| | | watch, |
| | | computed, |
| | | } from "vue"; |
| | | |
| | | const props = defineProps({ |
| | | items: { |
| | | type: Array, |
| | | default: () => [], |
| | | validator: (value) => { |
| | | return value.every(item => |
| | | item && typeof item.label !== 'undefined' && |
| | | typeof item.value !== 'undefined' && |
| | | typeof item.unit !== 'undefined' |
| | | ) |
| | | } |
| | | }, |
| | | visibleCount: { |
| | | type: Number, |
| | | default: 3 |
| | | } |
| | | }) |
| | | const props = defineProps({ |
| | | items: { |
| | | type: Array, |
| | | default: () => [], |
| | | validator: value => { |
| | | return value.every( |
| | | item => |
| | | item && |
| | | typeof item.label !== "undefined" && |
| | | typeof item.value !== "undefined" && |
| | | typeof item.unit !== "undefined" |
| | | ); |
| | | }, |
| | | }, |
| | | visibleCount: { |
| | | type: Number, |
| | | default: 3, |
| | | }, |
| | | }); |
| | | |
| | | const cardsContainerRef = ref(null) |
| | | const currentScrollLeft = ref(0) |
| | | const maxScrollLeft = ref(0) |
| | | const cardsContainerRef = ref(null); |
| | | const currentScrollLeft = ref(0); |
| | | const maxScrollLeft = ref(0); |
| | | |
| | | // æ£æ¥æ¯å¦å¯ä»¥åå·¦æ»å¨ |
| | | const canScrollLeft = computed(() => { |
| | | return currentScrollLeft.value > 0 |
| | | }) |
| | | // æ£æ¥æ¯å¦å¯ä»¥åå·¦æ»å¨ |
| | | const canScrollLeft = computed(() => { |
| | | return currentScrollLeft.value > 0; |
| | | }); |
| | | |
| | | // æ£æ¥æ¯å¦å¯ä»¥å峿»å¨ |
| | | const canScrollRight = computed(() => { |
| | | return currentScrollLeft.value < maxScrollLeft.value |
| | | }) |
| | | // æ£æ¥æ¯å¦å¯ä»¥å峿»å¨ |
| | | const canScrollRight = computed(() => { |
| | | return currentScrollLeft.value < maxScrollLeft.value; |
| | | }); |
| | | |
| | | // æ´æ°æ»å¨ç¶æ |
| | | const updateScrollState = () => { |
| | | const container = cardsContainerRef.value |
| | | if (!container) return |
| | | |
| | | currentScrollLeft.value = container.scrollLeft |
| | | maxScrollLeft.value = container.scrollWidth - container.clientWidth |
| | | } |
| | | // æ´æ°æ»å¨ç¶æ |
| | | const updateScrollState = () => { |
| | | const container = cardsContainerRef.value; |
| | | if (!container) return; |
| | | |
| | | // åå·¦æ»å¨ |
| | | const scrollLeftFn = () => { |
| | | const container = cardsContainerRef.value |
| | | if (!container) return |
| | | |
| | | const scrollItems = Array.from(container.querySelectorAll('.card-item')) |
| | | if (scrollItems.length === 0) return |
| | | |
| | | const itemWidth = scrollItems[0]?.offsetWidth || 0 |
| | | const gap = 12 |
| | | const scrollDistance = itemWidth + gap |
| | | |
| | | container.scrollBy({ |
| | | left: -scrollDistance, |
| | | behavior: 'smooth' |
| | | }) |
| | | |
| | | // å»¶è¿æ´æ°ç¶æï¼çå¾
æ»å¨å¨ç»å®æ |
| | | setTimeout(() => { |
| | | updateScrollState() |
| | | }, 300) |
| | | } |
| | | currentScrollLeft.value = container.scrollLeft; |
| | | maxScrollLeft.value = container.scrollWidth - container.clientWidth; |
| | | }; |
| | | |
| | | // å峿»å¨ |
| | | const scrollRightFn = () => { |
| | | const container = cardsContainerRef.value |
| | | if (!container) return |
| | | |
| | | const scrollItems = Array.from(container.querySelectorAll('.card-item')) |
| | | if (scrollItems.length === 0) return |
| | | |
| | | const itemWidth = scrollItems[0]?.offsetWidth || 0 |
| | | const gap = 12 |
| | | const scrollDistance = itemWidth + gap |
| | | |
| | | container.scrollBy({ |
| | | left: scrollDistance, |
| | | behavior: 'smooth' |
| | | }) |
| | | |
| | | // å»¶è¿æ´æ°ç¶æï¼çå¾
æ»å¨å¨ç»å®æ |
| | | setTimeout(() => { |
| | | updateScrollState() |
| | | }, 300) |
| | | } |
| | | // åå·¦æ»å¨ |
| | | const scrollLeftFn = () => { |
| | | const container = cardsContainerRef.value; |
| | | if (!container) return; |
| | | |
| | | // çå¬ items ååï¼æ´æ°æ»å¨ç¶æ |
| | | watch(() => props.items, () => { |
| | | nextTick(() => { |
| | | updateScrollState() |
| | | }) |
| | | }, { deep: true }) |
| | | const scrollItems = Array.from(container.querySelectorAll(".card-item")); |
| | | if (scrollItems.length === 0) return; |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => { |
| | | updateScrollState() |
| | | // ç嬿»å¨äºä»¶ |
| | | const container = cardsContainerRef.value |
| | | const itemWidth = scrollItems[0]?.offsetWidth || 0; |
| | | const gap = 12; |
| | | const scrollDistance = itemWidth + gap; |
| | | |
| | | container.scrollBy({ |
| | | left: -scrollDistance, |
| | | behavior: "smooth", |
| | | }); |
| | | |
| | | // å»¶è¿æ´æ°ç¶æï¼çå¾
æ»å¨å¨ç»å®æ |
| | | setTimeout(() => { |
| | | updateScrollState(); |
| | | }, 300); |
| | | }; |
| | | |
| | | // å峿»å¨ |
| | | const scrollRightFn = () => { |
| | | const container = cardsContainerRef.value; |
| | | if (!container) return; |
| | | |
| | | const scrollItems = Array.from(container.querySelectorAll(".card-item")); |
| | | if (scrollItems.length === 0) return; |
| | | |
| | | const itemWidth = scrollItems[0]?.offsetWidth || 0; |
| | | const gap = 12; |
| | | const scrollDistance = itemWidth + gap; |
| | | |
| | | container.scrollBy({ |
| | | left: scrollDistance, |
| | | behavior: "smooth", |
| | | }); |
| | | |
| | | // å»¶è¿æ´æ°ç¶æï¼çå¾
æ»å¨å¨ç»å®æ |
| | | setTimeout(() => { |
| | | updateScrollState(); |
| | | }, 300); |
| | | }; |
| | | |
| | | // çå¬ items ååï¼æ´æ°æ»å¨ç¶æ |
| | | watch( |
| | | () => props.items, |
| | | () => { |
| | | nextTick(() => { |
| | | updateScrollState(); |
| | | }); |
| | | }, |
| | | { deep: true } |
| | | ); |
| | | |
| | | onMounted(() => { |
| | | nextTick(() => { |
| | | updateScrollState(); |
| | | // ç嬿»å¨äºä»¶ |
| | | const container = cardsContainerRef.value; |
| | | if (container) { |
| | | container.addEventListener("scroll", updateScrollState); |
| | | } |
| | | }); |
| | | }); |
| | | |
| | | onBeforeUnmount(() => { |
| | | // æ¸
çæ»å¨äºä»¶çå¬å¨ |
| | | const container = cardsContainerRef.value; |
| | | if (container) { |
| | | container.addEventListener('scroll', updateScrollState) |
| | | container.removeEventListener("scroll", updateScrollState); |
| | | } |
| | | }) |
| | | }) |
| | | |
| | | onBeforeUnmount(() => { |
| | | // æ¸
çæ»å¨äºä»¶çå¬å¨ |
| | | const container = cardsContainerRef.value |
| | | if (container) { |
| | | container.removeEventListener('scroll', updateScrollState) |
| | | } |
| | | }) |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .carousel-cards { |
| | | width: 100%; |
| | | overflow: hidden; |
| | | position: relative; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | .carousel-cards { |
| | | width: 100%; |
| | | overflow: hidden; |
| | | position: relative; |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .cards-container { |
| | | display: flex; |
| | | gap: 12px; |
| | | width: 100%; |
| | | overflow-x: auto; |
| | | overflow-y: hidden; |
| | | scrollbar-width: none; /* Firefox */ |
| | | -ms-overflow-style: none; /* IE and Edge */ |
| | | padding-bottom: 4px; |
| | | scroll-behavior: smooth; |
| | | } |
| | | .cards-container { |
| | | display: flex; |
| | | gap: 12px; |
| | | width: 100%; |
| | | overflow-x: auto; |
| | | overflow-y: hidden; |
| | | scrollbar-width: none; /* Firefox */ |
| | | -ms-overflow-style: none; /* IE and Edge */ |
| | | padding-bottom: 4px; |
| | | scroll-behavior: smooth; |
| | | } |
| | | |
| | | .cards-container::-webkit-scrollbar { |
| | | display: none; /* Chrome, Safari, Opera */ |
| | | } |
| | | .cards-container::-webkit-scrollbar { |
| | | display: none; /* Chrome, Safari, Opera */ |
| | | } |
| | | |
| | | .nav-button { |
| | | position: absolute; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 32px; |
| | | height: 32px; |
| | | background: rgba(26, 88, 176, 0.6); |
| | | border: 1px solid rgba(26, 88, 176, 0.8); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | z-index: 10; |
| | | transition: all 0.3s ease; |
| | | padding: 0; |
| | | } |
| | | .nav-button { |
| | | position: absolute; |
| | | top: 50%; |
| | | transform: translateY(-50%); |
| | | width: 32px; |
| | | height: 32px; |
| | | background: rgba(26, 88, 176, 0.6); |
| | | border: 1px solid rgba(26, 88, 176, 0.8); |
| | | border-radius: 50%; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | cursor: pointer; |
| | | z-index: 10; |
| | | transition: all 0.3s ease; |
| | | padding: 0; |
| | | } |
| | | |
| | | .nav-button:hover { |
| | | background: rgba(26, 88, 176, 0.8); |
| | | transform: translateY(-50%) scale(1.1); |
| | | } |
| | | .nav-button:hover { |
| | | background: rgba(26, 88, 176, 0.8); |
| | | transform: translateY(-50%) scale(1.1); |
| | | } |
| | | |
| | | .nav-button-left { |
| | | left: -16px; |
| | | } |
| | | .nav-button-left { |
| | | left: -16px; |
| | | } |
| | | |
| | | .nav-button-left img { |
| | | width: 16px; |
| | | height: 16px; |
| | | transform: rotate(180deg); |
| | | } |
| | | .nav-button-left img { |
| | | width: 16px; |
| | | height: 16px; |
| | | transform: rotate(180deg); |
| | | } |
| | | |
| | | .nav-button-right { |
| | | right: -16px; |
| | | } |
| | | .nav-button-right { |
| | | right: -16px; |
| | | } |
| | | |
| | | .nav-button-right img { |
| | | width: 16px; |
| | | height: 16px; |
| | | } |
| | | .nav-button-right img { |
| | | width: 16px; |
| | | height: 16px; |
| | | } |
| | | |
| | | .card-item { |
| | | flex: 0 0 calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count)); |
| | | min-width: calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count)); |
| | | display: flex; |
| | | align-items: center; |
| | | background: linear-gradient(269deg, rgba(27,57,126,0.13) 0%, rgba(33,137,206,0.33) 98.13%, #24AFF4 100%); |
| | | border-radius: 8px 8px 8px 8px; |
| | | padding: 12px 16px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | .card-item { |
| | | flex: 0 0 |
| | | calc((100% - (var(--visible-count) - 1) * 12px) / var(--visible-count)); |
| | | min-width: calc( |
| | | (100% - (var(--visible-count) - 1) * 12px) / var(--visible-count) |
| | | ); |
| | | display: flex; |
| | | align-items: center; |
| | | background: linear-gradient( |
| | | 269deg, |
| | | rgba(27, 57, 126, 0.13) 0%, |
| | | rgba(33, 137, 206, 0.33) 98.13%, |
| | | #24aff4 100% |
| | | ); |
| | | border-radius: 8px 8px 8px 8px; |
| | | padding: 12px 16px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .card-item:hover { |
| | | transform: translateY(-2px); |
| | | } |
| | | .card-item:hover { |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | .card-icon { |
| | | width: 80px; |
| | | height: 60px; |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | flex-shrink: 0; |
| | | margin-right: 12px; |
| | | } |
| | | .card-icon { |
| | | width: 80px; |
| | | height: 60px; |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | flex-shrink: 0; |
| | | margin-right: 12px; |
| | | } |
| | | |
| | | .card-title { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | flex-direction: column; |
| | | flex: 1; |
| | | } |
| | | .card-title { |
| | | display: flex; |
| | | align-items: flex-start; |
| | | flex-direction: column; |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-label { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | margin-bottom: 4px; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | width: 100%; |
| | | } |
| | | .card-label { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #ffffff; |
| | | margin-bottom: 4px; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | width: 100%; |
| | | } |
| | | |
| | | .card-value { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 4px; |
| | | } |
| | | .card-value { |
| | | display: flex; |
| | | align-items: baseline; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .card-rate { |
| | | margin-top: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: rgba(255, 255, 255, 0.85); |
| | | } |
| | | .card-rate { |
| | | margin-top: 4px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 6px; |
| | | font-weight: 400; |
| | | font-size: 12px; |
| | | color: rgba(255, 255, 255, 0.85); |
| | | } |
| | | |
| | | .rate-label { |
| | | opacity: 0.85; |
| | | } |
| | | .rate-label { |
| | | opacity: 0.85; |
| | | } |
| | | |
| | | .rate-value { |
| | | font-weight: 500; |
| | | } |
| | | .rate-value { |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .value-number { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | line-height: 1; |
| | | } |
| | | .value-number { |
| | | font-weight: 400; |
| | | font-size: 14px; |
| | | color: #ffffff; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .value-unit { |
| | | font-size: 14px; |
| | | color: #FFFFFF; |
| | | font-weight: 400; |
| | | } |
| | | .value-unit { |
| | | font-size: 14px; |
| | | color: #ffffff; |
| | | font-weight: 400; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="sales-statistics-container"> |
| | | <div class="data-dashboard"> |
| | | <!-- 页颿 é¢ --> |
| | | <!-- <div class="dashboard-header"> |
| | | <div class="factory-name">éå®ç»è®¡çæ¿</div> |
| | | </div> --> |
| | | <!-- ç鿡件 --> |
| | | <div class="filter-area"> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">æ¶é´èå´ï¼</span> |
| | | <el-date-picker v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | @change="handleDateChange" |
| | | style="width: 240px;" /> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">产åç±»åï¼</span> |
| | | <el-select v-model="productType" |
| | | placeholder="è¯·éæ©äº§åç±»å" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="å
¨é¨" |
| | | value="" /> |
| | | <el-option label="ç å" |
| | | value="block" /> |
| | | <el-option label="æ¿æ" |
| | | value="board" /> |
| | | <el-option label="åæ" |
| | | value="profile" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">éå®åºåï¼</span> |
| | | <el-select v-model="salesArea" |
| | | placeholder="è¯·éæ©éå®åºå" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="å
¨é¨" |
| | | value="" /> |
| | | <el-option label="åä¸" |
| | | value="east" /> |
| | | <el-option label="åå" |
| | | value="north" /> |
| | | <el-option label="åå" |
| | | value="south" /> |
| | | <el-option label="西å" |
| | | value="southwest" /> |
| | | <el-option label="西å" |
| | | value="northwest" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">ç»è®¡ç»´åº¦ï¼</span> |
| | | <el-select v-model="statDimension" |
| | | placeholder="è¯·éæ©ç»è®¡ç»´åº¦" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="æåº¦" |
| | | value="month" /> |
| | | <el-option label="年度" |
| | | value="year" /> |
| | | </el-select> |
| | | </div> |
| | | </div> |
| | | <div class="dashboard-content"> |
| | | <!-- æ ¸å¿ææ å¡ç --> |
| | | <div class="row row-1"> |
| | | <div class="panel-card card-1"> |
| | | <div class="panel-title">å计éé</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-volume-color">{{ totalSalesVolume }}</div> |
| | | <div class="stat-unit">ç«æ¹ç±³</div> |
| | | <div class="stat-change">{{ salesVolumeChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-2"> |
| | | <div class="panel-title">éå®éé¢</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-amount-color">{{ totalSalesAmount }}</div> |
| | | <div class="stat-unit">ä¸å
</div> |
| | | <div class="stat-change">{{ salesAmountChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-3"> |
| | | <div class="panel-title">æ°å¢å®¢æ·</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value new-customer-color">{{ newCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ customerCountChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-4"> |
| | | <div class="panel-title">å计客æ·</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value total-customer-color">{{ totalCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ totalCustomerChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- ééåéå®éé¢è¶å¿ --> |
| | | <div class="row row-2"> |
| | | <div class="panel-card card-5"> |
| | | <div class="panel-title">ééè¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-6"> |
| | | <div class="panel-title">éå®éé¢è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- ç´¯è®¡æ°æ®è¶å¿ --> |
| | | <!-- <div class="row row-3"> |
| | | <div class="panel-card card-10"> |
| | | <div class="panel-title">累计ééè¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-11"> |
| | | <div class="panel-title">累计éå®éé¢è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> --> |
| | | <!-- å¾è¡¨åºååè¡¨æ ¼ --> |
| | | <div class="row row-4"> |
| | | <!-- 左边ï¼è¯¦ç»æ°æ®è¡¨æ ¼ --> |
| | | <div class="panel-card card-9" |
| | | style="flex: 2;"> |
| | | <div class="panel-title">éå®ç»è®¡è¯¦ç»æ°æ®</div> |
| | | <div class="table-container"> |
| | | <el-table :data="tableData" |
| | | style="width: 100%"> |
| | | <el-table-column prop="productType" |
| | | label="产åç±»å" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getProductTypeType(scope.row.productType)"> |
| | | {{ scope.row.productType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesArea" |
| | | label="éå®åºå" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getSalesAreaType(scope.row.salesArea)"> |
| | | {{ scope.row.salesArea }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="period" |
| | | label="ç»è®¡å¨æ" |
| | | width="120" /> |
| | | <el-table-column prop="salesVolume" |
| | | label="éé(ç«æ¹ç±³)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesVolume }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesAmount" |
| | | label="éå®éé¢(ä¸å
)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesAmount }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="newCustomers" |
| | | label="æ°å¢å®¢æ·(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.newCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCustomers" |
| | | label="å计客æ·(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.totalCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | <!-- å³è¾¹ï¼äº§åç±»ååå¸åéå®åºååå¸ --> |
| | | <div class="chart-column" |
| | | style="flex: 1; display: flex; flex-direction: column; gap: 20px;"> |
| | | <div class="panel-card card-7" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">产åç±»ååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="productTypeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-8" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">éå®åºååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAreaChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { |
| | | ref, |
| | | computed, |
| | | onMounted, |
| | | onBeforeUnmount, |
| | | watch, |
| | | nextTick, |
| | | } from "vue"; |
| | | import { useRouter } from "vue-router"; |
| | | import * as echarts from "echarts"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const router = useRouter(); |
| | | |
| | | // ç鿡件 |
| | | const dateRange = ref([]); |
| | | const productType = ref(""); |
| | | const salesArea = ref(""); |
| | | const statDimension = ref("month"); |
| | | |
| | | // å¾è¡¨å¼ç¨ |
| | | const salesVolumeChart = ref(null); |
| | | const salesAmountChart = ref(null); |
| | | const productTypeChart = ref(null); |
| | | const salesAreaChart = ref(null); |
| | | const cumulativeSalesVolumeChart = ref(null); |
| | | const cumulativeSalesAmountChart = ref(null); |
| | | |
| | | // å¾è¡¨å®ä¾ |
| | | let salesVolumeChartInstance = null; |
| | | let salesAmountChartInstance = null; |
| | | let productTypeChartInstance = null; |
| | | let salesAreaChartInstance = null; |
| | | let cumulativeSalesVolumeChartInstance = null; |
| | | let cumulativeSalesAmountChartInstance = null; |
| | | |
| | | // æ¨¡ææ°æ® |
| | | const mockData = [ |
| | | // 2026å¹´1ææ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åä¸", |
| | | period: "2026-01", |
| | | salesVolume: 1200, |
| | | salesAmount: 180, |
| | | newCustomers: 5, |
| | | totalCustomers: 120, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-01", |
| | | salesVolume: 800, |
| | | salesAmount: 120, |
| | | newCustomers: 3, |
| | | totalCustomers: 80, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-01", |
| | | salesVolume: 600, |
| | | salesAmount: 90, |
| | | newCustomers: 2, |
| | | totalCustomers: 60, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åä¸", |
| | | period: "2026-01", |
| | | salesVolume: 900, |
| | | salesAmount: 270, |
| | | newCustomers: 4, |
| | | totalCustomers: 100, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åå", |
| | | period: "2026-01", |
| | | salesVolume: 500, |
| | | salesAmount: 150, |
| | | newCustomers: 2, |
| | | totalCustomers: 70, |
| | | }, |
| | | { |
| | | productType: "åæ", |
| | | salesArea: "åä¸", |
| | | period: "2026-01", |
| | | salesVolume: 400, |
| | | salesAmount: 200, |
| | | newCustomers: 3, |
| | | totalCustomers: 50, |
| | | }, |
| | | // 2026å¹´2ææ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åä¸", |
| | | period: "2026-02", |
| | | salesVolume: 1300, |
| | | salesAmount: 195, |
| | | newCustomers: 4, |
| | | totalCustomers: 124, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-02", |
| | | salesVolume: 850, |
| | | salesAmount: 127.5, |
| | | newCustomers: 2, |
| | | totalCustomers: 82, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-02", |
| | | salesVolume: 650, |
| | | salesAmount: 97.5, |
| | | newCustomers: 1, |
| | | totalCustomers: 61, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åä¸", |
| | | period: "2026-02", |
| | | salesVolume: 950, |
| | | salesAmount: 285, |
| | | newCustomers: 3, |
| | | totalCustomers: 103, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åå", |
| | | period: "2026-02", |
| | | salesVolume: 550, |
| | | salesAmount: 165, |
| | | newCustomers: 1, |
| | | totalCustomers: 71, |
| | | }, |
| | | { |
| | | productType: "åæ", |
| | | salesArea: "åä¸", |
| | | period: "2026-02", |
| | | salesVolume: 450, |
| | | salesAmount: 225, |
| | | newCustomers: 2, |
| | | totalCustomers: 52, |
| | | }, |
| | | // 2026å¹´3ææ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åä¸", |
| | | period: "2026-03", |
| | | salesVolume: 1400, |
| | | salesAmount: 210, |
| | | newCustomers: 6, |
| | | totalCustomers: 130, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-03", |
| | | salesVolume: 900, |
| | | salesAmount: 135, |
| | | newCustomers: 3, |
| | | totalCustomers: 85, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "åå", |
| | | period: "2026-03", |
| | | salesVolume: 700, |
| | | salesAmount: 105, |
| | | newCustomers: 2, |
| | | totalCustomers: 63, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åä¸", |
| | | period: "2026-03", |
| | | salesVolume: 1000, |
| | | salesAmount: 300, |
| | | newCustomers: 5, |
| | | totalCustomers: 108, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "åå", |
| | | period: "2026-03", |
| | | salesVolume: 600, |
| | | salesAmount: 180, |
| | | newCustomers: 2, |
| | | totalCustomers: 73, |
| | | }, |
| | | { |
| | | productType: "åæ", |
| | | salesArea: "åä¸", |
| | | period: "2026-03", |
| | | salesVolume: 500, |
| | | salesAmount: 250, |
| | | newCustomers: 3, |
| | | totalCustomers: 55, |
| | | }, |
| | | // 西åå西åå°åºæ°æ® |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 500, |
| | | salesAmount: 75, |
| | | newCustomers: 2, |
| | | totalCustomers: 40, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 300, |
| | | salesAmount: 90, |
| | | newCustomers: 1, |
| | | totalCustomers: 30, |
| | | }, |
| | | { |
| | | productType: "ç å", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 400, |
| | | salesAmount: 60, |
| | | newCustomers: 1, |
| | | totalCustomers: 35, |
| | | }, |
| | | { |
| | | productType: "æ¿æ", |
| | | salesArea: "西å", |
| | | period: "2026-03", |
| | | salesVolume: 200, |
| | | salesAmount: 60, |
| | | newCustomers: 1, |
| | | totalCustomers: 25, |
| | | }, |
| | | ]; |
| | | |
| | | // 计ç®å±æ§ |
| | | const filteredData = computed(() => { |
| | | let result = [...mockData]; |
| | | |
| | | // æäº§åç±»åçé |
| | | if (productType.value) { |
| | | result = result.filter(item => { |
| | | const typeMap = { block: "ç å", board: "æ¿æ", profile: "åæ" }; |
| | | return item.productType === typeMap[productType.value]; |
| | | }); |
| | | } |
| | | |
| | | // æéå®åºåçé |
| | | if (salesArea.value) { |
| | | result = result.filter(item => { |
| | | const areaMap = { |
| | | east: "åä¸", |
| | | north: "åå", |
| | | south: "åå", |
| | | southwest: "西å", |
| | | northwest: "西å", |
| | | }; |
| | | return item.salesArea === areaMap[salesArea.value]; |
| | | }); |
| | | } |
| | | |
| | | // ææ¶é´èå´çé |
| | | if (dateRange.value && dateRange.value.length === 2) { |
| | | const startDate = dayjs(dateRange.value[0]); |
| | | const endDate = dayjs(dateRange.value[1]); |
| | | |
| | | result = result.filter(item => { |
| | | const itemDate = dayjs(item.period); |
| | | return ( |
| | | itemDate.isAfter(startDate.subtract(1, "day")) && |
| | | itemDate.isBefore(endDate.add(1, "day")) |
| | | ); |
| | | }); |
| | | } |
| | | |
| | | return result; |
| | | }); |
| | | |
| | | // æ ¸å¿ææ è®¡ç® |
| | | const totalSalesVolume = computed(() => { |
| | | return filteredData.value.reduce((sum, item) => sum + item.salesVolume, 0); |
| | | }); |
| | | |
| | | const totalSalesAmount = computed(() => { |
| | | return filteredData.value |
| | | .reduce((sum, item) => sum + item.salesAmount, 0) |
| | | .toFixed(2); |
| | | }); |
| | | |
| | | const newCustomerCount = computed(() => { |
| | | return filteredData.value.reduce((sum, item) => sum + item.newCustomers, 0); |
| | | }); |
| | | |
| | | const totalCustomerCount = computed(() => { |
| | | // è®¡ç®æ¯ä¸ªåºåå产åç±»åçæå¤§å®¢æ·æ° |
| | | const customerMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | const key = `${item.productType}-${item.salesArea}`; |
| | | if (!customerMap[key] || item.totalCustomers > customerMap[key]) { |
| | | customerMap[key] = item.totalCustomers; |
| | | } |
| | | }); |
| | | return Object.values(customerMap).reduce((sum, count) => sum + count, 0); |
| | | }); |
| | | |
| | | // ååç计ç®ï¼æ¨¡æï¼ |
| | | const salesVolumeChange = ref("+5.2"); |
| | | const salesAmountChange = ref("+7.8"); |
| | | const customerCountChange = ref("+3.5"); |
| | | const totalCustomerChange = ref("+2.1"); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const tableData = computed(() => { |
| | | return filteredData.value.map(item => { |
| | | // 计ç®ç´¯è®¡å¼ï¼æ¨¡æï¼ |
| | | const cumulativeSalesVolume = item.salesVolume * 1.5; |
| | | const cumulativeSalesAmount = item.salesAmount * 1.5; |
| | | const cumulativeNewCustomers = item.newCustomers * 2; |
| | | |
| | | return { |
| | | ...item, |
| | | cumulativeSalesVolume, |
| | | cumulativeSalesAmount, |
| | | cumulativeNewCustomers, |
| | | }; |
| | | }); |
| | | }); |
| | | |
| | | // ééè¶å¿å¾è¡¨é
ç½® |
| | | const salesVolumeChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesVolume; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "ééï¼ç«æ¹ç±³ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // éå®éé¢è¶å¿å¾è¡¨é
ç½® |
| | | const salesAmountChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesAmount; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ä¸å
", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "éå®éé¢ï¼ä¸å
ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 产åç±»ååå¸å¾è¡¨é
ç½® |
| | | const productTypeChartOption = computed(() => { |
| | | // æäº§åç±»ååç» |
| | | const typeMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!typeMap[item.productType]) { |
| | | typeMap[item.productType] = 0; |
| | | } |
| | | typeMap[item.productType] += item.salesVolume; |
| | | }); |
| | | |
| | | const types = Object.keys(typeMap); |
| | | const values = types.map(type => typeMap[type]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} ç«æ¹ç±³ ({d}%)", |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: types.map((type, index) => ({ |
| | | name: type, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // éå®åºååå¸å¾è¡¨é
ç½® |
| | | const salesAreaChartOption = computed(() => { |
| | | // æéå®åºååç» |
| | | const areaMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!areaMap[item.salesArea]) { |
| | | areaMap[item.salesArea] = 0; |
| | | } |
| | | areaMap[item.salesArea] += item.salesVolume; |
| | | }); |
| | | |
| | | const areas = Object.keys(areaMap); |
| | | const values = areas.map(area => areaMap[area]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} ç«æ¹ç±³ ({d}%)", |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: areas.map((area, index) => ({ |
| | | name: area, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 累计ééè¶å¿å¾è¡¨é
ç½® |
| | | const cumulativeSalesVolumeChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | let cumulativeValue = 0; |
| | | |
| | | // æå¨ææåº |
| | | const sortedData = [...filteredData.value].sort((a, b) => |
| | | a.period.localeCompare(b.period) |
| | | ); |
| | | |
| | | sortedData.forEach(item => { |
| | | cumulativeValue += item.salesVolume; |
| | | periodMap[item.period] = cumulativeValue; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计ééï¼ç«æ¹ç±³ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | areaStyle: { |
| | | opacity: 0.3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#E6A23C", |
| | | }, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 累计éå®éé¢è¶å¿å¾è¡¨é
ç½® |
| | | const cumulativeSalesAmountChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | let cumulativeValue = 0; |
| | | |
| | | // æå¨ææåº |
| | | const sortedData = [...filteredData.value].sort((a, b) => |
| | | a.period.localeCompare(b.period) |
| | | ); |
| | | |
| | | sortedData.forEach(item => { |
| | | cumulativeValue += item.salesAmount; |
| | | periodMap[item.period] = cumulativeValue; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | |
| | | return { |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ä¸å
", |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计éå®éé¢ï¼ä¸å
ï¼", |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | itemStyle: { |
| | | color: "#F56C6C", |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // æ¹æ³ |
| | | const goBack = () => { |
| | | router.back(); |
| | | }; |
| | | |
| | | const handleDateChange = () => { |
| | | // å¤çæ¥æåå |
| | | updateCharts(); |
| | | }; |
| | | |
| | | const handleFilterChange = () => { |
| | | // å¤çç鿡件åå |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | | // åå§åééè¶å¿å¾è¡¨ |
| | | if (salesVolumeChart.value && !salesVolumeChartInstance) { |
| | | salesVolumeChartInstance = echarts.init(salesVolumeChart.value); |
| | | } |
| | | |
| | | // åå§åéå®éé¢è¶å¿å¾è¡¨ |
| | | if (salesAmountChart.value && !salesAmountChartInstance) { |
| | | salesAmountChartInstance = echarts.init(salesAmountChart.value); |
| | | } |
| | | |
| | | // åå§å产åç±»ååå¸å¾è¡¨ |
| | | if (productTypeChart.value && !productTypeChartInstance) { |
| | | productTypeChartInstance = echarts.init(productTypeChart.value); |
| | | } |
| | | |
| | | // åå§åéå®åºååå¸å¾è¡¨ |
| | | if (salesAreaChart.value && !salesAreaChartInstance) { |
| | | salesAreaChartInstance = echarts.init(salesAreaChart.value); |
| | | } |
| | | |
| | | // åå§å累计ééè¶å¿å¾è¡¨ |
| | | if (cumulativeSalesVolumeChart.value && !cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance = echarts.init( |
| | | cumulativeSalesVolumeChart.value |
| | | ); |
| | | } |
| | | |
| | | // åå§å累计éå®éé¢è¶å¿å¾è¡¨ |
| | | if (cumulativeSalesAmountChart.value && !cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance = echarts.init( |
| | | cumulativeSalesAmountChart.value |
| | | ); |
| | | } |
| | | |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | const updateCharts = () => { |
| | | // æ´æ°ééè¶å¿å¾è¡¨ |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.setOption(salesVolumeChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°éå®éé¢è¶å¿å¾è¡¨ |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.setOption(salesAmountChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°äº§åç±»ååå¸å¾è¡¨ |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.setOption(productTypeChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°éå®åºååå¸å¾è¡¨ |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.setOption(salesAreaChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°ç´¯è®¡ééè¶å¿å¾è¡¨ |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.setOption( |
| | | cumulativeSalesVolumeChartOption.value |
| | | ); |
| | | } |
| | | |
| | | // æ´æ°ç´¯è®¡éå®éé¢è¶å¿å¾è¡¨ |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.setOption( |
| | | cumulativeSalesAmountChartOption.value |
| | | ); |
| | | } |
| | | }; |
| | | |
| | | // çå¬çªå£å¤§å°åå |
| | | const handleResize = () => { |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.resize(); |
| | | } |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.resize(); |
| | | } |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.resize(); |
| | | } |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.resize(); |
| | | } |
| | | }; |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // 设置é»è®¤æ¥æèå´ä¸ºæè¿3个æ |
| | | const endDate = dayjs(); |
| | | const startDate = endDate.subtract(3, "month"); |
| | | dateRange.value = [ |
| | | startDate.format("YYYY-MM-DD"), |
| | | endDate.format("YYYY-MM-DD"), |
| | | ]; |
| | | |
| | | // çå¾
DOMæ´æ°ååå§åå¾è¡¨ |
| | | nextTick(() => { |
| | | initCharts(); |
| | | }); |
| | | |
| | | // æ·»å çªå£å¤§å°ååçå¬ |
| | | window.addEventListener("resize", handleResize); |
| | | }); |
| | | |
| | | // è·å产åç±»åæ ç¾ç±»å |
| | | const getProductTypeType = type => { |
| | | const typeMap = { |
| | | ç å: "primary", |
| | | æ¿æ: "success", |
| | | åæ: "warning", |
| | | }; |
| | | return typeMap[type] || "info"; |
| | | }; |
| | | |
| | | // è·åéå®åºåæ ç¾ç±»å |
| | | const getSalesAreaType = area => { |
| | | const typeMap = { |
| | | åä¸: "primary", |
| | | åå: "success", |
| | | åå: "warning", |
| | | 西å: "danger", |
| | | 西å: "info", |
| | | }; |
| | | return typeMap[area] || "info"; |
| | | }; |
| | | |
| | | // ç»ä»¶å¸è½½æ¶éæ¯å¾è¡¨å®ä¾ |
| | | onBeforeUnmount(() => { |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.dispose(); |
| | | } |
| | | if (salesAmountChartInstance) { |
| | | salesAmountChartInstance.dispose(); |
| | | } |
| | | if (productTypeChartInstance) { |
| | | productTypeChartInstance.dispose(); |
| | | } |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesAmountChartInstance) { |
| | | cumulativeSalesAmountChartInstance.dispose(); |
| | | } |
| | | |
| | | // ç§»é¤çªå£å¤§å°ååçå¬ |
| | | window.removeEventListener("resize", handleResize); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* å¤é¨å®¹å¨ - å æ®æ´ä¸ªè§å£ */ |
| | | .sales-statistics-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页é¢å¨å¸¸è§å¸å±ä¸ï¼æé¡¶æ ï¼é»è®¤åå» 84pxï¼é¿å
å
容被è£å */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* å
é¨å
容åºå - èªéåºå®½åº¦ */ |
| | | .data-dashboard { |
| | | position: relative; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* è¡å¸å± */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼4ä¸ªææ å¡ç */ |
| | | .row-1 { |
| | | height: 180px; |
| | | } |
| | | |
| | | /* 第äºè¡ï¼2个è¶å¿å¾è¡¨ */ |
| | | .row-2 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼ç´¯è®¡æ°æ®è¶å¿ */ |
| | | .row-3 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第åè¡ï¼è¡¨æ ¼åå¾è¡¨ */ |
| | | .row-4 { |
| | | height: 600px; |
| | | } |
| | | |
| | | /* å¡çæ ·å¼ */ |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* å¡çå¸å± */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-5 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-7 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-8 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-9 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-10 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-11 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-5 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-6 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-7 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-8 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-9 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-10 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-11 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .stat-item { |
| | | background-color: #fafafa; |
| | | border-radius: 8px; |
| | | padding: 15px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 1px solid #e4e7ed; |
| | | min-height: 80px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | } |
| | | |
| | | .sales-volume-color { |
| | | color: #409eff; |
| | | text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | } |
| | | |
| | | .sales-amount-color { |
| | | color: #67c23a; |
| | | text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3); |
| | | } |
| | | |
| | | .new-customer-color { |
| | | color: #e6a23c; |
| | | text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3); |
| | | } |
| | | |
| | | .total-customer-color { |
| | | color: #f56c6c; |
| | | text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3); |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 3px; |
| | | } |
| | | |
| | | .stat-change { |
| | | font-size: 12px; |
| | | color: #67c23a; |
| | | } |
| | | |
| | | /* è¡¨æ ¼æ ·å¼ */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | } |
| | | |
| | | .data-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | /* ä¸æéæ©æ¡æ ·å¼ */ |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | } |
| | | |
| | | :deep(.el-date-picker) { |
| | | width: 100%; |
| | | } |
| | | </style> |
| | |
| | | <template> |
| | | <div class="sales-statistics-container"> |
| | | <div class="data-dashboard"> |
| | | <!-- 页颿 é¢ --> |
| | | <!-- <div class="dashboard-header"> |
| | | <div class="factory-name">éå®ç»è®¡çæ¿</div> |
| | | </div> --> |
| | | <!-- ç鿡件 --> |
| | | <div class="filter-area"> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">æ¶é´èå´ï¼</span> |
| | | <el-date-picker v-model="dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | value-format="YYYY-MM-DD" |
| | | @change="handleDateChange" |
| | | style="width: 240px;" /> |
| | | <div ref="screenRoot" |
| | | class="sales-statistics-container" |
| | | :class="{ 'is-fullscreen': isFullscreen }"> |
| | | <div class="bi-bg"></div> |
| | | <div class="bi-topbar"> |
| | | <img class="bi-topbar-title-bg" |
| | | src="@/assets/BI/biaoti.png" |
| | | alt="éå®çæ¿ç»è®¡" /> |
| | | <div class="bi-topbar-content"> |
| | | <div class="bi-topbar-left"> |
| | | <button class="fullscreen-btn" |
| | | @click="toggleFullscreen" |
| | | :title="isFullscreen ? 'éåºå
¨å±' : 'å
¨å±æ¾ç¤º'"> |
| | | <svg v-if="!isFullscreen" |
| | | width="1.6vh" |
| | | height="1.6vh" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3" /> |
| | | </svg> |
| | | <svg v-else |
| | | width="1.6vh" |
| | | height="1.6vh" |
| | | viewBox="0 0 24 24" |
| | | fill="none" |
| | | stroke="currentColor" |
| | | stroke-width="2"> |
| | | <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" /> |
| | | </svg> |
| | | </button> |
| | | <!-- <span class="status-sun">â</span> |
| | | <span>26â</span> |
| | | <span class="bi-topbar-sep">湿度ï¼1</span> --> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">产åç±»åï¼</span> |
| | | <el-select v-model="productType" |
| | | placeholder="è¯·éæ©äº§åç±»å" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="å
¨é¨" |
| | | value="" /> |
| | | <el-option label="ç å" |
| | | value="block" /> |
| | | <el-option label="æ¿æ" |
| | | value="board" /> |
| | | <el-option label="åæ" |
| | | value="profile" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">éå®åºåï¼</span> |
| | | <el-select v-model="salesArea" |
| | | placeholder="è¯·éæ©éå®åºå" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="å
¨é¨" |
| | | value="" /> |
| | | <el-option label="åä¸" |
| | | value="east" /> |
| | | <el-option label="åå" |
| | | value="north" /> |
| | | <el-option label="åå" |
| | | value="south" /> |
| | | <el-option label="西å" |
| | | value="southwest" /> |
| | | <el-option label="西å" |
| | | value="northwest" /> |
| | | </el-select> |
| | | </div> |
| | | <div class="filter-section"> |
| | | <span class="filter-label">ç»è®¡ç»´åº¦ï¼</span> |
| | | <el-select v-model="statDimension" |
| | | placeholder="è¯·éæ©ç»è®¡ç»´åº¦" |
| | | @change="handleFilterChange" |
| | | style="width: 160px;"> |
| | | <el-option label="æåº¦" |
| | | value="month" /> |
| | | <el-option label="年度" |
| | | value="year" /> |
| | | </el-select> |
| | | <div class="bi-topbar-title">éå®çæ¿ç»è®¡</div> |
| | | <div class="bi-topbar-meta"> |
| | | <span class="bi-topbar-time">{{ currentTime }}</span> |
| | | <span class="bi-topbar-sep">|</span> |
| | | <span class="bi-topbar-date">{{ currentDateText }}</span> |
| | | </div> |
| | | </div> |
| | | <div class="dashboard-content"> |
| | | <!-- æ ¸å¿ææ å¡ç --> |
| | | <div class="row row-1"> |
| | | <div class="panel-card card-1"> |
| | | <div class="panel-title">å计éé</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-volume-color">{{ totalSalesVolume }}</div> |
| | | <div class="stat-unit">ç«æ¹ç±³</div> |
| | | <div class="stat-change">{{ salesVolumeChange }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="bi-dashboard-grid"> |
| | | <!-- å·¦ä¸ï¼ééè¶å¿ --> |
| | | <div class="bi-panel bi-panel-top-left"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="éå®åæ-ç å" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item active">å¹´</span> |
| | | <span class="tab-item">æ</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-filter-tabs"> |
| | | <span class="cf-tab active">***éå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | </div> |
| | | <div class="panel-card card-2"> |
| | | <div class="panel-title">éå®éé¢</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value sales-amount-color">{{ totalSalesAmount }}</div> |
| | | <div class="stat-unit">ä¸å
</div> |
| | | <div class="stat-change">{{ salesAmountChange }}%</div> |
| | | </div> |
| | | </div> |
| | | <div class="chart-unit-row"> |
| | | <span>åä½ï¼ç«æ¹ç±³</span> |
| | | <span class="dot-legend">æ¿æ</span> |
| | | </div> |
| | | <div class="panel-card card-3"> |
| | | <div class="panel-title">æ°å¢å®¢æ·</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value new-customer-color">{{ newCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ customerCountChange }}%</div> |
| | | </div> |
| | | </div> |
| | | <div ref="salesVolumeChart" |
| | | class="echart-fill"></div> |
| | | </div> |
| | | </div> |
| | | <!-- å³ä¸ï¼éå®éé¢ --> |
| | | <div class="bi-panel bi-panel-top-right"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="éå®åæ-æ¿æ" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item active">å¹´</span> |
| | | <span class="tab-item">æ</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-filter-tabs"> |
| | | <span class="cf-tab active">***éå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | </div> |
| | | <div class="panel-card card-4"> |
| | | <div class="panel-title">å计客æ·</div> |
| | | <div class="stats-grid"> |
| | | <div class="stat-item"> |
| | | <div class="stat-value total-customer-color">{{ totalCustomerCount }}</div> |
| | | <div class="stat-unit">个</div> |
| | | <div class="stat-change">{{ totalCustomerChange }}%</div> |
| | | </div> |
| | | </div> |
| | | <div class="chart-unit-row"> |
| | | <span>åä½ï¼ä»¶</span> |
| | | <span class="dot-legend">æ¿æ</span> |
| | | </div> |
| | | <div ref="salesAmountChart" |
| | | class="echart-fill"></div> |
| | | </div> |
| | | </div> |
| | | <!-- ä¸é´ä¸å¿ç¯ --> |
| | | <div class="center-ring"> |
| | | <!-- <img class="center-ring-bg" |
| | | src="@/assets/BI/zonghetongbingtubiankuang@2x.png" |
| | | alt="" /> --> |
| | | <!-- <div class="center-ring-content"> --> |
| | | <!-- <div class="center-ring-title">éå®<br />ä¸å¿</div> |
| | | <div class="center-metric m1"> |
| | | <div class="center-metric-label">æ°å¢å®¢æ·</div> |
| | | <div class="center-metric-value">{{ centerNewCustomerCount }}</div> |
| | | <div class="center-metric-unit">人</div> |
| | | </div> |
| | | <div class="center-metric m2"> |
| | | <div class="center-metric-label">æäº¤æ»è®¢å</div> |
| | | <div class="center-metric-value">{{ completedOrders }}</div> |
| | | <div class="center-metric-unit">å</div> |
| | | </div> |
| | | <div class="center-metric m3"> |
| | | <div class="center-metric-label">æ°å¢è®¢å</div> |
| | | <div class="center-metric-value">{{ salesOrderCount }}</div> |
| | | <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> --> |
| | | <!-- </div> --> |
| | | <div class="center-ring-box"> |
| | | <div class="center-metric m1"> |
| | | <div class="center-metric-label">æ°å¢å®¢æ·</div> |
| | | <div class="center-metric-value">{{ centerNewCustomerCount }}</div> |
| | | <div class="center-metric-unit">人</div> |
| | | </div> |
| | | <div class="center-metric m2"> |
| | | <div class="center-metric-label">æäº¤æ»è®¢å</div> |
| | | <div class="center-metric-value">{{ completedOrders }}</div> |
| | | <div class="center-metric-unit">å</div> |
| | | </div> |
| | | <div class="center-metric m3"> |
| | | <div class="center-metric-label">æ°å¢è®¢å</div> |
| | | <div class="center-metric-value">{{ salesOrderCount }}</div> |
| | | <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> |
| | | </div> |
| | | <!-- ééåéå®éé¢è¶å¿ --> |
| | | <div class="row row-2"> |
| | | <div class="panel-card card-5"> |
| | | <div class="panel-title">ééè¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-6"> |
| | | <div class="panel-title">éå®éé¢è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- å·¦ä¸ï¼äº§åç±»åéé --> |
| | | <div class="bi-panel bi-panel-bottom-left"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="客æ·ééæååæ-ç å" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item active">å¹´</span> |
| | | <span class="tab-item">æ</span> |
| | | </div> |
| | | <!-- ç´¯è®¡æ°æ®è¶å¿ --> |
| | | <!-- <div class="row row-3"> |
| | | <div class="panel-card card-10"> |
| | | <div class="panel-title">累计ééè¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesVolumeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-filter-tabs"> |
| | | <span class="cf-tab active">***éå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | </div> |
| | | <div class="panel-card card-11"> |
| | | <div class="panel-title">累计éå®éé¢è¶å¿</div> |
| | | <div class="chart-container"> |
| | | <div ref="cumulativeSalesAmountChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | <div ref="productTypeChart" |
| | | class="echart-fill"></div> |
| | | </div> |
| | | </div> |
| | | <!-- ä¸ä¸ï¼æ°å¢å®¢æ·åæï¼å产åç±»åè¶å¿ï¼ --> |
| | | <div class="bi-panel bi-panel-bottom-center"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="æ°å¢å®¢æ·è¶å¿åæ" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item active">å¹´</span> |
| | | <span class="tab-item">æ</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-mini-title"> |
| | | <span class="diamond"></span> |
| | | <span>æ°å¢å®¢æ·æ°</span> |
| | | </div> |
| | | </div> --> |
| | | <!-- å¾è¡¨åºååè¡¨æ ¼ --> |
| | | <div class="row row-4"> |
| | | <!-- 左边ï¼è¯¦ç»æ°æ®è¡¨æ ¼ --> |
| | | <div class="panel-card card-9" |
| | | style="flex: 2;"> |
| | | <div class="panel-title">éå®ç»è®¡è¯¦ç»æ°æ®</div> |
| | | <div class="table-container"> |
| | | <el-table :data="tableData" |
| | | style="width: 100%"> |
| | | <el-table-column prop="productType" |
| | | label="产åç±»å" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getProductTypeType(scope.row.productType)"> |
| | | {{ scope.row.productType }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesArea" |
| | | label="éå®åºå" |
| | | width="120" |
| | | align="center"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getSalesAreaType(scope.row.salesArea)"> |
| | | {{ scope.row.salesArea }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="period" |
| | | label="ç»è®¡å¨æ" |
| | | width="120" /> |
| | | <el-table-column prop="salesVolume" |
| | | label="éé(ç«æ¹ç±³)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesVolume }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="salesAmount" |
| | | label="éå®éé¢(ä¸å
)" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.salesAmount }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="newCustomers" |
| | | label="æ°å¢å®¢æ·(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.newCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalCustomers" |
| | | label="å计客æ·(个)" |
| | | width="150" |
| | | align="right"> |
| | | <template #default="scope"> |
| | | <span class="data-value">{{ scope.row.totalCustomers }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | <div class="chart-unit-row chart-unit-single"> |
| | | <span>åä½ï¼äºº</span> |
| | | </div> |
| | | <!-- å³è¾¹ï¼äº§åç±»ååå¸åéå®åºååå¸ --> |
| | | <div class="chart-column" |
| | | style="flex: 1; display: flex; flex-direction: column; gap: 20px;"> |
| | | <div class="panel-card card-7" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">产åç±»ååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="productTypeChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div class="panel-card card-8" |
| | | style="flex: 1;"> |
| | | <div class="panel-title">éå®åºååå¸</div> |
| | | <div class="chart-container"> |
| | | <div ref="salesAreaChart" |
| | | style="width: 100%; height: 100%;"></div> |
| | | </div> |
| | | </div> |
| | | <div ref="productTypeTrendChart" |
| | | class="echart-fill"></div> |
| | | </div> |
| | | </div> |
| | | <!-- å³ä¸ï¼éå®åºåéé --> |
| | | <div class="bi-panel bi-panel-bottom-right"> |
| | | <PanelHeader :isFullscreen="true" |
| | | title="客æ·ééæååæ-æ¿æ" /> |
| | | <div class="panel-tabs"> |
| | | <span class="tab-item active">å¹´</span> |
| | | <span class="tab-item">æ</span> |
| | | </div> |
| | | <div class="bi-panel-body"> |
| | | <div class="chart-filter-tabs"> |
| | | <span class="cf-tab active">***éå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | <span class="cf-tab">xxxéå®åº</span> |
| | | </div> |
| | | <div ref="salesAreaChart" |
| | | class="echart-fill"></div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | |
| | | import { useRouter } from "vue-router"; |
| | | import * as echarts from "echarts"; |
| | | import dayjs from "dayjs"; |
| | | import PanelHeader from "@/views/reportAnalysis/PSIDataAnalysis/components/PanelHeader.vue"; |
| | | |
| | | const router = useRouter(); |
| | | const screenRoot = ref(null); |
| | | const isFullscreen = ref(false); |
| | | |
| | | // 顶鍿 æ¶é´ï¼ç¨äºå¹é
BI大屿æå¾ï¼ |
| | | const now = ref(dayjs()); |
| | | const currentTime = computed(() => now.value.format("HH:mm:ss")); |
| | | const currentDateText = computed(() => { |
| | | const weekMap = { |
| | | 0: "æææ¥", |
| | | 1: "ææä¸", |
| | | 2: "ææäº", |
| | | 3: "ææä¸", |
| | | 4: "ææå", |
| | | 5: "ææäº", |
| | | 6: "ææå
", |
| | | }; |
| | | return `${now.value.format("YYYY-MM-DD")} ${weekMap[now.value.day()] || ""}`; |
| | | }); |
| | | let timeTicker = null; |
| | | |
| | | const handleFullscreenChange = () => { |
| | | isFullscreen.value = !!document.fullscreenElement; |
| | | nextTick(() => { |
| | | handleResize(); |
| | | }); |
| | | }; |
| | | |
| | | const toggleFullscreen = async () => { |
| | | const rootEl = screenRoot.value; |
| | | if (!rootEl) return; |
| | | try { |
| | | if (!document.fullscreenElement) { |
| | | await rootEl.requestFullscreen(); |
| | | } else { |
| | | await document.exitFullscreen(); |
| | | } |
| | | } catch (error) { |
| | | console.error("å
¨å±åæ¢å¤±è´¥:", error); |
| | | } |
| | | }; |
| | | |
| | | // ç鿡件 |
| | | const dateRange = ref([]); |
| | |
| | | const salesAmountChart = ref(null); |
| | | const productTypeChart = ref(null); |
| | | const salesAreaChart = ref(null); |
| | | const productTypeTrendChart = ref(null); |
| | | const cumulativeSalesVolumeChart = ref(null); |
| | | const cumulativeSalesAmountChart = ref(null); |
| | | |
| | |
| | | let salesAmountChartInstance = null; |
| | | let productTypeChartInstance = null; |
| | | let salesAreaChartInstance = null; |
| | | let productTypeTrendChartInstance = null; |
| | | let cumulativeSalesVolumeChartInstance = null; |
| | | let cumulativeSalesAmountChartInstance = null; |
| | | |
| | |
| | | return Object.values(customerMap).reduce((sum, count) => sum + count, 0); |
| | | }); |
| | | |
| | | // ä¸é´ä¸å¿ç¯ææ ï¼ç¨äºå¤§å±å±ç¤ºï¼ä½¿ç¨ç°æç»è®¡æ°æ®åæ å°ï¼ |
| | | const centerNewCustomerCount = computed(() => 112); |
| | | const completedOrders = computed(() => 1829); |
| | | const salesOrderCount = computed(() => 34); |
| | | const totalSalesAreaCount = computed(() => 12); |
| | | |
| | | // ååç计ç®ï¼æ¨¡æï¼ |
| | | const salesVolumeChange = ref("+5.2"); |
| | | const salesAmountChange = ref("+7.8"); |
| | |
| | | |
| | | // ééè¶å¿å¾è¡¨é
ç½® |
| | | const salesVolumeChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesVolume; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | const periods = ["6/9", "6/10", "6/11", "6/12", "6/12", "6/13"]; |
| | | const values = [132, 168, 168, 198, 168, 198]; |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | tooltip: { |
| | | trigger: "axis", |
| | | backgroundColor: "rgba(0,0,0,0.55)", |
| | | borderColor: "rgba(64,158,255,0.25)", |
| | | borderWidth: getResponsiveValue(1), |
| | | textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | grid: { |
| | | left: "10%", |
| | | right: "4%", |
| | | bottom: "16%", |
| | | top: "28%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(10), |
| | | }, |
| | | splitLine: { show: false }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "ééï¼ç«æ¹ç±³ï¼", |
| | | name: "", |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(8), |
| | | }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "line", |
| | | smooth: true, |
| | | lineStyle: { |
| | | width: 3, |
| | | }, |
| | | itemStyle: { |
| | | color: "#409EFF", |
| | | symbolSize: getResponsiveValue(8), |
| | | lineStyle: { width: getResponsiveValue(3), color: "#00A4ED" }, |
| | | itemStyle: { color: "#00A4ED" }, |
| | | areaStyle: { |
| | | opacity: 1, |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(0,164,237,0.35)" }, |
| | | { offset: 1, color: "rgba(0,164,237,0)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | ], |
| | |
| | | |
| | | // éå®éé¢è¶å¿å¾è¡¨é
ç½® |
| | | const salesAmountChartOption = computed(() => { |
| | | // æå¨æåç» |
| | | const periodMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!periodMap[item.period]) { |
| | | periodMap[item.period] = 0; |
| | | } |
| | | periodMap[item.period] += item.salesAmount; |
| | | }); |
| | | |
| | | const periods = Object.keys(periodMap).sort(); |
| | | const values = periods.map(period => periodMap[period]); |
| | | const periods = ["6/9", "6/10", "6/11", "6/12", "6/12", "6/13"]; |
| | | const values = [132, 168, 168, 198, 168, 198]; |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | tooltip: { |
| | | trigger: "axis", |
| | | backgroundColor: "rgba(0,0,0,0.55)", |
| | | borderColor: "rgba(64,158,255,0.25)", |
| | | borderWidth: getResponsiveValue(1), |
| | | textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | formatter: "{b}: {c} ä¸å
", |
| | | }, |
| | | grid: { |
| | | left: "10%", |
| | | right: "4%", |
| | | bottom: "16%", |
| | | top: "28%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(10), |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "éå®éé¢ï¼ä¸å
ï¼", |
| | | name: "", |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(8), |
| | | }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | series: [ |
| | | { |
| | | data: values, |
| | | type: "bar", |
| | | type: "line", |
| | | smooth: true, |
| | | symbolSize: getResponsiveValue(8), |
| | | itemStyle: { |
| | | color: "#67C23A", |
| | | color: "#00A4ED", |
| | | }, |
| | | lineStyle: { width: getResponsiveValue(3), color: "#00A4ED" }, |
| | | areaStyle: { |
| | | opacity: 1, |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(0,164,237,0.35)" }, |
| | | { offset: 1, color: "rgba(0,164,237,0)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // 产åç±»ååå¸å¾è¡¨é
ç½® |
| | | // 产åç±»åééå¾è¡¨é
ç½®ï¼æ¨ªåæ±ç¶ï¼ |
| | | const productTypeChartOption = computed(() => { |
| | | // æäº§åç±»ååç» |
| | | const typeMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!typeMap[item.productType]) { |
| | | typeMap[item.productType] = 0; |
| | | } |
| | | typeMap[item.productType] += item.salesVolume; |
| | | }); |
| | | |
| | | const types = Object.keys(typeMap); |
| | | const values = types.map(type => typeMap[type]); |
| | | const types = ["客æ·BB", "客æ·AA", "客æ·CC", "客æ·DD", "客æ·DD", "客æ·DD"]; |
| | | const values = [130, 120, 102, 90, 90, 70]; |
| | | const barColors = [ |
| | | "#34D8F7", |
| | | "#4A8BFF", |
| | | "#8A6BFF", |
| | | "#C8C447", |
| | | "#C8C447", |
| | | "#C8C447", |
| | | ]; |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} ç«æ¹ç±³ ({d}%)", |
| | | trigger: "axis", |
| | | axisPointer: { type: "shadow" }, |
| | | backgroundColor: "rgba(0,0,0,0.55)", |
| | | borderColor: "rgba(64,158,255,0.25)", |
| | | borderWidth: getResponsiveValue(1), |
| | | textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | grid: { |
| | | left: "14%", |
| | | right: "6%", |
| | | top: "16%", |
| | | bottom: "8%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "value", |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | yAxis: { |
| | | type: "category", |
| | | data: types, |
| | | axisTick: { show: false }, |
| | | axisLine: { show: false }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(8), |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: types.map((type, index) => ({ |
| | | name: type, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | name: "ééï¼ç«æ¹ç±³ï¼", |
| | | type: "bar", |
| | | barWidth: getResponsiveValue(14), |
| | | data: values, |
| | | itemStyle: { |
| | | color: params => barColors[params.dataIndex] || "#00A4ED", |
| | | borderRadius: [ |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | ], |
| | | }, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // éå®åºååå¸å¾è¡¨é
ç½® |
| | | // éå®åºåééå¾è¡¨é
ç½®ï¼æ¨ªåæ±ç¶ï¼ |
| | | const salesAreaChartOption = computed(() => { |
| | | // æéå®åºååç» |
| | | const areaMap = {}; |
| | | filteredData.value.forEach(item => { |
| | | if (!areaMap[item.salesArea]) { |
| | | areaMap[item.salesArea] = 0; |
| | | } |
| | | areaMap[item.salesArea] += item.salesVolume; |
| | | }); |
| | | |
| | | const areas = Object.keys(areaMap); |
| | | const values = areas.map(area => areaMap[area]); |
| | | const areas = ["客æ·BB", "客æ·AA", "客æ·CC", "客æ·DD", "客æ·DD", "客æ·DD"]; |
| | | const values = [130, 120, 102, 90, 90, 70]; |
| | | const barColors = [ |
| | | "#34D8F7", |
| | | "#4A8BFF", |
| | | "#8A6BFF", |
| | | "#C8C447", |
| | | "#C8C447", |
| | | "#C8C447", |
| | | ]; |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}: {c} ç«æ¹ç±³ ({d}%)", |
| | | trigger: "axis", |
| | | axisPointer: { type: "shadow" }, |
| | | backgroundColor: "rgba(0,0,0,0.55)", |
| | | borderColor: "rgba(64,158,255,0.25)", |
| | | borderWidth: getResponsiveValue(1), |
| | | textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | }, |
| | | grid: { |
| | | left: "14%", |
| | | right: "6%", |
| | | top: "16%", |
| | | bottom: "8%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "value", |
| | | axisLine: { show: false }, |
| | | axisLabel: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | yAxis: { |
| | | type: "category", |
| | | data: areas, |
| | | axisTick: { show: false }, |
| | | axisLine: { show: false }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(8), |
| | | }, |
| | | }, |
| | | series: [ |
| | | { |
| | | type: "pie", |
| | | radius: "60%", |
| | | data: areas.map((area, index) => ({ |
| | | name: area, |
| | | value: values[index], |
| | | })), |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 10, |
| | | shadowOffsetX: 0, |
| | | shadowColor: "rgba(0, 0, 0, 0.5)", |
| | | }, |
| | | name: "ééï¼ç«æ¹ç±³ï¼", |
| | | type: "bar", |
| | | barWidth: getResponsiveValue(14), |
| | | data: values, |
| | | itemStyle: { |
| | | color: params => barColors[params.dataIndex] || "#00A4ED", |
| | | borderRadius: [ |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | getResponsiveValue(6), |
| | | ], |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | | }); |
| | | |
| | | // æ°å¢å®¢æ·è¶å¿å¾è¡¨é
ç½®ï¼æäº§åç±»åå¤æçº¿ï¼ |
| | | const productTypeTrendChartOption = computed(() => { |
| | | const typeOrder = ["AAAéå®åº", "BBBéå®åº", "CCCéå®åº", "DDDéå®åº"]; |
| | | const colorMap = { |
| | | AAAéå®åº: "#65A0FF", |
| | | BBBéå®åº: "#33F5FF", |
| | | CCCéå®åº: "#FFD54A", |
| | | DDDéå®åº: "#EE52FF", |
| | | }; |
| | | const areaColorMap = { |
| | | AAAéå®åº: "rgba(101,160,255,0.28)", |
| | | BBBéå®åº: "rgba(51,245,255,0.30)", |
| | | CCCéå®åº: "rgba(255,213,74,0.25)", |
| | | DDDéå®åº: "rgba(238,82,255,0.25)", |
| | | }; |
| | | const periods = [ |
| | | "6/9", |
| | | "6/10", |
| | | "6/11", |
| | | "6/12", |
| | | "6/12", |
| | | "6/13", |
| | | "6/14", |
| | | "6/15", |
| | | ]; |
| | | const map = { |
| | | AAAéå®åº: [85, 112, 112, 112, 140, 112, 112, 140], |
| | | BBBéå®åº: [140, 180, 180, 180, 230, 180, 180, 230], |
| | | CCCéå®åº: [112, 140, 140, 140, 180, 140, 140, 180], |
| | | DDDéå®åº: [200, 165, 200, 200, 165, 165, 140, 140], |
| | | }; |
| | | |
| | | const series = typeOrder.map(t => ({ |
| | | name: t, |
| | | type: "line", |
| | | smooth: true, |
| | | symbolSize: getResponsiveValue(7), |
| | | showSymbol: true, |
| | | data: map[t] || [], |
| | | lineStyle: { width: getResponsiveValue(3), color: colorMap[t] }, |
| | | itemStyle: { color: colorMap[t] }, |
| | | areaStyle: { |
| | | opacity: 0.25, |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: areaColorMap[t] }, |
| | | { offset: 1, color: "rgba(0,0,0,0)" }, |
| | | ]), |
| | | }, |
| | | })); |
| | | |
| | | return { |
| | | backgroundColor: "transparent", |
| | | |
| | | legend: { |
| | | top: getResponsiveValue(10), |
| | | left: "center", |
| | | textStyle: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | padding: [0, 0, 0, getResponsiveValue(2)], |
| | | }, |
| | | itemWidth: getResponsiveValue(12), |
| | | itemHeight: getResponsiveValue(10), |
| | | itemGap: getResponsiveValue(18), |
| | | }, |
| | | tooltip: { |
| | | trigger: "axis", |
| | | backgroundColor: "rgba(0,0,0,0.55)", |
| | | borderColor: "rgba(64,158,255,0.25)", |
| | | borderWidth: getResponsiveValue(1), |
| | | textStyle: { color: "#B8C8E0", fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | grid: { |
| | | left: "10%", |
| | | right: "6%", |
| | | bottom: "14%", |
| | | top: "26%", |
| | | containLabel: true, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(10), |
| | | }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "", |
| | | axisLine: { lineStyle: { color: "rgba(184,200,224,0.25)" } }, |
| | | axisLabel: { |
| | | color: "#B8C8E0", |
| | | fontSize: getResponsiveValue(11), |
| | | margin: getResponsiveValue(8), |
| | | }, |
| | | splitLine: { lineStyle: { color: "rgba(184,200,224,0.12)" } }, |
| | | }, |
| | | series, |
| | | }; |
| | | }); |
| | | |
| | |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ç«æ¹ç±³", |
| | | textStyle: { fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | axisLabel: { fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计ééï¼ç«æ¹ç±³ï¼", |
| | | axisLabel: { fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | color: "#E6A23C", |
| | | }, |
| | | lineStyle: { |
| | | width: 3, |
| | | width: getResponsiveValue(3), |
| | | }, |
| | | }, |
| | | ], |
| | |
| | | tooltip: { |
| | | trigger: "axis", |
| | | formatter: "{b}: {c} ä¸å
", |
| | | textStyle: { fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: periods, |
| | | axisLabel: { fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "累计éå®éé¢ï¼ä¸å
ï¼", |
| | | axisLabel: { fontSize: getResponsiveValue(11) }, |
| | | }, |
| | | series: [ |
| | | { |
| | |
| | | // å¤çç鿡件åå |
| | | updateCharts(); |
| | | }; |
| | | const baseWidth = ref(1650); |
| | | // 计ç®ååºå¼å¼ |
| | | const getResponsiveValue = baseValue => { |
| | | return Math.round((baseValue * window.innerWidth) / baseWidth.value); |
| | | }; |
| | | |
| | | // åå§åå¾è¡¨ |
| | | const initCharts = () => { |
| | |
| | | salesAreaChartInstance = echarts.init(salesAreaChart.value); |
| | | } |
| | | |
| | | // åå§åæ°å¢å®¢æ·è¶å¿å¾è¡¨ |
| | | if (productTypeTrendChart.value && !productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance = echarts.init(productTypeTrendChart.value); |
| | | } |
| | | |
| | | // åå§å累计ééè¶å¿å¾è¡¨ |
| | | if (cumulativeSalesVolumeChart.value && !cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance = echarts.init( |
| | |
| | | |
| | | updateCharts(); |
| | | }; |
| | | |
| | | // æ´æ°å¾è¡¨ |
| | | const updateCharts = () => { |
| | | // æ´æ°ééè¶å¿å¾è¡¨ |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.setOption(salesVolumeChartOption.value); |
| | | salesVolumeChartInstance.setOption( |
| | | JSON.parse(JSON.stringify(salesVolumeChartOption.value)) |
| | | ); |
| | | } |
| | | |
| | | // æ´æ°éå®éé¢è¶å¿å¾è¡¨ |
| | |
| | | // æ´æ°éå®åºååå¸å¾è¡¨ |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.setOption(salesAreaChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°æ°å¢å®¢æ·è¶å¿å¾è¡¨ |
| | | if (productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance.setOption(productTypeTrendChartOption.value); |
| | | } |
| | | |
| | | // æ´æ°ç´¯è®¡ééè¶å¿å¾è¡¨ |
| | |
| | | |
| | | // çå¬çªå£å¤§å°åå |
| | | const handleResize = () => { |
| | | console.log("resize"); |
| | | // å
æ´æ°å¾è¡¨é项ï¼éæ°è®¡ç®ååºå¼å¼ |
| | | updateCharts(); |
| | | // ç¶åè°æ´å¾è¡¨å¤§å° |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.resize(); |
| | | } |
| | |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.resize(); |
| | | } |
| | | if (productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance.resize(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.resize(); |
| | | } |
| | |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // å¯å¨é¡¶é¨æ æ¶é´å·æ° |
| | | if (!timeTicker) { |
| | | timeTicker = setInterval(() => { |
| | | now.value = dayjs(); |
| | | }, 1000); |
| | | } |
| | | |
| | | // 设置é»è®¤æ¥æèå´ä¸ºæè¿3个æ |
| | | const endDate = dayjs(); |
| | | const startDate = endDate.subtract(3, "month"); |
| | |
| | | |
| | | // æ·»å çªå£å¤§å°ååçå¬ |
| | | window.addEventListener("resize", handleResize); |
| | | document.addEventListener("fullscreenchange", handleFullscreenChange); |
| | | }); |
| | | |
| | | // è·å产åç±»åæ ç¾ç±»å |
| | |
| | | |
| | | // ç»ä»¶å¸è½½æ¶éæ¯å¾è¡¨å®ä¾ |
| | | onBeforeUnmount(() => { |
| | | if (timeTicker) { |
| | | clearInterval(timeTicker); |
| | | timeTicker = null; |
| | | } |
| | | |
| | | if (salesVolumeChartInstance) { |
| | | salesVolumeChartInstance.dispose(); |
| | | } |
| | |
| | | if (salesAreaChartInstance) { |
| | | salesAreaChartInstance.dispose(); |
| | | } |
| | | |
| | | if (productTypeTrendChartInstance) { |
| | | productTypeTrendChartInstance.dispose(); |
| | | } |
| | | if (cumulativeSalesVolumeChartInstance) { |
| | | cumulativeSalesVolumeChartInstance.dispose(); |
| | | } |
| | |
| | | |
| | | // ç§»é¤çªå£å¤§å°ååçå¬ |
| | | window.removeEventListener("resize", handleResize); |
| | | document.removeEventListener("fullscreenchange", handleFullscreenChange); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | /* å¤é¨å®¹å¨ - å æ®æ´ä¸ªè§å£ */ |
| | | .sales-statistics-container { |
| | | position: relative; |
| | | width: 100%; |
| | | /* 页é¢å¨å¸¸è§å¸å±ä¸ï¼æé¡¶æ ï¼é»è®¤åå» 84pxï¼é¿å
å
容被è£å */ |
| | | min-height: calc(100vh - 84px); |
| | | background-color: #f5f7fa; |
| | | min-height: calc(100vh - 8.4vh); |
| | | overflow: hidden; |
| | | color: #b8c8e0; |
| | | background: #041026; |
| | | } |
| | | |
| | | /* å
é¨å
容åºå - èªéåºå®½åº¦ */ |
| | | .data-dashboard { |
| | | .sales-statistics-container.is-fullscreen { |
| | | min-height: 100vh; |
| | | height: 100vh; |
| | | } |
| | | |
| | | /* æ·±è²èæ¯å¾ */ |
| | | .bi-bg { |
| | | position: absolute; |
| | | inset: 0; |
| | | /* background-image: url("@/assets/BI/backImage@2x.png"); */ |
| | | background-size: cover; |
| | | background-position: center; |
| | | background-repeat: no-repeat; |
| | | z-index: 0; |
| | | } |
| | | |
| | | /* 顶鍿 颿 */ |
| | | .bi-topbar { |
| | | position: relative; |
| | | z-index: 2; |
| | | height: 5.8vh; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .bi-topbar-title-bg { |
| | | position: absolute; |
| | | top: 0; |
| | | left: 0; |
| | | height: 8vh; |
| | | width: 100%; |
| | | min-height: 100%; |
| | | background-color: #ffffff; |
| | | box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); |
| | | object-fit: cover; |
| | | z-index: 0; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .filter-area { |
| | | padding: 20px; |
| | | background-color: #ffffff; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | display: flex; |
| | | gap: 40px; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .filter-section { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .filter-label { |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .dashboard-content { |
| | | .bi-topbar-content { |
| | | position: relative; |
| | | z-index: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 20px; |
| | | padding: 20px; |
| | | min-height: 800px; |
| | | overflow: hidden; |
| | | } |
| | | |
| | | /* è¡å¸å± */ |
| | | .row { |
| | | display: flex; |
| | | gap: 20px; |
| | | align-items: stretch; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼4ä¸ªææ å¡ç */ |
| | | .row-1 { |
| | | height: 180px; |
| | | } |
| | | |
| | | /* 第äºè¡ï¼2个è¶å¿å¾è¡¨ */ |
| | | .row-2 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第ä¸è¡ï¼ç´¯è®¡æ°æ®è¶å¿ */ |
| | | .row-3 { |
| | | height: 350px; |
| | | } |
| | | |
| | | /* 第åè¡ï¼è¡¨æ ¼åå¾è¡¨ */ |
| | | .row-4 { |
| | | height: 600px; |
| | | } |
| | | |
| | | /* å¡çæ ·å¼ */ |
| | | .panel-card { |
| | | background-color: #ffffff; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | overflow: hidden; |
| | | display: flex; |
| | | flex-direction: column; |
| | | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .panel-card:hover { |
| | | box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | /* å¡çå¸å± */ |
| | | .card-1 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-2 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-3 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-4 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-5 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-6 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-7 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-8 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-9 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-10 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-11 { |
| | | flex: 1; |
| | | } |
| | | |
| | | .panel-title { |
| | | padding: 15px 20px; |
| | | font-size: 16px; |
| | | font-weight: 500; |
| | | color: #303133; |
| | | border-bottom: 1px solid #e4e7ed; |
| | | background-color: #fafafa; |
| | | } |
| | | |
| | | .card-1 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-2 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-3 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-4 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-5 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-6 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-7 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .card-8 .panel-title { |
| | | border-left: 4px solid #f56c6c; |
| | | } |
| | | |
| | | .card-9 .panel-title { |
| | | border-left: 4px solid #409eff; |
| | | } |
| | | |
| | | .card-10 .panel-title { |
| | | border-left: 4px solid #67c23a; |
| | | } |
| | | |
| | | .card-11 .panel-title { |
| | | border-left: 4px solid #e6a23c; |
| | | } |
| | | |
| | | .chart-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | } |
| | | |
| | | .table-container { |
| | | flex: 1; |
| | | padding: 20px; |
| | | overflow: auto; |
| | | } |
| | | |
| | | .stats-grid { |
| | | flex: 1; |
| | | padding: 15px; |
| | | width: 100%; |
| | | padding: 0 2.8vh; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | } |
| | | |
| | | .stat-item { |
| | | background-color: #fafafa; |
| | | border-radius: 8px; |
| | | padding: 15px; |
| | | .bi-topbar-title { |
| | | position: absolute; |
| | | left: 50%; |
| | | transform: translateX(-50%); |
| | | font-size: 2.6vh; |
| | | font-weight: 800; |
| | | letter-spacing: 0.1vh; |
| | | background: linear-gradient(180deg, #ffffff 0%, #b8dfff 100%); |
| | | -webkit-background-clip: text; |
| | | background-clip: text; |
| | | -webkit-text-fill-color: transparent; |
| | | color: transparent; |
| | | text-shadow: 0 0 2.6vh rgba(0, 164, 237, 0.55); |
| | | } |
| | | |
| | | .bi-topbar-left { |
| | | position: absolute; |
| | | left: 1vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 0.8vh; |
| | | color: rgba(208, 231, 255, 0.85); |
| | | font-size: 1.3vh; |
| | | } |
| | | |
| | | .status-sun { |
| | | color: #ffd85e; |
| | | text-shadow: 0 0 1vh rgba(255, 216, 94, 0.8); |
| | | font-size: 1.3vh; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .bi-topbar-meta { |
| | | position: absolute; |
| | | right: 5.2vh; |
| | | /* top: 1.6vh; */ |
| | | font-size: 1.2vh; |
| | | font-weight: 500; |
| | | letter-spacing: 0.05vh; |
| | | color: rgba(208, 231, 255, 0.85); |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 1vh; |
| | | } |
| | | |
| | | .fullscreen-btn { |
| | | position: absolute; |
| | | bottom: -1vh; |
| | | transform: none; |
| | | border: 0.1vh solid rgba(64, 158, 255, 0.45); |
| | | background: rgba(0, 164, 237, 0.14); |
| | | color: #d0e7ff; |
| | | width: 3.4vh; |
| | | height: 3.4vh; |
| | | border-radius: 0.6vh; |
| | | padding: 0; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | z-index: 10; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | border: 1px solid #e4e7ed; |
| | | min-height: 80px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | margin-bottom: 5px; |
| | | .fullscreen-btn:hover { |
| | | background: rgba(0, 164, 237, 0.24); |
| | | box-shadow: 0 0 1.2vh rgba(0, 164, 237, 0.3); |
| | | } |
| | | |
| | | .sales-volume-color { |
| | | color: #409eff; |
| | | text-shadow: 0 2px 4px rgba(64, 158, 255, 0.3); |
| | | .bi-topbar-sep { |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .sales-amount-color { |
| | | color: #67c23a; |
| | | text-shadow: 0 2px 4px rgba(103, 194, 58, 0.3); |
| | | /* 主ä½ç½æ ¼å¸å± */ |
| | | .bi-dashboard-grid { |
| | | position: relative; |
| | | z-index: 2; |
| | | height: calc(100vh - 8.4vh - 5.8vh); |
| | | min-height: 45vh; |
| | | padding: 1vh 1.8vh 1.4vh; |
| | | display: grid; |
| | | grid-template-columns: 1fr 1.05fr 1fr; |
| | | grid-template-rows: 1fr 1fr; |
| | | gap: 1.2vh; |
| | | } |
| | | |
| | | .new-customer-color { |
| | | color: #e6a23c; |
| | | text-shadow: 0 2px 4px rgba(230, 162, 60, 0.3); |
| | | .sales-statistics-container.is-fullscreen .bi-dashboard-grid { |
| | | height: calc(100vh - 5.8vh); |
| | | } |
| | | |
| | | .total-customer-color { |
| | | color: #f56c6c; |
| | | text-shadow: 0 2px 4px rgba(245, 108, 108, 0.3); |
| | | } |
| | | |
| | | .stat-unit { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-bottom: 3px; |
| | | } |
| | | |
| | | .stat-change { |
| | | font-size: 12px; |
| | | color: #67c23a; |
| | | } |
| | | |
| | | /* è¡¨æ ¼æ ·å¼ */ |
| | | :deep(.el-table) { |
| | | border-radius: 8px; |
| | | .bi-panel { |
| | | background: rgba(3, 18, 46, 0.62); |
| | | border: 0.1vh solid rgba(64, 158, 255, 0.35); |
| | | border-radius: 0.4vh; |
| | | overflow: hidden; |
| | | box-shadow: 0 0 2.2vh rgba(0, 164, 237, 0.12); |
| | | display: flex; |
| | | flex-direction: column; |
| | | position: relative; |
| | | } |
| | | |
| | | :deep(.el-table th) { |
| | | background-color: #fafafa; |
| | | .bi-panel-title { |
| | | height: 4.4vh; |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 0 1.8vh; |
| | | font-size: 1.5vh; |
| | | font-weight: 700; |
| | | color: #b8c8e0; |
| | | background: linear-gradient( |
| | | 90deg, |
| | | rgba(0, 164, 237, 0.2), |
| | | rgba(0, 164, 237, 0.04) |
| | | ); |
| | | border-bottom: 0.1vh solid rgba(64, 158, 255, 0.25); |
| | | } |
| | | |
| | | .panel-tabs { |
| | | position: absolute; |
| | | top: 0.8vh; |
| | | right: 1.2vh; |
| | | display: flex; |
| | | gap: 0.6vh; |
| | | z-index: 4; |
| | | } |
| | | |
| | | .tab-item { |
| | | font-size: 1.2vh; |
| | | color: rgba(184, 200, 224, 0.75); |
| | | padding: 0.1vh 0.5vh; |
| | | border: 0.1vh solid rgba(64, 158, 255, 0.25); |
| | | border-radius: 0.3vh; |
| | | line-height: 1.4; |
| | | } |
| | | |
| | | .tab-item.active { |
| | | color: #ffffff; |
| | | border-color: rgba(0, 164, 237, 0.65); |
| | | background: rgba(0, 164, 237, 0.22); |
| | | } |
| | | |
| | | .bi-panel-body { |
| | | flex: 1; |
| | | padding: 0.8vh 1vh; |
| | | } |
| | | |
| | | .echart-fill { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | .chart-filter-tabs { |
| | | display: flex; |
| | | gap: 0.6vh; |
| | | margin: 0 0 0.5vh 0; |
| | | } |
| | | |
| | | .cf-tab { |
| | | font-size: 1.1vh; |
| | | color: rgba(184, 200, 224, 0.68); |
| | | background: rgba(18, 56, 106, 0.65); |
| | | border: 0.1vh solid rgba(64, 158, 255, 0.25); |
| | | padding: 0.3vh 0.9vh; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .cf-tab.active { |
| | | color: #d9ecff; |
| | | background: rgba(0, 108, 208, 0.85); |
| | | border-color: rgba(64, 158, 255, 0.65); |
| | | } |
| | | |
| | | .chart-unit-row { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | font-size: 1.2vh; |
| | | color: rgba(208, 231, 255, 0.88); |
| | | margin-bottom: 0.4vh; |
| | | padding: 0 0.2vh; |
| | | } |
| | | |
| | | .dot-legend::before { |
| | | content: ""; |
| | | display: inline-block; |
| | | width: 0.8vh; |
| | | height: 0.8vh; |
| | | background: #65a0ff; |
| | | margin-right: 0.6vh; |
| | | } |
| | | |
| | | .chart-mini-title { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 0.8vh; |
| | | font-size: 1.8vh; |
| | | color: #d9ecff; |
| | | font-weight: 700; |
| | | margin: 0 0 0.8vh 0; |
| | | line-height: 1; |
| | | } |
| | | |
| | | .diamond { |
| | | width: 1vh; |
| | | height: 1vh; |
| | | background: #1e8bff; |
| | | transform: rotate(45deg); |
| | | display: inline-block; |
| | | } |
| | | |
| | | .chart-unit-single { |
| | | justify-content: flex-start; |
| | | margin-bottom: 0.2vh; |
| | | } |
| | | |
| | | .bi-panel-top-left .echart-fill, |
| | | .bi-panel-top-right .echart-fill { |
| | | height: calc(100% - 4.4vh); |
| | | } |
| | | |
| | | .bi-panel-bottom-left .echart-fill, |
| | | .bi-panel-bottom-right .echart-fill { |
| | | height: calc(100% - 2.8vh); |
| | | } |
| | | |
| | | .bi-panel-bottom-center .echart-fill { |
| | | height: calc(100% - 4.4vh); |
| | | } |
| | | |
| | | .bi-panel-top-left { |
| | | grid-column: 1; |
| | | grid-row: 1; |
| | | position: relative; |
| | | } |
| | | |
| | | .bi-panel-top-right { |
| | | grid-column: 3; |
| | | grid-row: 1; |
| | | position: relative; |
| | | } |
| | | |
| | | .bi-panel-bottom-left { |
| | | grid-column: 1; |
| | | grid-row: 2; |
| | | } |
| | | |
| | | .bi-panel-bottom-center { |
| | | grid-column: 2; |
| | | grid-row: 2; |
| | | } |
| | | |
| | | .bi-panel-bottom-right { |
| | | grid-column: 3; |
| | | grid-row: 2; |
| | | } |
| | | |
| | | /* ä¸å¿ç¯æµ®å±ï¼ç»å¯¹å®ä½å¨ç½æ ¼ä¸æ¹ï¼ */ |
| | | .center-ring { |
| | | grid-column: 2; |
| | | grid-row: 1 / span 2; |
| | | position: absolute; |
| | | background: url("@/assets/BI/imageSS@2x.png") no-repeat bottom center; |
| | | background-size: 100% 30%; |
| | | left: 25%; |
| | | top: 25%; |
| | | transform: translate(-50%, -50%); |
| | | width: 60vh; |
| | | height: 40.5vh; |
| | | z-index: 3; |
| | | pointer-events: none; |
| | | } |
| | | .center-ring-box { |
| | | position: absolute; |
| | | /* inset: 0; */ |
| | | height: 100%; |
| | | width: 100%; |
| | | /* background-color: #fff; */ |
| | | background: url("@/assets/BI/imageSStop.png") no-repeat center center; |
| | | background-size: 80% 90%; |
| | | } |
| | | |
| | | .center-ring-bg { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: contain; |
| | | filter: drop-shadow(0 0 20px rgba(0, 164, 237, 0.35)); |
| | | } |
| | | |
| | | .center-ring-content { |
| | | position: absolute; |
| | | inset: 0; |
| | | } |
| | | |
| | | .center-ring-content::before, |
| | | .center-ring-content::after { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 56%; |
| | | width: 37vh; |
| | | height: 14.6vh; |
| | | transform: translate(-50%, -50%) rotate(-18deg); |
| | | border: 0.2vh solid rgba(40, 186, 255, 0.45); |
| | | border-radius: 50%; |
| | | filter: drop-shadow(0 0 0.8vh rgba(0, 164, 237, 0.35)); |
| | | opacity: 0.7; |
| | | } |
| | | |
| | | .center-ring-content::after { |
| | | width: 36vh; |
| | | height: 15vh; |
| | | transform: translate(-50%, -50%) rotate(26deg); |
| | | border-color: rgba(80, 220, 255, 0.35); |
| | | opacity: 0.55; |
| | | } |
| | | |
| | | .center-ring-title { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | font-size: 3.6vh; |
| | | line-height: 1.05; |
| | | text-align: center; |
| | | font-weight: 900; |
| | | color: #eaf6ff; |
| | | text-shadow: 0 0 2.2vh rgba(0, 164, 237, 0.55); |
| | | z-index: 2; |
| | | } |
| | | |
| | | .center-ring-title::before { |
| | | content: ""; |
| | | position: absolute; |
| | | left: 50%; |
| | | top: 50%; |
| | | width: 15.5vh; |
| | | height: 15.5vh; |
| | | transform: translate(-50%, -50%); |
| | | background: radial-gradient( |
| | | circle, |
| | | rgba(43, 199, 255, 0.26) 0%, |
| | | rgba(8, 28, 61, 0.86) 70% |
| | | ); |
| | | border: 0.2vh solid rgba(39, 198, 255, 0.46); |
| | | border-radius: 50%; |
| | | box-shadow: 0 0 2vh rgba(0, 164, 237, 0.45), |
| | | inset 0 0 2.6vh rgba(0, 164, 237, 0.2); |
| | | z-index: -1; |
| | | } |
| | | |
| | | .center-metric { |
| | | position: absolute; |
| | | width: 15.5vh; |
| | | z-index: 3; |
| | | text-align: center; |
| | | height: 12vh; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | |
| | | .center-metric-label { |
| | | font-size: 1.2vh; |
| | | font-weight: 500; |
| | | color: rgba(234, 246, 255, 0.9); |
| | | margin-top: 0; |
| | | } |
| | | |
| | | :deep(.el-table tr:hover > td) { |
| | | background-color: #ecf5ff; |
| | | .center-metric-value { |
| | | font-size: 3.4vh; |
| | | font-weight: 800; |
| | | color: #eaf6ff; |
| | | text-shadow: 0 0 0.8vh rgba(0, 229, 255, 0.22); |
| | | line-height: 1; |
| | | } |
| | | |
| | | .data-value { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | .center-metric-unit { |
| | | margin-top: 0; |
| | | font-size: 1.2vh; |
| | | color: rgba(208, 231, 255, 0.85); |
| | | } |
| | | |
| | | /* ä¸æéæ©æ¡æ ·å¼ */ |
| | | :deep(.el-select) { |
| | | width: 100%; |
| | | .m1 { |
| | | top: 2.5vh; |
| | | left: 2.3vw; |
| | | text-align: left; |
| | | } |
| | | |
| | | :deep(.el-date-picker) { |
| | | width: 100%; |
| | | .m2 { |
| | | top: 4.1vh; |
| | | right: 4.3vw; |
| | | text-align: right; |
| | | } |
| | | |
| | | .m3 { |
| | | bottom: 7.9vh; |
| | | left: 4vh; |
| | | text-align: left; |
| | | } |
| | | |
| | | .m4 { |
| | | bottom: 7vh; |
| | | right: 5.4vh; |
| | | text-align: right; |
| | | } |
| | | |
| | | @media (max-width: 1100px) { |
| | | .bi-topbar-content { |
| | | padding: 0 1.4vh; |
| | | } |
| | | .center-ring { |
| | | left: 45.2%; |
| | | width: 33vh; |
| | | height: 24.5vh; |
| | | top: 2.4vh; |
| | | } |
| | | .center-ring-title { |
| | | top: 50%; |
| | | font-size: 2.8vh; |
| | | transform: translate(-50%, -50%); |
| | | } |
| | | .center-metric { |
| | | height: 10.5vh; |
| | | } |
| | | .m1 { |
| | | top: 5.2vh; |
| | | left: 4.2vh; |
| | | } |
| | | .m2 { |
| | | top: 5.4vh; |
| | | right: 4.2vh; |
| | | } |
| | | .m3 { |
| | | bottom: 6.2vh; |
| | | left: 4.8vh; |
| | | } |
| | | .m4 { |
| | | bottom: 6.8vh; |
| | | right: 4.4vh; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | <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> |
| | | <el-form-item label="年份:"> |
| | | <el-select v-model="searchForm.year" |
| | | placeholder="è¯·éæ©å¹´ä»½" |
| | |
| | | :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="è½èç±»å:"> |
| | |
| | | label="åä½" |
| | | width="120" |
| | | align="center" /> |
| | | <el-table-column label="æåº¦æ°æ®"> |
| | | <el-table-column label="æåº¦æ°æ®" |
| | | v-if="searchForm.timeDimension === 'month'"> |
| | | <el-table-column prop="monthlyUnitConsumption" |
| | | label="æåº¦ç´¯è®¡åè" |
| | | align="right"> |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table-column> |
| | | <el-table-column label="å¹´åº¦æ°æ®"> |
| | | <el-table-column label="å¹´åº¦æ°æ®" |
| | | v-if="searchForm.timeDimension === 'year'"> |
| | | <el-table-column prop="annualUnitConsumption" |
| | | label="年度累计åè" |
| | | align="right"> |
| | |
| | | |
| | | // æç´¢è¡¨å |
| | | const searchForm = reactive({ |
| | | timeDimension: "year", |
| | | year: new Date().getFullYear(), |
| | | month: new Date().getMonth() + 1, |
| | | energyType: "", |
| | | }); |
| | | |
| | |
| | | // æ´æ°å¾è¡¨ |
| | | const updateChart = () => { |
| | | const data = tableData.value; |
| | | const months = [ |
| | | "1æ", |
| | | "2æ", |
| | | "3æ", |
| | | "4æ", |
| | | "5æ", |
| | | "6æ", |
| | | "7æ", |
| | | "8æ", |
| | | "9æ", |
| | | "10æ", |
| | | "11æ", |
| | | "12æ", |
| | | ]; |
| | | let xAxisData = []; |
| | | let seriesDataKey = "monthlyData"; |
| | | let seriesDataMap = item => item.unitConsumption; |
| | | |
| | | // æ ¹æ®æ¶é´ç»´åº¦å夿°æ® |
| | | if (searchForm.timeDimension === "year") { |
| | | // 年度模å¼ï¼12个æ |
| | | xAxisData = [ |
| | | "1æ", |
| | | "2æ", |
| | | "3æ", |
| | | "4æ", |
| | | "5æ", |
| | | "6æ", |
| | | "7æ", |
| | | "8æ", |
| | | "9æ", |
| | | "10æ", |
| | | "11æ", |
| | | "12æ", |
| | | ]; |
| | | } 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"; |
| | | } |
| | | |
| | | // åå¤å¾è¡¨æ°æ® |
| | | const series = []; |
| | |
| | | |
| | | energyTypes.forEach(type => { |
| | | const typeData = data.find(item => item.energyType === type); |
| | | if (typeData && typeData.monthlyData) { |
| | | if (typeData && typeData[seriesDataKey]) { |
| | | series.push({ |
| | | name: type, |
| | | type: "line", |
| | | data: typeData.monthlyData.map(item => item.unitConsumption), |
| | | data: typeData[seriesDataKey].map(seriesDataMap), |
| | | smooth: true, |
| | | symbol: "circle", |
| | | symbolSize: 8, |
| | |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: months, |
| | | axisLabel: { color: "#606266" }, |
| | | data: xAxisData, |
| | | axisLabel: { |
| | | color: "#606266", |
| | | rotate: searchForm.timeDimension === "month" ? 45 : 0, |
| | | }, |
| | | axisLine: { lineStyle: { color: "#ebeef5" } }, |
| | | splitLine: { show: false }, |
| | | }, |
| | |
| | | |
| | | // éç½® |
| | | const handleReset = () => { |
| | | searchForm.timeDimension = "year"; |
| | | searchForm.year = new Date().getFullYear(); |
| | | searchForm.month = new Date().getMonth() + 1; |
| | | searchForm.energyType = ""; |
| | | handleQuery(); |
| | | }; |
| | |
| | | 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: "çµ", |
| | |
| | | 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: "è¸æ±½", |
| | |
| | | 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), |
| | | }, |
| | | ]; |
| | | |
| | |
| | | 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(); |