| | |
| | | <template> |
| | | <div class="std-cost-page"> |
| | | <el-card class="filter-card" shadow="never"> |
| | | <div class="page-bg" aria-hidden="true"> |
| | | <div class="bg-mesh" /> |
| | | <div class="bg-orb bg-orb--a" /> |
| | | <div class="bg-orb bg-orb--b" /> |
| | | <div class="bg-orb bg-orb--c" /> |
| | | <div class="bg-grid" /> |
| | | </div> |
| | | |
| | | <div class="page-inner"> |
| | | <el-card class="filter-card glass-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 class="title-badge"> |
| | | <el-icon class="card-icon ui-icon"><DataLine /></el-icon> |
| | | </div> |
| | | <div class="title-block"> |
| | | <div class="title-row"> |
| | | <span class="card-title shimmer-text">标准/实际成本对比分析</span> |
| | | <span class="live-pill">实时分析</span> |
| | | </div> |
| | | <span class="subtle">差异 = 实际成本 − 标准成本</span> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-card class="panel-card" shadow="never"> |
| | | <el-card class="panel-card glass-card kpi-card" shadow="never"> |
| | | <div class="kpi-strip"> |
| | | <div class="kpi-item kpi-std"> |
| | | <div class="kpi-label">标准成本合计</div> |
| | | <div class="kpi-top"> |
| | | <el-icon class="kpi-ico"><Histogram /></el-icon> |
| | | <div class="kpi-label">标准成本合计</div> |
| | | </div> |
| | | <div class="kpi-value">¥{{ formatMoney(overview.standardCost) }}</div> |
| | | <div class="kpi-glow" /> |
| | | </div> |
| | | <div class="kpi-item kpi-act"> |
| | | <div class="kpi-label">实际成本合计</div> |
| | | <div class="kpi-top"> |
| | | <el-icon class="kpi-ico"><TrendCharts /></el-icon> |
| | | <div class="kpi-label">实际成本合计</div> |
| | | </div> |
| | | <div class="kpi-value">¥{{ formatMoney(overview.actualCost) }}</div> |
| | | <div class="kpi-glow" /> |
| | | </div> |
| | | <div class="kpi-item kpi-diff"> |
| | | <div class="kpi-label">差异合计</div> |
| | | <div class="kpi-top"> |
| | | <el-icon class="kpi-ico"><Switch /></el-icon> |
| | | <div class="kpi-label">差异合计</div> |
| | | </div> |
| | | <div class="kpi-value" :class="overview.diff >= 0 ? 'cost-value' : 'ok-value'"> |
| | | ¥{{ formatMoney(overview.diff) }} |
| | | </div> |
| | | <div class="kpi-glow" /> |
| | | </div> |
| | | <div class="kpi-item kpi-rate"> |
| | | <div class="kpi-label">差异率</div> |
| | | <div class="kpi-top"> |
| | | <el-icon class="kpi-ico"><PieChart /></el-icon> |
| | | <div class="kpi-label">差异率</div> |
| | | </div> |
| | | <div class="kpi-value">{{ formatPercent(overview.diffRate) }}</div> |
| | | <div class="kpi-glow" /> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <el-card class="table-card" shadow="never"> |
| | | <el-card class="table-card glass-card chart-section" shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">标准/实际成本可视化(柱状 + 折线)</span> |
| | | <div class="panel-head-main"> |
| | | <span class="panel-accent" /> |
| | | <div> |
| | | <span class="card-title">标准/实际成本可视化</span> |
| | | <span class="chart-tag">柱状 · 折线</span> |
| | | </div> |
| | | </div> |
| | | <span class="subtle">支持按月份、产品类别、成本类型筛选</span> |
| | | </div> |
| | | </template> |
| | | <div class="chart-wrap"> |
| | | <div class="chart-tools chart-tools-inline" @click.stop> |
| | | <button class="chart-tool" type="button" @click="openLargeChart">查看大图</button> |
| | | <button class="chart-tool" type="button" @click="downloadChartImage">下载图表</button> |
| | | <button class="chart-tool chart-tool--primary" type="button" @click="openLargeChart"> |
| | | <el-icon><ZoomIn /></el-icon> |
| | | 查看大图 |
| | | </button> |
| | | <button class="chart-tool" type="button" @click="downloadChartImage"> |
| | | <el-icon><Download /></el-icon> |
| | | 下载图表 |
| | | </button> |
| | | </div> |
| | | <div ref="chartRef" class="chart-content"></div> |
| | | </div> |
| | |
| | | title="标准/实际成本对比大图" |
| | | width="88%" |
| | | top="6vh" |
| | | append-to-body |
| | | destroy-on-close |
| | | @opened="initLargeChart" |
| | | @closed="disposeLargeChart" |
| | |
| | | <div ref="largeChartRef" class="large-chart-content"></div> |
| | | </el-dialog> |
| | | |
| | | <el-card class="table-card" shadow="never"> |
| | | <el-card class="table-card glass-card" shadow="never"> |
| | | <template #header> |
| | | <div class="panel-head"> |
| | | <span class="card-title">对比明细</span> |
| | | <span class="subtle">共 {{ tableData.length }} 条</span> |
| | | <div class="panel-head-main"> |
| | | <span class="panel-accent panel-accent--emerald" /> |
| | | <span class="card-title">对比明细</span> |
| | | </div> |
| | | <span class="count-chip">共 {{ tableData.length }} 条</span> |
| | | </div> |
| | | </template> |
| | | <el-table :data="pagedTableData" stripe class="lux-table"> |
| | | <el-table :data="pagedTableData" stripe class="lux-table" @sort-change="handleSortChange"> |
| | | <el-table-column prop="month" label="月份" width="110" /> |
| | | <el-table-column prop="category" label="产品类别" min-width="140" /> |
| | | <el-table-column prop="costType" label="成本类型" min-width="120" /> |
| | | <el-table-column prop="standardCost" label="标准成本(元)" align="right"> |
| | | <el-table-column prop="standardCost" label="标准成本(元)" sortable="custom" align="right"> |
| | | <template #default="scope">¥{{ formatMoney(scope.row.standardCost) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="actualCost" label="实际成本(元)" align="right"> |
| | | <el-table-column prop="actualCost" label="实际成本(元)" sortable="custom" align="right"> |
| | | <template #default="scope">¥{{ formatMoney(scope.row.actualCost) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="diff" label="差异(元)" align="right"> |
| | | <el-table-column prop="diff" label="差异(元)" sortable="custom" align="right"> |
| | | <template #default="scope"> |
| | | <span :class="scope.row.diff >= 0 ? 'cost-value' : 'ok-value'"> |
| | | {{ formatSignedMoney(scope.row.diff) }} |
| | | </span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="diffRate" label="差异率" align="right"> |
| | | <el-table-column prop="diffRate" label="差异率" sortable="custom" align="right"> |
| | | <template #default="scope"> |
| | | <span :class="scope.row.diffRate >= 0 ? 'cost-value' : 'ok-value'"> |
| | | {{ formatPercent(scope.row.diffRate) }} |
| | |
| | | /> |
| | | </div> |
| | | </el-card> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue"; |
| | | import { ArrowDown, DataLine } from "@element-plus/icons-vue"; |
| | | import { |
| | | ArrowDown, |
| | | DataLine, |
| | | Download, |
| | | Histogram, |
| | | PieChart, |
| | | Switch, |
| | | TrendCharts, |
| | | ZoomIn, |
| | | } from "@element-plus/icons-vue"; |
| | | import { ElMessage } from "element-plus"; |
| | | import * as echarts from "echarts"; |
| | | import * as XLSX from "xlsx"; |
| | |
| | | const largeChartVisible = ref(false); |
| | | const currentChartOption = ref(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 actualCostSource = ref([]); |
| | | const standardCostSource = ref([]); |
| | | |
| | | 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 fakeMonths = ["2026-01", "2026-02", "2026-03"]; |
| | | const fakeCategories = [ |
| | | "粉煤灰", |
| | | "石灰", |
| | | "水泥", |
| | | "铝粉膏", |
| | | "脱模剂", |
| | | "石膏", |
| | | "打包带", |
| | | "防腐剂(板材用)", |
| | | "氧化镁(板材用)", |
| | | "冷挤丝(板材用)", |
| | | "卡扣(板材用)", |
| | | "材料小计", |
| | | "水", |
| | | "电", |
| | | "蒸汽", |
| | | ]; |
| | | |
| | | const fakeCostType = (category) => (["水", "电", "蒸汽"].includes(category) ? "能耗成本" : "生产成本"); |
| | | |
| | | // 每个类别的标准成本基准值(仅用于假数据) |
| | | const baseStandardCostByCategory = { |
| | | 粉煤灰: 98000, |
| | | 石灰: 52000, |
| | | 水泥: 175000, |
| | | 铝粉膏: 32000, |
| | | 脱模剂: 21000, |
| | | 石膏: 41000, |
| | | 打包带: 14500, |
| | | "防腐剂(板材用)": 12500, |
| | | "氧化镁(板材用)": 22000, |
| | | "冷挤丝(板材用)": 9800, |
| | | "卡扣(板材用)": 8600, |
| | | 材料小计: 420000, |
| | | 水: 6800, |
| | | 电: 26000, |
| | | 蒸汽: 52000, |
| | | }; |
| | | |
| | | // 月份波动系数(让图表看起来更“真实”一些) |
| | | const monthFactorByMonth = { |
| | | "2026-01": 1.0, |
| | | "2026-02": 1.06, |
| | | "2026-03": 0.97, |
| | | }; |
| | | |
| | | // 实际成本相对标准成本的偏移比例(用于测试正负差异展示) |
| | | const diffRatioByCategory = { |
| | | 粉煤灰: 0.05, |
| | | 石灰: -0.01, |
| | | 水泥: 0.03, |
| | | 铝粉膏: 0.0, |
| | | 脱模剂: -0.04, |
| | | 石膏: 0.02, |
| | | 打包带: -0.03, |
| | | "防腐剂(板材用)": 0.06, |
| | | "氧化镁(板材用)": -0.02, |
| | | "冷挤丝(板材用)": 0.01, |
| | | "卡扣(板材用)": -0.05, |
| | | 材料小计: 0.02, |
| | | 水: -0.01, |
| | | 电: 0.04, |
| | | 蒸汽: -0.03, |
| | | }; |
| | | |
| | | const buildFakeSources = () => { |
| | | const stdRows = []; |
| | | const actRows = []; |
| | | |
| | | for (const month of fakeMonths) { |
| | | const monthFactor = monthFactorByMonth[month] ?? 1; |
| | | const monthAdj = month === "2026-02" ? 0.005 : month === "2026-03" ? -0.006 : 0; |
| | | |
| | | for (const category of fakeCategories) { |
| | | const costType = fakeCostType(category); |
| | | const base = baseStandardCostByCategory[category] ?? 0; |
| | | const standardCost = Math.round(base * monthFactor); |
| | | const diffRatio = (diffRatioByCategory[category] ?? 0) + monthAdj; |
| | | const actualCost = Math.round(standardCost * (1 + diffRatio)); |
| | | |
| | | stdRows.push({ month, category, costType, standardCost }); |
| | | actRows.push({ month, category, costType, actualCost }); |
| | | } |
| | | } |
| | | |
| | | standardCostSource.value = stdRows; |
| | | actualCostSource.value = actRows; |
| | | }; |
| | | |
| | | buildFakeSources(); |
| | | |
| | | const categoryOptions = computed(() => { |
| | | const all = [...actualCostSource.value, ...standardCostSource.value]; |
| | |
| | | size: 10, |
| | | }); |
| | | |
| | | /** sortable="custom" 需在 sort-change 里自行排序,再分页 */ |
| | | const tableSort = reactive({ |
| | | prop: "", |
| | | order: "", |
| | | }); |
| | | |
| | | const handleSortChange = ({ prop, order }) => { |
| | | tableSort.prop = prop || ""; |
| | | tableSort.order = order || ""; |
| | | page.current = 1; |
| | | }; |
| | | |
| | | const sortedTableData = computed(() => { |
| | | const rows = [...tableData.value]; |
| | | if (!tableSort.prop || !tableSort.order) return rows; |
| | | const dir = tableSort.order === "ascending" ? 1 : -1; |
| | | const key = tableSort.prop; |
| | | rows.sort((a, b) => { |
| | | const na = Number(a[key]); |
| | | const nb = Number(b[key]); |
| | | const va = Number.isFinite(na) ? na : 0; |
| | | const vb = Number.isFinite(nb) ? nb : 0; |
| | | if (va === vb) return 0; |
| | | return va < vb ? -dir : dir; |
| | | }); |
| | | return rows; |
| | | }); |
| | | |
| | | const pagedTableData = computed(() => { |
| | | const start = (page.current - 1) * page.size; |
| | | return tableData.value.slice(start, start + page.size); |
| | | return sortedTableData.value.slice(start, start + page.size); |
| | | }); |
| | | |
| | | const overview = computed(() => { |
| | |
| | | const buildChartOption = () => { |
| | | const { xAxis, standard, actual, diffRate } = getChartData(); |
| | | return { |
| | | animation: true, |
| | | animationDuration: 920, |
| | | animationEasing: "cubicOut", |
| | | textStyle: { fontFamily: "inherit" }, |
| | | tooltip: { |
| | | trigger: "axis", |
| | | axisPointer: { type: "shadow" }, |
| | | axisPointer: { |
| | | type: "cross", |
| | | crossStyle: { color: "rgba(47, 111, 237, 0.35)" }, |
| | | lineStyle: { type: "dashed" }, |
| | | }, |
| | | backgroundColor: "rgba(255, 255, 255, 0.94)", |
| | | borderColor: "rgba(47, 111, 237, 0.22)", |
| | | borderWidth: 1, |
| | | padding: [12, 14], |
| | | textStyle: { color: "rgba(15, 23, 42, 0.88)" }, |
| | | extraCssText: "box-shadow: 0 12px 40px rgba(15, 23, 42, 0.12); border-radius: 12px;", |
| | | formatter: (params) => { |
| | | const row = tableData.value[params[0]?.dataIndex] || {}; |
| | | return [ |
| | |
| | | ].join("<br/>"); |
| | | }, |
| | | }, |
| | | legend: { data: ["标准成本", "实际成本", "差异率"] }, |
| | | grid: { left: "4%", right: "4%", top: "16%", bottom: "16%", containLabel: true }, |
| | | legend: { |
| | | data: ["标准成本", "实际成本", "差异率"], |
| | | top: 6, |
| | | itemGap: 18, |
| | | textStyle: { color: "rgba(15, 23, 42, 0.72)" }, |
| | | }, |
| | | grid: { left: "3%", right: "3%", top: "18%", bottom: "14%", containLabel: true }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: xAxis, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.62)" }, |
| | | axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.08)" } }, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.62)", fontSize: 11 }, |
| | | axisLine: { lineStyle: { color: "rgba(15, 23, 42, 0.1)" } }, |
| | | axisTick: { show: false }, |
| | | }, |
| | | yAxis: [ |
| | | { |
| | | type: "value", |
| | | name: "成本(元)", |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.58)" }, |
| | | splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)" } }, |
| | | nameTextStyle: { color: "rgba(15, 23, 42, 0.5)", fontSize: 11 }, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.55)" }, |
| | | splitLine: { lineStyle: { color: "rgba(15, 23, 42, 0.06)", type: "dashed" } }, |
| | | }, |
| | | { |
| | | type: "value", |
| | | name: "差异率(%)", |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.58)" }, |
| | | nameTextStyle: { color: "rgba(15, 23, 42, 0.5)", fontSize: 11 }, |
| | | axisLabel: { color: "rgba(15, 23, 42, 0.55)" }, |
| | | splitLine: { show: false }, |
| | | }, |
| | | ], |
| | |
| | | { |
| | | name: "标准成本", |
| | | type: "bar", |
| | | barMaxWidth: 24, |
| | | barMaxWidth: 26, |
| | | data: standard, |
| | | itemStyle: { color: "#5b8cff", borderRadius: [4, 4, 0, 0] }, |
| | | itemStyle: { |
| | | borderRadius: [6, 6, 0, 0], |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#8eb4ff" }, |
| | | { offset: 1, color: "#3d74f5" }, |
| | | ]), |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 12, |
| | | shadowColor: "rgba(61, 116, 245, 0.45)", |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | name: "实际成本", |
| | | type: "bar", |
| | | barMaxWidth: 24, |
| | | barMaxWidth: 26, |
| | | data: actual, |
| | | itemStyle: { color: "#f59e0b", borderRadius: [4, 4, 0, 0] }, |
| | | itemStyle: { |
| | | borderRadius: [6, 6, 0, 0], |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "#fcd34d" }, |
| | | { offset: 1, color: "#ea580c" }, |
| | | ]), |
| | | }, |
| | | emphasis: { |
| | | itemStyle: { |
| | | shadowBlur: 12, |
| | | shadowColor: "rgba(234, 88, 12, 0.4)", |
| | | }, |
| | | }, |
| | | }, |
| | | { |
| | | name: "差异率", |
| | | type: "line", |
| | | yAxisIndex: 1, |
| | | smooth: true, |
| | | symbol: "circle", |
| | | symbolSize: 7, |
| | | showSymbol: true, |
| | | data: diffRate, |
| | | itemStyle: { color: "#ef4444" }, |
| | | lineStyle: { width: 2 }, |
| | | lineStyle: { width: 3, shadowBlur: 8, shadowColor: "rgba(239, 68, 68, 0.35)" }, |
| | | itemStyle: { |
| | | color: "#ef4444", |
| | | borderColor: "#fff", |
| | | borderWidth: 2, |
| | | }, |
| | | areaStyle: { |
| | | color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ |
| | | { offset: 0, color: "rgba(239, 68, 68, 0.28)" }, |
| | | { offset: 1, color: "rgba(239, 68, 68, 0.02)" }, |
| | | ]), |
| | | }, |
| | | }, |
| | | ], |
| | | }; |
| | |
| | | |
| | | const downloadTemplate = () => { |
| | | const sample = [ |
| | | { 月份: "2026-03", 产品类别: "瓷砖", 成本类型: "标准能耗成本", 标准成本: 185000 }, |
| | | { 月份: "2026-03", 产品类别: "瓷砖", 成本类型: "标准生产成本", 标准成本: 461000 }, |
| | | { 月份: "2026-03", 产品类别: "水泥", 成本类型: "标准能耗成本", 标准成本: 140000 }, |
| | | { 月份: "2026-03", 产品类别: "水泥", 成本类型: "标准生产成本", 标准成本: 405000 }, |
| | | { 月份: "2026-03", 产品类别: "粉煤灰", 成本类型: "标准生产成本", 标准成本: 98000 }, |
| | | { 月份: "2026-03", 产品类别: "水泥", 成本类型: "标准生产成本", 标准成本: 175000 }, |
| | | { 月份: "2026-03", 产品类别: "电", 成本类型: "标准能耗成本", 标准成本: 26000 }, |
| | | { 月份: "2026-03", 产品类别: "蒸汽", 成本类型: "标准能耗成本", 标准成本: 52000 }, |
| | | { 月份: "2026-03", 产品类别: "水", 成本类型: "标准能耗成本", 标准成本: 6800 }, |
| | | ]; |
| | | const ws = XLSX.utils.json_to_sheet(sample); |
| | | const wb = XLSX.utils.book_new(); |
| | |
| | | searchForm.monthRange = getDefaultMonthRange(); |
| | | searchForm.category = ""; |
| | | searchForm.costType = ""; |
| | | tableSort.prop = ""; |
| | | tableSort.order = ""; |
| | | page.current = 1; |
| | | handleQuery(); |
| | | }; |
| | |
| | | } else { |
| | | updateChart(); |
| | | } |
| | | // 弹窗出现后容器尺寸会变化,强制 resize 防止 canvas 溢出遮挡表头/关闭按钮 |
| | | largeChartInstance?.resize?.(); |
| | | }); |
| | | }; |
| | | |
| | |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | @keyframes mesh-shift { |
| | | 0%, |
| | | 100% { |
| | | opacity: 1; |
| | | transform: scale(1) translate(0, 0); |
| | | } |
| | | 50% { |
| | | opacity: 0.85; |
| | | transform: scale(1.02) translate(-1%, 1%); |
| | | } |
| | | } |
| | | |
| | | @keyframes orb-float { |
| | | 0%, |
| | | 100% { |
| | | transform: translate(0, 0) scale(1); |
| | | } |
| | | 33% { |
| | | transform: translate(12px, -18px) scale(1.05); |
| | | } |
| | | 66% { |
| | | transform: translate(-8px, 10px) scale(0.98); |
| | | } |
| | | } |
| | | |
| | | @keyframes shimmer { |
| | | 0% { |
| | | background-position: 200% center; |
| | | } |
| | | 100% { |
| | | background-position: -200% center; |
| | | } |
| | | } |
| | | |
| | | .std-cost-page { |
| | | --lux-bg: #f6f7fb; |
| | | --lux-card: rgba(255, 255, 255, 0.86); |
| | | --lux-border: rgba(15, 23, 42, 0.08); |
| | | --lux-bg: #eef1f8; |
| | | --lux-card: rgba(255, 255, 255, 0.72); |
| | | --lux-border: rgba(15, 23, 42, 0.1); |
| | | --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; |
| | | --lux-shadow-soft: 0 12px 40px rgba(15, 23, 42, 0.08); |
| | | --lux-shadow-lift: 0 20px 50px rgba(47, 111, 237, 0.12); |
| | | --lux-radius: 16px; |
| | | |
| | | 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%); |
| | | position: relative; |
| | | min-height: 100%; |
| | | padding: 20px 22px 28px; |
| | | overflow: hidden; |
| | | background: linear-gradient(165deg, #e8ecf7 0%, #f4f6fb 42%, #fafbfd 100%); |
| | | } |
| | | |
| | | .page-bg { |
| | | pointer-events: none; |
| | | position: absolute; |
| | | inset: 0; |
| | | z-index: 0; |
| | | } |
| | | |
| | | .bg-mesh { |
| | | position: absolute; |
| | | inset: -20%; |
| | | background: |
| | | radial-gradient(ellipse 80% 50% at 15% 0%, rgba(47, 111, 237, 0.18), transparent 50%), |
| | | radial-gradient(ellipse 60% 45% at 85% 15%, rgba(245, 158, 11, 0.12), transparent 45%), |
| | | radial-gradient(ellipse 50% 40% at 50% 100%, rgba(22, 163, 74, 0.08), transparent 50%); |
| | | animation: mesh-shift 14s ease-in-out infinite; |
| | | } |
| | | |
| | | .bg-orb { |
| | | position: absolute; |
| | | border-radius: 50%; |
| | | filter: blur(60px); |
| | | opacity: 0.55; |
| | | animation: orb-float 18s ease-in-out infinite; |
| | | } |
| | | |
| | | .bg-orb--a { |
| | | width: 320px; |
| | | height: 320px; |
| | | top: -80px; |
| | | right: 5%; |
| | | background: rgba(47, 111, 237, 0.35); |
| | | } |
| | | |
| | | .bg-orb--b { |
| | | width: 260px; |
| | | height: 260px; |
| | | bottom: 10%; |
| | | left: -40px; |
| | | background: rgba(99, 102, 241, 0.28); |
| | | animation-delay: -6s; |
| | | } |
| | | |
| | | .bg-orb--c { |
| | | width: 200px; |
| | | height: 200px; |
| | | top: 40%; |
| | | right: 25%; |
| | | background: rgba(245, 158, 11, 0.22); |
| | | animation-delay: -12s; |
| | | } |
| | | |
| | | .bg-grid { |
| | | position: absolute; |
| | | inset: 0; |
| | | opacity: 0.35; |
| | | background-image: |
| | | linear-gradient(rgba(15, 23, 42, 0.04) 1px, transparent 1px), |
| | | linear-gradient(90deg, rgba(15, 23, 42, 0.04) 1px, transparent 1px); |
| | | background-size: 48px 48px; |
| | | mask-image: radial-gradient(ellipse 90% 70% at 50% 30%, black 20%, transparent 75%); |
| | | } |
| | | |
| | | .page-inner { |
| | | position: relative; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .glass-card { |
| | | backdrop-filter: blur(14px); |
| | | -webkit-backdrop-filter: blur(14px); |
| | | border: 1px solid rgba(255, 255, 255, 0.65); |
| | | box-shadow: var(--lux-shadow-soft), inset 0 1px 0 rgba(255, 255, 255, 0.85); |
| | | transition: box-shadow 0.35s ease, transform 0.35s ease; |
| | | } |
| | | |
| | | .glass-card:hover { |
| | | box-shadow: var(--lux-shadow-lift), inset 0 1px 0 rgba(255, 255, 255, 0.9); |
| | | } |
| | | |
| | | .filter-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; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .title-badge { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 44px; |
| | | height: 44px; |
| | | border-radius: 14px; |
| | | background: linear-gradient(145deg, rgba(47, 111, 237, 0.2), rgba(47, 111, 237, 0.06)); |
| | | box-shadow: 0 8px 24px rgba(47, 111, 237, 0.15); |
| | | } |
| | | |
| | | .card-icon { |
| | | font-size: 22px; |
| | | color: var(--lux-primary); |
| | | } |
| | | |
| | | .title-block { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 4px; |
| | | } |
| | | |
| | | .title-row { |
| | | display: flex; |
| | | align-items: center; |
| | | flex-wrap: wrap; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .shimmer-text { |
| | | background: linear-gradient( |
| | | 90deg, |
| | | var(--lux-text) 0%, |
| | | var(--lux-text) 40%, |
| | | rgba(47, 111, 237, 0.95) 50%, |
| | | var(--lux-text) 60%, |
| | | var(--lux-text) 100% |
| | | ); |
| | | background-size: 200% auto; |
| | | -webkit-background-clip: text; |
| | | background-clip: text; |
| | | color: transparent; |
| | | animation: shimmer 5s linear infinite; |
| | | } |
| | | |
| | | .live-pill { |
| | | font-size: 11px; |
| | | font-weight: 650; |
| | | letter-spacing: 0.04em; |
| | | padding: 4px 10px; |
| | | border-radius: 999px; |
| | | color: #0d47a1; |
| | | background: linear-gradient(135deg, rgba(47, 111, 237, 0.18), rgba(47, 111, 237, 0.06)); |
| | | border: 1px solid rgba(47, 111, 237, 0.22); |
| | | box-shadow: 0 2px 10px rgba(47, 111, 237, 0.12); |
| | | } |
| | | |
| | | .filter-layout { |
| | |
| | | margin: 0; |
| | | } |
| | | |
| | | .filter-form :deep(.el-input__wrapper), |
| | | .filter-form :deep(.el-select .el-input__wrapper) { |
| | | border-radius: 10px; |
| | | box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04); |
| | | transition: box-shadow 0.2s ease; |
| | | } |
| | | |
| | | .filter-form :deep(.el-input__wrapper:hover) { |
| | | box-shadow: 0 4px 14px rgba(47, 111, 237, 0.1); |
| | | } |
| | | |
| | | .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); |
| | | padding-top: 12px; |
| | | border-top: 1px dashed rgba(15, 23, 42, 0.12); |
| | | } |
| | | |
| | | .filter-actions :deep(.lux-btn) { |
| | | border-radius: 10px; |
| | | font-weight: 600; |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | | } |
| | | |
| | | .filter-actions :deep(.lux-btn:hover) { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 8px 20px rgba(47, 111, 237, 0.18); |
| | | } |
| | | |
| | | .filter-actions :deep(.el-button--success.is-plain) { |
| | | border-color: rgba(22, 163, 74, 0.35); |
| | | } |
| | | |
| | | .action-group { |
| | |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 10px; |
| | | gap: 12px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .card-head-left { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .card-icon { |
| | | .panel-head-main { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | } |
| | | |
| | | .panel-accent { |
| | | width: 4px; |
| | | height: 22px; |
| | | border-radius: 4px; |
| | | background: linear-gradient(180deg, #5b8cff, #2f6fed); |
| | | box-shadow: 0 2px 8px rgba(47, 111, 237, 0.35); |
| | | } |
| | | |
| | | .panel-accent--emerald { |
| | | background: linear-gradient(180deg, #34d399, #059669); |
| | | box-shadow: 0 2px 8px rgba(5, 150, 105, 0.35); |
| | | } |
| | | |
| | | .chart-tag { |
| | | margin-left: 10px; |
| | | font-size: 11px; |
| | | font-weight: 650; |
| | | color: rgba(15, 23, 42, 0.55); |
| | | padding: 2px 8px; |
| | | border-radius: 6px; |
| | | background: rgba(15, 23, 42, 0.05); |
| | | } |
| | | |
| | | .count-chip { |
| | | font-size: 12px; |
| | | font-weight: 650; |
| | | color: var(--lux-primary); |
| | | padding: 6px 12px; |
| | | border-radius: 999px; |
| | | background: rgba(47, 111, 237, 0.1); |
| | | border: 1px solid rgba(47, 111, 237, 0.15); |
| | | } |
| | | |
| | | .card-title { |
| | | font-weight: 760; |
| | | color: var(--lux-text); |
| | | letter-spacing: -0.02em; |
| | | } |
| | | |
| | | .subtle { |
| | |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .kpi-card { |
| | | padding: 2px; |
| | | } |
| | | |
| | | .kpi-strip { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .kpi-item { |
| | | padding: 12px 14px; |
| | | border-radius: 12px; |
| | | border: 1px solid rgba(15, 23, 42, 0.08); |
| | | position: relative; |
| | | padding: 16px 16px 14px; |
| | | border-radius: 14px; |
| | | border: 1px solid rgba(255, 255, 255, 0.7); |
| | | overflow: hidden; |
| | | transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.3s ease; |
| | | } |
| | | |
| | | .kpi-item:hover { |
| | | transform: translateY(-4px); |
| | | box-shadow: 0 16px 36px rgba(15, 23, 42, 0.1); |
| | | } |
| | | |
| | | .kpi-glow { |
| | | position: absolute; |
| | | right: -20%; |
| | | bottom: -40%; |
| | | width: 120px; |
| | | height: 120px; |
| | | border-radius: 50%; |
| | | filter: blur(40px); |
| | | opacity: 0.45; |
| | | pointer-events: none; |
| | | } |
| | | |
| | | .kpi-std .kpi-glow { |
| | | background: rgba(47, 111, 237, 0.55); |
| | | } |
| | | |
| | | .kpi-act .kpi-glow { |
| | | background: rgba(245, 158, 11, 0.5); |
| | | } |
| | | |
| | | .kpi-diff .kpi-glow { |
| | | background: rgba(239, 68, 68, 0.45); |
| | | } |
| | | |
| | | .kpi-rate .kpi-glow { |
| | | background: rgba(22, 163, 74, 0.5); |
| | | } |
| | | |
| | | .kpi-top { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .kpi-ico { |
| | | font-size: 18px; |
| | | opacity: 0.88; |
| | | } |
| | | |
| | | .kpi-std { |
| | | background: linear-gradient(135deg, rgba(47, 111, 237, 0.1), rgba(255, 255, 255, 0.86)); |
| | | background: linear-gradient(145deg, rgba(47, 111, 237, 0.14), rgba(255, 255, 255, 0.92)); |
| | | } |
| | | |
| | | .kpi-std .kpi-ico { |
| | | color: #2563eb; |
| | | } |
| | | |
| | | .kpi-act { |
| | | background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(255, 255, 255, 0.86)); |
| | | background: linear-gradient(145deg, rgba(245, 158, 11, 0.16), rgba(255, 255, 255, 0.92)); |
| | | } |
| | | |
| | | .kpi-act .kpi-ico { |
| | | color: #d97706; |
| | | } |
| | | |
| | | .kpi-diff { |
| | | background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(255, 255, 255, 0.86)); |
| | | background: linear-gradient(145deg, rgba(239, 68, 68, 0.12), rgba(255, 255, 255, 0.92)); |
| | | } |
| | | |
| | | .kpi-diff .kpi-ico { |
| | | color: #dc2626; |
| | | } |
| | | |
| | | .kpi-rate { |
| | | background: linear-gradient(135deg, rgba(22, 163, 74, 0.1), rgba(255, 255, 255, 0.86)); |
| | | background: linear-gradient(145deg, rgba(22, 163, 74, 0.12), rgba(255, 255, 255, 0.92)); |
| | | } |
| | | |
| | | .kpi-rate .kpi-ico { |
| | | color: #059669; |
| | | } |
| | | |
| | | .kpi-label { |
| | | font-size: 12px; |
| | | color: var(--lux-subtle); |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .kpi-value { |
| | | margin-top: 6px; |
| | | position: relative; |
| | | z-index: 1; |
| | | font-size: 22px; |
| | | font-weight: 780; |
| | | color: var(--lux-text); |
| | | font-variant-numeric: tabular-nums; |
| | | } |
| | | |
| | | .cost-value { |
| | |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .chart-section :deep(.el-card__header) { |
| | | border-bottom: 1px solid rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | .chart-wrap { |
| | | position: relative; |
| | | padding-top: 34px; |
| | | border-radius: 12px; |
| | | padding-top: 40px; |
| | | border-radius: 14px; |
| | | overflow: hidden; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.5) 0%, rgba(248, 250, 252, 0.95) 100%); |
| | | border: 1px solid rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | .chart-content { |
| | | height: 360px; |
| | | height: 380px; |
| | | } |
| | | |
| | | .chart-tools { |
| | |
| | | |
| | | .chart-tools-inline { |
| | | position: absolute; |
| | | top: 4px; |
| | | right: 6px; |
| | | top: 6px; |
| | | right: 8px; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .chart-tool { |
| | | font-size: 11px; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | gap: 4px; |
| | | font-size: 12px; |
| | | font-weight: 650; |
| | | line-height: 1; |
| | | padding: 6px 10px; |
| | | padding: 8px 12px; |
| | | border-radius: 10px; |
| | | border: 1px solid rgba(15, 23, 42, 0.1); |
| | | background: rgba(255, 255, 255, 0.78); |
| | | color: rgba(15, 23, 42, 0.72); |
| | | background: rgba(255, 255, 255, 0.88); |
| | | color: rgba(15, 23, 42, 0.75); |
| | | cursor: pointer; |
| | | transition: background-color 0.16s ease, border-color 0.16s ease, transform 0.16s ease; |
| | | transition: |
| | | background-color 0.2s ease, |
| | | border-color 0.2s ease, |
| | | transform 0.2s ease, |
| | | box-shadow 0.2s ease; |
| | | } |
| | | |
| | | .chart-tool--primary { |
| | | border-color: rgba(47, 111, 237, 0.28); |
| | | background: linear-gradient(135deg, rgba(47, 111, 237, 0.12), rgba(255, 255, 255, 0.95)); |
| | | color: #1e40af; |
| | | } |
| | | |
| | | .chart-tool:hover { |
| | | background: rgba(47, 111, 237, 0.08); |
| | | border-color: rgba(47, 111, 237, 0.22); |
| | | transform: translateY(-1px); |
| | | background: rgba(47, 111, 237, 0.1); |
| | | border-color: rgba(47, 111, 237, 0.28); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 6px 16px rgba(47, 111, 237, 0.12); |
| | | } |
| | | |
| | | .large-chart-content { |
| | | height: 70vh; |
| | | min-height: 520px; |
| | | width: 100%; |
| | | overflow: hidden; // 防止 ECharts 画布溢出遮挡弹窗标题栏 |
| | | } |
| | | |
| | | .std-cost-page :deep(.el-dialog__header) { |
| | | position: relative; |
| | | z-index: 3; |
| | | } |
| | | |
| | | .std-cost-page :deep(.el-dialog__headerbtn) { |
| | | position: relative; |
| | | z-index: 4; |
| | | } |
| | | |
| | | .std-cost-page :deep(.el-dialog__body) { |
| | | position: relative; |
| | | z-index: 1; |
| | | } |
| | | |
| | | .pagination-container { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | padding-top: 12px; |
| | | padding-top: 14px; |
| | | } |
| | | |
| | | .w-260 { |
| | |
| | | ::deep(.lux-table) { |
| | | border-radius: 12px; |
| | | overflow: hidden; |
| | | --el-table-border-color: rgba(15, 23, 42, 0.06); |
| | | } |
| | | |
| | | ::deep(.lux-table th.el-table__cell) { |
| | | background: rgba(15, 23, 42, 0.03); |
| | | background: linear-gradient(180deg, rgba(15, 23, 42, 0.04), rgba(15, 23, 42, 0.02)); |
| | | font-weight: 700; |
| | | color: rgba(15, 23, 42, 0.75); |
| | | } |
| | | |
| | | ::deep(.lux-table .el-table__row) { |
| | | transition: background-color 0.2s ease; |
| | | } |
| | | |
| | | ::deep(.lux-table .el-table__row:hover > td.el-table__cell) { |
| | | background-color: rgba(47, 111, 237, 0.06) !important; |
| | | background-color: rgba(47, 111, 237, 0.07) !important; |
| | | } |
| | | |
| | | @media (max-width: 1100px) { |
| | |
| | | justify-content: flex-start; |
| | | padding-top: 8px; |
| | | } |
| | | |
| | | .shimmer-text { |
| | | animation: none; |
| | | color: var(--lux-text); |
| | | background: none; |
| | | -webkit-background-clip: unset; |
| | | background-clip: unset; |
| | | } |
| | | } |
| | | |
| | | @media (prefers-reduced-motion: reduce) { |
| | | .bg-mesh, |
| | | .bg-orb, |
| | | .shimmer-text { |
| | | animation: none; |
| | | } |
| | | |
| | | .shimmer-text { |
| | | color: var(--lux-text); |
| | | background: none; |
| | | -webkit-background-clip: unset; |
| | | background-clip: unset; |
| | | } |
| | | |
| | | .kpi-item:hover { |
| | | transform: none; |
| | | } |
| | | } |
| | | </style> |