From 9147b2a6a242b4126eaf90998cab3000bc9f40e4 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期一, 23 三月 2026 11:18:46 +0800
Subject: [PATCH] 标准/实际成本对比分析
---
src/views/costAccounting/stdVsActCostAnalysis/index.vue | 735 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 735 insertions(+), 0 deletions(-)
diff --git a/src/views/costAccounting/stdVsActCostAnalysis/index.vue b/src/views/costAccounting/stdVsActCostAnalysis/index.vue
new file mode 100644
index 0000000..599e1d9
--- /dev/null
+++ b/src/views/costAccounting/stdVsActCostAnalysis/index.vue
@@ -0,0 +1,735 @@
+<template>
+ <div class="std-cost-page">
+ <el-card class="filter-card" shadow="never">
+ <template #header>
+ <div class="card-head">
+ <div class="card-head-left">
+ <el-icon class="card-icon ui-icon"><DataLine /></el-icon>
+ <span class="card-title">鏍囧噯/瀹為檯鎴愭湰瀵规瘮鍒嗘瀽</span>
+ <span class="subtle">宸紓 = 瀹為檯鎴愭湰 - 鏍囧噯鎴愭湰</span>
+ </div>
+ </div>
+ </template>
+
+ <div class="filter-layout">
+ <el-form :model="searchForm" :inline="true" class="filter-form">
+ <el-form-item label="鏈堜唤鑼冨洿">
+ <el-date-picker
+ v-model="searchForm.monthRange"
+ type="monthrange"
+ range-separator="鑷�"
+ start-placeholder="寮�濮嬫湀浠�"
+ end-placeholder="缁撴潫鏈堜唤"
+ value-format="YYYY-MM"
+ class="w-260"
+ @change="handleQuery"
+ />
+ </el-form-item>
+ <el-form-item label="浜у搧绫诲埆">
+ <el-select
+ v-model="searchForm.category"
+ clearable
+ filterable
+ placeholder="鍏ㄩ儴绫诲埆"
+ class="w-180"
+ @change="handleQuery"
+ >
+ <el-option
+ v-for="item in categoryOptions"
+ :key="item"
+ :label="item"
+ :value="item"
+ />
+ </el-select>
+ </el-form-item>
+ <el-form-item label="鎴愭湰绫诲瀷">
+ <el-select
+ v-model="searchForm.costType"
+ clearable
+ placeholder="鍏ㄩ儴绫诲瀷"
+ class="w-180"
+ @change="handleQuery"
+ >
+ <el-option label="鑳借�楁垚鏈�" value="鑳借�楁垚鏈�" />
+ <el-option label="鐢熶骇鎴愭湰" value="鐢熶骇鎴愭湰" />
+ </el-select>
+ </el-form-item>
+ </el-form>
+
+ <div class="filter-actions">
+ <div class="action-group">
+ <el-button class="lux-btn" type="primary" @click="handleQuery">鍒锋柊</el-button>
+ <el-button class="lux-btn" @click="handleReset">閲嶇疆</el-button>
+ </div>
+ <div class="action-group">
+ <el-dropdown trigger="click" @command="handleImportCommand">
+ <el-button class="lux-btn" type="success" plain>
+ 鏍囧噯鎴愭湰瀵煎叆
+ <el-icon class="el-icon--right"><ArrowDown /></el-icon>
+ </el-button>
+ <template #dropdown>
+ <el-dropdown-menu>
+ <el-dropdown-item command="template">涓嬭浇瀵煎叆妯℃澘</el-dropdown-item>
+ <el-dropdown-item command="upload">Excel 瀵煎叆</el-dropdown-item>
+ </el-dropdown-menu>
+ </template>
+ </el-dropdown>
+ <el-upload
+ ref="uploadRef"
+ class="hidden-upload"
+ :auto-upload="false"
+ :show-file-list="false"
+ accept=".xlsx,.xls"
+ :on-change="handleFileChange"
+ />
+ </div>
+ </div>
+ </div>
+ </el-card>
+
+ <el-card class="panel-card" shadow="never">
+ <div class="kpi-strip">
+ <div class="kpi-item kpi-std">
+ <div class="kpi-label">鏍囧噯鎴愭湰鍚堣</div>
+ <div class="kpi-value">楼{{ formatMoney(overview.standardCost) }}</div>
+ </div>
+ <div class="kpi-item kpi-act">
+ <div class="kpi-label">瀹為檯鎴愭湰鍚堣</div>
+ <div class="kpi-value">楼{{ formatMoney(overview.actualCost) }}</div>
+ </div>
+ <div class="kpi-item kpi-diff">
+ <div class="kpi-label">宸紓鍚堣</div>
+ <div class="kpi-value" :class="overview.diff >= 0 ? 'cost-value' : 'ok-value'">
+ 楼{{ formatMoney(overview.diff) }}
+ </div>
+ </div>
+ <div class="kpi-item kpi-rate">
+ <div class="kpi-label">宸紓鐜�</div>
+ <div class="kpi-value">{{ formatPercent(overview.diffRate) }}</div>
+ </div>
+ </div>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <template #header>
+ <div class="panel-head">
+ <span class="card-title">鏍囧噯/瀹為檯鎴愭湰鍙鍖栵紙鏌辩姸 + 鎶樼嚎锛�</span>
+ <span class="subtle">鏀寔鎸夋湀浠姐�佷骇鍝佺被鍒�佹垚鏈被鍨嬬瓫閫�</span>
+ </div>
+ </template>
+ <div class="chart-wrap">
+ <div ref="chartRef" class="chart-content"></div>
+ </div>
+ </el-card>
+
+ <el-card class="table-card" shadow="never">
+ <template #header>
+ <div class="panel-head">
+ <span class="card-title">瀵规瘮鏄庣粏</span>
+ <span class="subtle">鍏� {{ tableData.length }} 鏉�</span>
+ </div>
+ </template>
+ <el-table :data="pagedTableData" stripe class="lux-table">
+ <el-table-column prop="month" label="鏈堜唤" width="110" />
+ <el-table-column prop="category" label="浜у搧绫诲埆" min-width="140" />
+ <el-table-column prop="costType" label="鎴愭湰绫诲瀷" min-width="120" />
+ <el-table-column prop="standardCost" label="鏍囧噯鎴愭湰(鍏�)" align="right">
+ <template #default="scope">楼{{ formatMoney(scope.row.standardCost) }}</template>
+ </el-table-column>
+ <el-table-column prop="actualCost" label="瀹為檯鎴愭湰(鍏�)" align="right">
+ <template #default="scope">楼{{ formatMoney(scope.row.actualCost) }}</template>
+ </el-table-column>
+ <el-table-column prop="diff" label="宸紓(鍏�)" align="right">
+ <template #default="scope">
+ <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'">
+ {{ formatSignedMoney(scope.row.diff) }}
+ </span>
+ </template>
+ </el-table-column>
+ <el-table-column prop="diffRate" label="宸紓鐜�" align="right">
+ <template #default="scope">
+ <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'">
+ {{ formatPercent(scope.row.diffRate) }}
+ </span>
+ </template>
+ </el-table-column>
+ </el-table>
+ <div class="pagination-container">
+ <el-pagination
+ v-model:current-page="page.current"
+ v-model:page-size="page.size"
+ :page-sizes="[10, 20, 50, 100]"
+ :total="tableData.length"
+ layout="total, sizes, prev, pager, next, jumper"
+ @size-change="handleSizeChange"
+ @current-change="handleCurrentChange"
+ />
+ </div>
+ </el-card>
+ </div>
+</template>
+
+<script setup>
+import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
+import { ArrowDown, DataLine } from "@element-plus/icons-vue";
+import { ElMessage } from "element-plus";
+import * as echarts from "echarts";
+import * as XLSX from "xlsx";
+
+const getDefaultMonthRange = () => {
+ const end = new Date();
+ const start = new Date();
+ start.setMonth(start.getMonth() - 2);
+ return [start.toISOString().slice(0, 7), end.toISOString().slice(0, 7)];
+};
+
+const searchForm = reactive({
+ monthRange: getDefaultMonthRange(),
+ category: "",
+ costType: "",
+});
+
+const uploadRef = ref();
+const chartRef = ref(null);
+let chartInstance = null;
+
+const actualCostSource = ref([
+ { month: "2026-01", category: "鐡风爾", costType: "鑳借�楁垚鏈�", actualCost: 182000 },
+ { month: "2026-01", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", actualCost: 465000 },
+ { month: "2026-01", category: "姘存偿", costType: "鑳借�楁垚鏈�", actualCost: 138500 },
+ { month: "2026-01", category: "姘存偿", costType: "鐢熶骇鎴愭湰", actualCost: 398000 },
+ { month: "2026-02", category: "鐡风爾", costType: "鑳借�楁垚鏈�", actualCost: 191500 },
+ { month: "2026-02", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", actualCost: 472500 },
+ { month: "2026-02", category: "姘存偿", costType: "鑳借�楁垚鏈�", actualCost: 142300 },
+ { month: "2026-02", category: "姘存偿", costType: "鐢熶骇鎴愭湰", actualCost: 407000 },
+ { month: "2026-03", category: "鐮傛祮", costType: "鑳借�楁垚鏈�", actualCost: 95800 },
+ { month: "2026-03", category: "鐮傛祮", costType: "鐢熶骇鎴愭湰", actualCost: 265400 },
+ { month: "2026-03", category: "鐡风爾", costType: "鑳借�楁垚鏈�", actualCost: 189800 },
+ { month: "2026-03", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", actualCost: 469900 },
+]);
+
+const standardCostSource = ref([
+ { month: "2026-01", category: "鐡风爾", costType: "鑳借�楁垚鏈�", standardCost: 176000 },
+ { month: "2026-01", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", standardCost: 452000 },
+ { month: "2026-01", category: "姘存偿", costType: "鑳借�楁垚鏈�", standardCost: 136000 },
+ { month: "2026-01", category: "姘存偿", costType: "鐢熶骇鎴愭湰", standardCost: 392000 },
+ { month: "2026-02", category: "鐡风爾", costType: "鑳借�楁垚鏈�", standardCost: 186000 },
+ { month: "2026-02", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", standardCost: 458000 },
+ { month: "2026-02", category: "姘存偿", costType: "鑳借�楁垚鏈�", standardCost: 139000 },
+ { month: "2026-02", category: "姘存偿", costType: "鐢熶骇鎴愭湰", standardCost: 401000 },
+ { month: "2026-03", category: "鐮傛祮", costType: "鑳借�楁垚鏈�", standardCost: 93000 },
+ { month: "2026-03", category: "鐮傛祮", costType: "鐢熶骇鎴愭湰", standardCost: 259000 },
+ { month: "2026-03", category: "鐡风爾", costType: "鑳借�楁垚鏈�", standardCost: 185000 },
+ { month: "2026-03", category: "鐡风爾", costType: "鐢熶骇鎴愭湰", standardCost: 461000 },
+]);
+
+const categoryOptions = computed(() => {
+ const all = [...actualCostSource.value, ...standardCostSource.value];
+ return Array.from(new Set(all.map((item) => item.category)));
+});
+
+const inRange = (value, range) => {
+ if (!Array.isArray(range) || range.length !== 2 || !range[0] || !range[1]) return true;
+ return value >= range[0] && value <= range[1];
+};
+
+const mergedRows = computed(() => {
+ const key = (item) => `${item.month}__${item.category}__${item.costType}`;
+ const stdMap = new Map(standardCostSource.value.map((item) => [key(item), item]));
+ const actMap = new Map(actualCostSource.value.map((item) => [key(item), item]));
+ const keySet = new Set([...stdMap.keys(), ...actMap.keys()]);
+ const rows = [];
+
+ for (const k of keySet) {
+ const std = stdMap.get(k);
+ const act = actMap.get(k);
+ const month = std?.month || act?.month || "";
+ const category = std?.category || act?.category || "";
+ const costType = std?.costType || act?.costType || "";
+ const standardCost = Number(std?.standardCost || 0);
+ const actualCost = Number(act?.actualCost || 0);
+ const diff = actualCost - standardCost;
+ const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
+
+ rows.push({ month, category, costType, standardCost, actualCost, diff, diffRate });
+ }
+
+ return rows.sort((a, b) => {
+ if (a.month !== b.month) return a.month > b.month ? 1 : -1;
+ if (a.category !== b.category) return a.category.localeCompare(b.category, "zh-Hans-CN");
+ return a.costType.localeCompare(b.costType, "zh-Hans-CN");
+ });
+});
+
+const tableData = computed(() =>
+ mergedRows.value.filter((item) => {
+ const hitMonth = inRange(item.month, searchForm.monthRange);
+ const hitCategory = !searchForm.category || item.category === searchForm.category;
+ const hitCostType = !searchForm.costType || item.costType === searchForm.costType;
+ return hitMonth && hitCategory && hitCostType;
+ })
+);
+
+const page = reactive({
+ current: 1,
+ size: 10,
+});
+
+const pagedTableData = computed(() => {
+ const start = (page.current - 1) * page.size;
+ return tableData.value.slice(start, start + page.size);
+});
+
+const overview = computed(() => {
+ const standardCost = tableData.value.reduce((sum, item) => sum + item.standardCost, 0);
+ const actualCost = tableData.value.reduce((sum, item) => sum + item.actualCost, 0);
+ const diff = actualCost - standardCost;
+ const diffRate = standardCost === 0 ? 0 : (diff / standardCost) * 100;
+ return { standardCost, actualCost, diff, diffRate };
+});
+
+const getChartData = () => {
+ const xAxis = tableData.value.map(
+ (item) => `${item.month}\n${item.category}-${item.costType.replace("鎴愭湰", "")}`
+ );
+ const standard = tableData.value.map((item) => item.standardCost);
+ const actual = tableData.value.map((item) => item.actualCost);
+ const diffRate = tableData.value.map((item) => Number(item.diffRate.toFixed(2)));
+ return { xAxis, standard, actual, diffRate };
+};
+
+const updateChart = () => {
+ if (!chartInstance) return;
+ const { xAxis, standard, actual, diffRate } = getChartData();
+ chartInstance.setOption({
+ tooltip: {
+ trigger: "axis",
+ axisPointer: { type: "shadow" },
+ formatter: (params) => {
+ const row = tableData.value[params[0]?.dataIndex] || {};
+ return [
+ `${row.month || ""} ${row.category || ""} ${row.costType || ""}`,
+ `鏍囧噯鎴愭湰锛毬�${formatMoney(row.standardCost || 0)}`,
+ `瀹為檯鎴愭湰锛毬�${formatMoney(row.actualCost || 0)}`,
+ `宸紓锛�${formatSignedMoney(row.diff || 0)}`,
+ `宸紓鐜囷細${formatPercent(row.diffRate || 0)}`,
+ ].join("<br/>");
+ },
+ },
+ legend: { data: ["鏍囧噯鎴愭湰", "瀹為檯鎴愭湰", "宸紓鐜�"] },
+ grid: { left: "4%", right: "4%", top: "16%", bottom: "16%", containLabel: true },
+ xAxis: {
+ type: "category",
+ data: xAxis,
+ axisLabel: { color: "rgba(15, 23, 42, 0.62)" },
+ axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } },
+ },
+ yAxis: [
+ {
+ type: "value",
+ name: "鎴愭湰(鍏�)",
+ axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
+ splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } },
+ },
+ {
+ type: "value",
+ name: "宸紓鐜�(%)",
+ axisLabel: { color: "rgba(15, 23, 42, 0.58)" },
+ splitLine: { show: false },
+ },
+ ],
+ series: [
+ {
+ name: "鏍囧噯鎴愭湰",
+ type: "bar",
+ barMaxWidth: 24,
+ data: standard,
+ itemStyle: { color: "#5b8cff", borderRadius: [4, 4, 0, 0] },
+ },
+ {
+ name: "瀹為檯鎴愭湰",
+ type: "bar",
+ barMaxWidth: 24,
+ data: actual,
+ itemStyle: { color: "#f59e0b", borderRadius: [4, 4, 0, 0] },
+ },
+ {
+ name: "宸紓鐜�",
+ type: "line",
+ yAxisIndex: 1,
+ smooth: true,
+ data: diffRate,
+ itemStyle: { color: "#ef4444" },
+ lineStyle: { width: 2 },
+ },
+ ],
+ });
+};
+
+const normalizeCostType = (value) => {
+ const text = String(value || "").trim();
+ if (!text) return "";
+ if (text.includes("鑳借��")) return "鑳借�楁垚鏈�";
+ if (text.includes("鐢熶骇")) return "鐢熶骇鎴愭湰";
+ return text;
+};
+
+const parseImportedRows = (rows) => {
+ const normalized = rows
+ .map((item) => {
+ const month = String(item["鏈堜唤"] || item.month || "").slice(0, 7);
+ const category = String(item["浜у搧绫诲埆"] || item.category || "").trim();
+ const costType = normalizeCostType(item["鎴愭湰绫诲瀷"] || item.costType);
+ const standardCost = Number(item["鏍囧噯鎴愭湰"] ?? item.standardCost ?? 0);
+ return { month, category, costType, standardCost };
+ })
+ .filter((item) => item.month && item.category && item.costType && Number.isFinite(item.standardCost));
+
+ return normalized;
+};
+
+const replaceStandardSourceByImport = (importRows) => {
+ const map = new Map();
+ for (const item of importRows) {
+ const k = `${item.month}__${item.category}__${item.costType}`;
+ map.set(k, item);
+ }
+ standardCostSource.value = Array.from(map.values());
+};
+
+const handleFileChange = async (uploadFile) => {
+ try {
+ const file = uploadFile.raw;
+ if (!file) return;
+ const data = await file.arrayBuffer();
+ const workbook = XLSX.read(data, { type: "array" });
+ const sheetName = workbook.SheetNames[0];
+ const sheet = workbook.Sheets[sheetName];
+ const rows = XLSX.utils.sheet_to_json(sheet, { defval: "" });
+ const parsed = parseImportedRows(rows);
+ if (!parsed.length) {
+ ElMessage.warning("瀵煎叆澶辫触锛氭ā鏉垮唴瀹逛负绌烘垨瀛楁涓嶅尮閰�");
+ return;
+ }
+ replaceStandardSourceByImport(parsed);
+ ElMessage.success(`瀵煎叆鎴愬姛锛�${parsed.length} 鏉℃爣鍑嗘垚鏈褰昤);
+ handleQuery();
+ } catch (error) {
+ console.error(error);
+ ElMessage.error("瀵煎叆澶辫触锛岃妫�鏌� Excel 鏍煎紡");
+ } finally {
+ uploadRef.value?.clearFiles?.();
+ }
+};
+
+const openUploadSelector = () => {
+ const input = uploadRef.value?.$el?.querySelector?.("input[type='file']");
+ if (!input) {
+ ElMessage.warning("涓婁紶缁勪欢灏氭湭灏辩华锛岃绋嶅悗閲嶈瘯");
+ return;
+ }
+ input.click();
+};
+
+const handleImportCommand = (command) => {
+ if (command === "template") {
+ downloadTemplate();
+ return;
+ }
+ if (command === "upload") {
+ openUploadSelector();
+ }
+};
+
+const downloadTemplate = () => {
+ const sample = [
+ { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "鐡风爾", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 185000 },
+ { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "鐡风爾", 鎴愭湰绫诲瀷: "鏍囧噯鐢熶骇鎴愭湰", 鏍囧噯鎴愭湰: 461000 },
+ { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "姘存偿", 鎴愭湰绫诲瀷: "鏍囧噯鑳借�楁垚鏈�", 鏍囧噯鎴愭湰: 140000 },
+ { 鏈堜唤: "2026-03", 浜у搧绫诲埆: "姘存偿", 鎴愭湰绫诲瀷: "鏍囧噯鐢熶骇鎴愭湰", 鏍囧噯鎴愭湰: 405000 },
+ ];
+ const ws = XLSX.utils.json_to_sheet(sample);
+ const wb = XLSX.utils.book_new();
+ XLSX.utils.book_append_sheet(wb, ws, "鏍囧噯鎴愭湰妯℃澘");
+ XLSX.writeFile(wb, "鏍囧噯鎴愭湰鎸夋湀瀵煎叆妯℃澘.xlsx");
+ ElMessage.success("妯℃澘宸蹭笅杞�");
+};
+
+const handleQuery = () => {
+ updateChart();
+};
+
+const handleReset = () => {
+ searchForm.monthRange = getDefaultMonthRange();
+ searchForm.category = "";
+ searchForm.costType = "";
+ page.current = 1;
+ handleQuery();
+};
+
+const handleSizeChange = (val) => {
+ page.size = val;
+ page.current = 1;
+};
+
+const handleCurrentChange = (val) => {
+ page.current = val;
+};
+
+const formatMoney = (v) => {
+ const n = Number.parseFloat(v);
+ const value = Number.isFinite(n) ? n : 0;
+ return value.toLocaleString("zh-CN", {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ });
+};
+
+const formatSignedMoney = (v) => {
+ const n = Number.parseFloat(v);
+ const value = Number.isFinite(n) ? n : 0;
+ const sign = value >= 0 ? "+" : "";
+ return `${sign}楼${formatMoney(value)}`;
+};
+
+const formatPercent = (v) => {
+ const n = Number.parseFloat(v);
+ const value = Number.isFinite(n) ? n : 0;
+ const sign = value >= 0 ? "+" : "";
+ return `${sign}${value.toFixed(2)}%`;
+};
+
+const handleResize = () => {
+ chartInstance?.resize?.();
+};
+
+onMounted(() => {
+ nextTick(() => {
+ if (chartRef.value && !chartInstance) {
+ chartInstance = echarts.init(chartRef.value);
+ }
+ updateChart();
+ });
+ window.addEventListener("resize", handleResize);
+});
+
+onUnmounted(() => {
+ window.removeEventListener("resize", handleResize);
+ chartInstance?.dispose?.();
+ chartInstance = null;
+});
+
+watch(tableData, () => {
+ const maxPage = Math.max(1, Math.ceil(tableData.value.length / page.size));
+ if (page.current > maxPage) page.current = maxPage;
+ nextTick(updateChart);
+});
+</script>
+
+<style scoped lang="scss">
+.std-cost-page {
+ --lux-bg: #f6f7fb;
+ --lux-card: rgba(255, 255, 255, 0.86);
+ --lux-border: rgba(15, 23, 42, 0.08);
+ --lux-text: rgba(15, 23, 42, 0.92);
+ --lux-subtle: rgba(15, 23, 42, 0.58);
+ --lux-primary: #2f6fed;
+ --lux-success: #16a34a;
+ --lux-warning: #f59e0b;
+ --lux-danger: #ef4444;
+ --lux-shadow-soft: 0 10px 28px rgba(15, 23, 42, 0.06);
+ --lux-radius: 14px;
+
+ padding: 18px 22px 24px;
+ background: radial-gradient(
+ 1200px 420px at 20% 0%,
+ rgba(47, 111, 237, 0.1),
+ transparent 55%
+ ),
+ linear-gradient(180deg, var(--lux-bg) 0%, #ffffff 58%);
+}
+
+.filter-card,
+.panel-card,
+.table-card {
+ border-radius: var(--lux-radius);
+ border-color: var(--lux-border);
+ background: var(--lux-card);
+ box-shadow: var(--lux-shadow-soft);
+}
+
+.filter-card,
+.panel-card,
+.table-card {
+ margin-bottom: 14px;
+}
+
+.filter-layout {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.filter-form {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px 14px;
+ align-items: center;
+}
+
+.filter-form :deep(.el-form-item) {
+ margin: 0;
+}
+
+.filter-actions {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 10px 14px;
+ padding-top: 10px;
+ border-top: 1px dashed rgba(15, 23, 42, 0.1);
+}
+
+.action-group {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.filter-actions :deep(.el-upload) {
+ display: inline-flex;
+}
+
+.hidden-upload {
+ width: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+.card-head,
+.panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.card-head-left {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.card-icon {
+ color: var(--lux-primary);
+}
+
+.card-title {
+ font-weight: 760;
+ color: var(--lux-text);
+}
+
+.subtle {
+ color: var(--lux-subtle);
+ font-size: 12px;
+}
+
+.kpi-strip {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.kpi-item {
+ padding: 12px 14px;
+ border-radius: 12px;
+ border: 1px solid rgba(15, 23, 42, 0.08);
+}
+
+.kpi-std {
+ background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-act {
+ background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-diff {
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-rate {
+ background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86));
+}
+
+.kpi-label {
+ font-size: 12px;
+ color: var(--lux-subtle);
+}
+
+.kpi-value {
+ margin-top: 6px;
+ font-size: 22px;
+ font-weight: 780;
+ color: var(--lux-text);
+}
+
+.cost-value {
+ color: var(--lux-danger);
+ font-weight: 700;
+}
+
+.ok-value {
+ color: var(--lux-success);
+ font-weight: 700;
+}
+
+.chart-wrap {
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+.chart-content {
+ height: 360px;
+}
+
+.pagination-container {
+ display: flex;
+ justify-content: flex-end;
+ padding-top: 12px;
+}
+
+.w-260 {
+ width: 260px;
+}
+
+.w-180 {
+ width: 180px;
+}
+
+::deep(.lux-table) {
+ border-radius: 12px;
+ overflow: hidden;
+}
+
+::deep(.lux-table th.el-table__cell) {
+ background: rgba(15, 23, 42, 0.03);
+}
+
+::deep(.lux-table .el-table__row:hover > td.el-table__cell) {
+ background-color: rgba(47, 111, 237, 0.06) !important;
+}
+
+@media (max-width: 1100px) {
+ .kpi-strip {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .filter-actions {
+ justify-content: flex-start;
+ padding-top: 8px;
+ }
+}
+</style>
\ No newline at end of file
--
Gitblit v1.9.3