| | |
| | | <main class="container mx-auto px-4 pb-10"> |
| | | <!-- 财务指标卡片 --> |
| | | <div class="stats-cards"> |
| | | <!-- 总营收 --> |
| | | <div class="stat-card stat-card-blue"> |
| | | <div class="stat-icon"> |
| | | <img src="@/assets/icons/png/walletBlue@2x.png" |
| | | alt="总营收" /> |
| | | </div> |
| | | <div class="stat-icon"><img src="@/assets/icons/png/walletBlue@2x.png" |
| | | alt="总营收" /></div> |
| | | <div class="stat-content"> |
| | | <div class="stat-label">总营收</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }} 元</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' 元' : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <!-- 总支出 --> |
| | | <div class="stat-card stat-card-orange"> |
| | | <div class="stat-icon"> |
| | | <img src="@/assets/icons/png/walletOrange@2x.png" |
| | | alt="总支出" /> |
| | | </div> |
| | | <div class="stat-icon"><img src="@/assets/icons/png/walletOrange@2x.png" |
| | | alt="总支出" /></div> |
| | | <div class="stat-content"> |
| | | <div class="stat-label">总支出</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }} 元</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' 元' : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <!-- 总收入笔数 --> |
| | | <div class="stat-card stat-card-green"> |
| | | <div class="stat-icon"> |
| | | <img src="@/assets/icons/png/walletGreen@2x.png" |
| | | alt="总收入笔数" /> |
| | | </div> |
| | | <div class="stat-icon"><img src="@/assets/icons/png/walletGreen@2x.png" |
| | | alt="应收账款" /></div> |
| | | <div class="stat-content"> |
| | | <div class="stat-label">总收入笔数</div> |
| | | <div class="stat-value">{{ pageInfo.incomeNumber || 0 }} 笔</div> |
| | | <div class="stat-label">应收账款</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.totalReceivable || 0) }}{{ Math.abs(pageInfo.totalReceivable) < 10000 ? ' 元' : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <!-- 总支出笔数 --> |
| | | <div class="stat-card stat-card-red"> |
| | | <div class="stat-icon"> |
| | | <img src="@/assets/icons/png/walletRed@2x.png" |
| | | alt="总支出笔数" /> |
| | | </div> |
| | | <div class="stat-icon"><img src="@/assets/icons/png/walletRed@2x.png" |
| | | alt="应付账款" /></div> |
| | | <div class="stat-content"> |
| | | <div class="stat-label">总支出笔数</div> |
| | | <div class="stat-value">{{ pageInfo.expenseNumber || 0 }} 笔</div> |
| | | <div class="stat-label">应付账款</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.totalPayable || 0) }}{{ Math.abs(pageInfo.totalPayable) < 10000 ? ' 元' : '' }}</div> |
| | | </div> |
| | | </div> |
| | | <!-- 净收入 --> |
| | | <div class="stat-card stat-card-yellow"> |
| | | <div class="stat-icon"> |
| | | <img src="@/assets/icons/png/walletYellow@2x.png" |
| | | alt="净收入" /> |
| | | </div> |
| | | <div class="stat-icon"><img src="@/assets/icons/png/walletYellow@2x.png" |
| | | alt="净利润" /></div> |
| | | <div class="stat-content"> |
| | | <div class="stat-label">净收入</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }} 元</div> |
| | | <div class="stat-label">净利润</div> |
| | | <div class="stat-value">{{ formatMoney(pageInfo.netRevenue || 0) }}{{ Math.abs(pageInfo.netRevenue) < 10000 ? ' 元' : '' }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 中间图表区域 --> |
| | | <!-- 图表区域 --> |
| | | <div class="charts-row"> |
| | | <!-- 左侧:收入支出分析 --> |
| | | <!-- 1. 收支构成分析 (双环形图 + 净利中心) --> |
| | | <el-card class="chart-card"> |
| | | <h2 class="section-title">收入支出分析</h2> |
| | | <div class="pie-chart-container"> |
| | | <Echarts :legend="pieLegendIncomeExpense" |
| | | :chartStyle="chartStylePie" |
| | | :series="pieSeriesIncomeExpense" |
| | | :tooltip="pieTooltipIncomeExpense" |
| | | style="height: 320px; width: 100%;"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="header-title">收支构成及净利分析</span> |
| | | <el-tooltip content="左侧为收入构成,右侧为支出构成,中间展示盈亏净额" |
| | | placement="top"> |
| | | <el-icon> |
| | | <QuestionFilled /> |
| | | </el-icon> |
| | | </el-tooltip> |
| | | </div> |
| | | </template> |
| | | <div class="financial-overview-container"> |
| | | <!-- 收入展示 (左侧) --> |
| | | <div style="width:60%"> |
| | | <div class="overview-item income" |
| | | style="margin-bottom: 20px;"> |
| | | <div class="overview-box"> |
| | | <div class="icon-circle"> |
| | | <el-icon> |
| | | <TrendCharts /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="data-content"> |
| | | <div class="label">本期总收入</div> |
| | | <div class="value">{{ formatMoney(pageInfo.totalIncome) }}</div> |
| | | <div class="unit">RMB{{ Math.abs(pageInfo.totalIncome) < 10000 ? ' / 元' : '' }}</div> |
| | | </div> |
| | | <div class="bg-decoration">INCOME</div> |
| | | </div> |
| | | </div> |
| | | <div class="overview-item expense"> |
| | | <div class="overview-box"> |
| | | <div class="icon-circle"> |
| | | <el-icon> |
| | | <Sell /> |
| | | </el-icon> |
| | | </div> |
| | | <div class="data-content"> |
| | | <div class="label">本期总支出</div> |
| | | <div class="value">{{ formatMoney(pageInfo.totalExpense) }}</div> |
| | | <div class="unit">RMB{{ Math.abs(pageInfo.totalExpense) < 10000 ? ' / 元' : '' }}</div> |
| | | </div> |
| | | <div class="bg-decoration">EXPENSE</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 净利润核心指示 (中间) --> |
| | | <div class="profit-indicator"> |
| | | <div class="profit-gauge-wrapper"> |
| | | <Echarts :chartStyle="chartStylePie" |
| | | :series="profitGaugeSeries" |
| | | :tooltip="gaugeTooltip" |
| | | style="height: 200px; width: 100%; max-width: 200px;"> |
| | | </Echarts> |
| | | <div class="pie-stats"> |
| | | <div class="bar-stat-item"> |
| | | <span class="bar-stat-label">收入数量</span> |
| | | <span class="bar-stat-value">{{ pageInfo.incomeNumber || 0 }}</span> |
| | | <div class="profit-center-text"> |
| | | <div class="label">净利润</div> |
| | | <div class="value" |
| | | :class="pageInfo.netRevenue >= 0 ? 'plus' : 'minus'"> |
| | | {{ pageInfo.netRevenue >= 0 ? '+' : '' }}{{ formatMoney(pageInfo.netRevenue) }} |
| | | </div> |
| | | <div class="bar-stat-item"> |
| | | <span class="bar-stat-label">支出数量</span> |
| | | <span class="bar-stat-value">{{ pageInfo.expenseNumber || 0 }}</span> |
| | | <div class="rate">利润率: {{ pageInfo.totalIncome > 0 ? ((pageInfo.netRevenue / pageInfo.totalIncome) * 100).toFixed(1) : 0 }}%</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <!-- 支出展示 (右侧) --> |
| | | </div> |
| | | </el-card> |
| | | <!-- 右侧:行项盈利分析 --> |
| | | <!-- 2. 应收/应付对冲分析 (柱状图) --> |
| | | <el-card class="chart-card"> |
| | | <h2 class="section-title">行项盈利分析</h2> |
| | | <div class="bar-chart-header"> |
| | | <div class="bar-stat-item"> |
| | | <span class="bar-stat-label">当前总个数</span> |
| | | <span class="bar-stat-value">{{ allBarTypes.value?.length || 0 }}</span> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="header-title">应收/应付概览</span> |
| | | <el-tooltip content="对比当前各月份的应收账款与应付账款" |
| | | placement="top"> |
| | | <el-icon> |
| | | <QuestionFilled /> |
| | | </el-icon> |
| | | </el-tooltip> |
| | | </div> |
| | | <div class="bar-stat-item"> |
| | | <span class="bar-stat-label">支出金额</span> |
| | | <span class="bar-stat-value">{{ formatMoney(pageInfo.totalExpense || 0) }}</span> |
| | | </div> |
| | | <div class="bar-stat-item"> |
| | | <span class="bar-stat-label">收入金额</span> |
| | | <span class="bar-stat-value">{{ formatMoney(pageInfo.totalIncome || 0) }}</span> |
| | | </div> |
| | | </div> |
| | | <Echarts ref="barChart" |
| | | :chartStyle="chartStyle" |
| | | </template> |
| | | <Echarts :chartStyle="chartStyle" |
| | | :grid="barGrid" |
| | | :legend="barLegend" |
| | | :series="barSeries" |
| | | :tooltip="barTooltip" |
| | | :xAxis="barXAxis" |
| | | :yAxis="barYAxis" |
| | | style="height: 300px; width: 100%;"> |
| | | style="height: 270px; width: 100%;"> |
| | | </Echarts> |
| | | </el-card> |
| | | </div> |
| | | <!-- 底部:营收趋势分析 --> |
| | | <!-- 3. 财务综合趋势分析 (折线图) --> |
| | | <el-card class="trend-chart-card"> |
| | | <h2 class="section-title">营收趋势分析</h2> |
| | | <Echarts ref="trendChart" |
| | | :chartStyle="chartStyle" |
| | | :grid="grid" |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span class="header-title">财务绩效综合趋势</span> |
| | | <el-tooltip content="展示收入、支出及净利润的月度变化趋势" |
| | | placement="top"> |
| | | <el-icon> |
| | | <QuestionFilled /> |
| | | </el-icon> |
| | | </el-tooltip> |
| | | </div> |
| | | </template> |
| | | <Echarts :chartStyle="chartStyle" |
| | | :grid="trendGrid" |
| | | :legend="trendLegend" |
| | | :series="trendSeries" |
| | | :tooltip="tooltip" |
| | | :xAxis="xAxis0" |
| | | :tooltip="trendTooltip" |
| | | :xAxis="trendXAxis" |
| | | :yAxis="trendYAxis" |
| | | style="height: 350px; width: 100%;"> |
| | | </Echarts> |
| | |
| | | nextTick, |
| | | getCurrentInstance, |
| | | } from "vue"; |
| | | import "element-plus/dist/index.css"; |
| | | import { QuestionFilled, TrendCharts, Sell } from "@element-plus/icons-vue"; |
| | | import Echarts from "@/components/Echarts/echarts.vue"; |
| | | import { |
| | | reportForms, |
| | | reportIncome, |
| | | reportExpense, |
| | | } from "@/api/financialManagement/financialStatements"; |
| | | import { accountStatementDetailsByMonth } from "@/api/financialManagement/financialStatements"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | // 日期范围 |
| | | const dateRange = ref(null); |
| | | const { proxy } = getCurrentInstance(); |
| | | const chartStyle = { |
| | | width: "100%", |
| | | height: "100%", // 设置图表容器的高度 |
| | | position: "relative", |
| | | }; |
| | | const grid = { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }; |
| | | const lineLegend = { |
| | | show: false, |
| | | }; |
| | | // 折线图提示框 |
| | | const tooltip = reactive({ |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | const dateRange = ref(null); |
| | | const pageInfo = reactive({ |
| | | totalIncome: 0, |
| | | totalExpense: 0, |
| | | totalReceivable: 0, |
| | | totalPayable: 0, |
| | | netRevenue: 0, |
| | | }); |
| | | |
| | | const chartStyle = { width: "100%", height: "100%", position: "relative" }; |
| | | const chartStylePie = { width: "100%", height: "100%" }; |
| | | |
| | | const monthlyTrendList = ref([]); |
| | | const receivablePayableList = ref([]); |
| | | |
| | | // --- 1. 收支构成分析 (简化版逻辑) --- |
| | | const gaugeTooltip = { show: false }; |
| | | |
| | | const profitGaugeSeries = computed(() => { |
| | | const rate = |
| | | pageInfo.totalIncome > 0 |
| | | ? (pageInfo.netRevenue / pageInfo.totalIncome) * 100 |
| | | : 0; |
| | | return [ |
| | | { |
| | | type: "gauge", |
| | | startAngle: 210, |
| | | endAngle: -30, |
| | | min: 0, |
| | | max: 100, |
| | | splitNumber: 10, |
| | | radius: "100%", |
| | | progress: { |
| | | show: true, |
| | | width: 14, |
| | | itemStyle: { color: pageInfo.netRevenue >= 0 ? "#10b981" : "#f43f5e" }, |
| | | }, |
| | | pointer: { show: false }, |
| | | axisLine: { lineStyle: { width: 14, color: [[1, "#f1f5f9"]] } }, |
| | | axisTick: { show: false }, |
| | | splitLine: { show: false }, |
| | | axisLabel: { show: false }, |
| | | anchor: { show: false }, |
| | | title: { show: false }, |
| | | detail: { show: false }, |
| | | data: [{ value: Math.max(0, Math.min(100, rate)) }], |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | // --- 2. 应收/应付概览 (柱状图) --- |
| | | const barGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true }; |
| | | const barLegend = { top: "0", right: "center" }; |
| | | const barXAxis = computed(() => [ |
| | | { |
| | | type: "category", |
| | | data: receivablePayableList.value.map(item => item.month || ""), |
| | | axisTick: { alignWithLabel: true }, |
| | | }, |
| | | ]); |
| | | const barYAxis = [{ type: "value", name: "金额 (元)" }]; |
| | | const barTooltip = { trigger: "axis", axisPointer: { type: "shadow" } }; |
| | | const barSeries = computed(() => [ |
| | | { |
| | | name: "应收账款", |
| | | type: "bar", |
| | | barWidth: "30%", |
| | | data: receivablePayableList.value.map(item => item.receivable || 0), |
| | | itemStyle: { color: "#10b981" }, |
| | | }, |
| | | { |
| | | name: "应付账款", |
| | | type: "bar", |
| | | barWidth: "30%", |
| | | data: receivablePayableList.value.map(item => item.payable || 0), |
| | | itemStyle: { color: "#ef4444" }, |
| | | }, |
| | | ]); |
| | | |
| | | // --- 3. 财务综合趋势分析 (折线图) --- |
| | | const trendGrid = { left: "3%", right: "4%", bottom: "3%", containLabel: true }; |
| | | const trendLegend = { top: "0", right: "center" }; |
| | | const trendXAxis = computed(() => [ |
| | | { |
| | | type: "category", |
| | | boundaryGap: false, |
| | | data: monthlyTrendList.value.map(item => item.month || ""), |
| | | }, |
| | | ]); |
| | | const trendYAxis = [{ type: "value", name: "金额 (元)" }]; |
| | | const trendTooltip = { trigger: "axis" }; |
| | | const trendSeries = computed(() => [ |
| | | { |
| | | name: "总营收", |
| | | type: "line", |
| | | lineStyle: { color: "#aaa" }, |
| | | }, |
| | | // 自定义内容 |
| | | formatter: function (params) { |
| | | if (!params || !params.length) return ""; |
| | | const axisLabel = params[0].axisValueLabel || params[0].axisValue || ""; |
| | | const rows = params |
| | | .map(p => { |
| | | const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`; |
| | | return `${colorDot}${p.seriesName}: ${p.value}`; |
| | | }) |
| | | .join("<br/>"); |
| | | return `<div>${axisLabel}</div><div>${rows}</div>`; |
| | | }, |
| | | }); |
| | | const lineSeries0 = ref([]); |
| | | const lineSeries1 = ref([]); |
| | | |
| | | // 根据月份范围生成 x 轴数据 |
| | | const generateMonthLabels = (startMonth, endMonth) => { |
| | | const labels = []; |
| | | let current = dayjs(startMonth); |
| | | const end = dayjs(endMonth); |
| | | |
| | | while (current.isBefore(end) || current.isSame(end, "month")) { |
| | | labels.push(`${current.month() + 1}月`); |
| | | current = current.add(1, "month"); |
| | | } |
| | | |
| | | return labels; |
| | | }; |
| | | |
| | | const xAxis0 = ref([ |
| | | { |
| | | type: "category", |
| | | axisTick: { show: true, alignWithLabel: true }, |
| | | data: [], |
| | | }, |
| | | ]); |
| | | const xAxis1 = ref([ |
| | | { |
| | | type: "category", |
| | | axisTick: { show: true, alignWithLabel: true }, |
| | | data: [], |
| | | }, |
| | | ]); |
| | | const yAxis0 = [ |
| | | { |
| | | type: "value", |
| | | name: "收入统计", // 左侧y轴 |
| | | position: "left", |
| | | min: 0, |
| | | // 坐标轴名称样式 |
| | | nameTextStyle: { |
| | | color: "#000", |
| | | fontSize: 14, |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | const yAxis1 = [ |
| | | { |
| | | type: "value", |
| | | name: "支出统计", // 左侧y轴 |
| | | position: "left", |
| | | min: 0, |
| | | // 坐标轴名称样式 |
| | | nameTextStyle: { |
| | | color: "#000", |
| | | fontSize: 14, |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | const chartStylePie = { |
| | | width: "100%", |
| | | height: "100%", // 设置图表容器的高度 |
| | | }; |
| | | const pieColors = [ |
| | | "#F04864", |
| | | "#FACC14", |
| | | "#8543E0", |
| | | "#1890FF", |
| | | "#13C2C2", |
| | | "#2FC25B", |
| | | ]; // 可根据实际调整 |
| | | const pieData0 = ref([]); |
| | | const pieData1 = ref([]); |
| | | |
| | | const pieLegend0 = computed(() => ({ |
| | | show: true, |
| | | top: "center", |
| | | left: "60%", |
| | | orient: "vertical", |
| | | icon: "circle", |
| | | data: (pieData0.value || []) |
| | | .filter(item => item && item.name) |
| | | .map(item => item.name), |
| | | formatter: function (name) { |
| | | if (!name) return ""; |
| | | const item = pieData0.value.find(i => i && i.name === name); |
| | | if (!item) return name; |
| | | return `${name} | ${item.percent} ${item.amount}`; |
| | | }, |
| | | textStyle: { |
| | | color: "#333", |
| | | fontSize: 14, |
| | | lineHeight: 26, |
| | | }, |
| | | })); |
| | | const pieLegend1 = computed(() => ({ |
| | | show: true, |
| | | top: "center", |
| | | left: "60%", |
| | | orient: "vertical", |
| | | icon: "circle", |
| | | data: (pieData1.value || []) |
| | | .filter(item => item && item.name) |
| | | .map(item => item.name), |
| | | formatter: function (name) { |
| | | if (!name) return ""; |
| | | const item = pieData1.value.find(i => i && i.name === name); |
| | | if (!item) return name; |
| | | return `${name} | ${item.percent} ${item.amount}`; |
| | | }, |
| | | textStyle: { |
| | | color: "#333", |
| | | fontSize: 14, |
| | | lineHeight: 26, |
| | | }, |
| | | })); |
| | | |
| | | const materialPieSeries0 = computed(() => [ |
| | | { |
| | | type: "pie", |
| | | radius: ["50%", "65%"], |
| | | center: ["25%", "50%"], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderColor: "#fff", |
| | | borderWidth: 2, |
| | | }, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | data: (pieData0.value || []).filter(item => item && item.name), |
| | | color: pieColors, |
| | | }, |
| | | ]); |
| | | const materialPieSeries1 = computed(() => [ |
| | | { |
| | | type: "pie", |
| | | radius: ["50%", "65%"], |
| | | center: ["25%", "50%"], |
| | | avoidLabelOverlap: false, |
| | | itemStyle: { |
| | | borderColor: "#fff", |
| | | borderWidth: 2, |
| | | }, |
| | | label: { |
| | | show: false, |
| | | }, |
| | | data: (pieData1.value || []).filter(item => item && item.name), |
| | | color: pieColors, |
| | | }, |
| | | ]); |
| | | const pieTooltip = reactive({ |
| | | trigger: "item", |
| | | formatter: function (params) { |
| | | // 检查数据是否存在 |
| | | if (!params.data) return params.name; |
| | | // 拼接完整内容 |
| | | return ` |
| | | <div> |
| | | <div style="color:${params.color};font-size:16px;">●</div> |
| | | <div>${params.name}</div> |
| | | <div>占比:${params.data.percent}</div> |
| | | <div>金额:${params.data.amount}</div> |
| | | </div> |
| | | `; |
| | | }, |
| | | }); |
| | | |
| | | const pageInfo = ref({}); |
| | | |
| | | // 格式化金额 |
| | | const formatMoney = value => { |
| | | if (!value && value !== 0) return "0"; |
| | | return Number(value).toLocaleString("zh-CN", { |
| | | minimumFractionDigits: 2, |
| | | maximumFractionDigits: 2, |
| | | }); |
| | | }; |
| | | |
| | | // 收入支出分析饼图 |
| | | const pieDataIncomeExpense = computed(() => { |
| | | const totalIncome = Number(pageInfo.value.totalIncome) || 0; |
| | | const totalExpense = Number(pageInfo.value.totalExpense) || 0; |
| | | const total = totalIncome + totalExpense; |
| | | if (total === 0) { |
| | | return [ |
| | | { name: "收入", value: 0, percent: "0%" }, |
| | | { name: "支出", value: 0, percent: "0%" }, |
| | | ]; |
| | | } |
| | | const incomePercent = ((totalIncome / total) * 100).toFixed(0); |
| | | const expensePercent = ((totalExpense / total) * 100).toFixed(0); |
| | | return [ |
| | | { name: "收入", value: totalIncome, percent: `${incomePercent}%` }, |
| | | { name: "支出", value: totalExpense, percent: `${expensePercent}%` }, |
| | | ]; |
| | | }); |
| | | |
| | | const pieLegendIncomeExpense = computed(() => ({ |
| | | show: false, |
| | | })); |
| | | |
| | | const pieTooltipIncomeExpense = reactive({ |
| | | trigger: "item", |
| | | formatter: function (params) { |
| | | if (!params.data) return params.name; |
| | | return `${params.name}占比 ${params.percent}%`; |
| | | }, |
| | | }); |
| | | |
| | | const pieSeriesIncomeExpense = computed(() => [ |
| | | { |
| | | type: "pie", |
| | | radius: ["0%", "70%"], |
| | | center: ["50%", "50%"], |
| | | avoidLabelOverlap: true, |
| | | itemStyle: { |
| | | borderColor: "#fff", |
| | | borderWidth: 2, |
| | | }, |
| | | label: { |
| | | show: true, |
| | | position: "outside", |
| | | formatter: function (params) { |
| | | return `${params.name}占比 ${params.percent}%`; |
| | | }, |
| | | fontSize: 14, |
| | | color: "#333", |
| | | }, |
| | | labelLine: { |
| | | show: true, |
| | | length: 15, |
| | | length2: 10, |
| | | lineStyle: { |
| | | color: "#333", |
| | | }, |
| | | }, |
| | | emphasis: { |
| | | label: { |
| | | show: true, |
| | | fontSize: 16, |
| | | fontWeight: "bold", |
| | | }, |
| | | }, |
| | | data: pieDataIncomeExpense.value, |
| | | color: ["#1890FF", "#FACC14"], |
| | | }, |
| | | ]); |
| | | |
| | | // 行项盈利分析柱状图 |
| | | const barXAxis = computed(() => { |
| | | return [ |
| | | { |
| | | type: "category", |
| | | data: |
| | | allBarTypes.value && allBarTypes.value.length > 0 |
| | | ? allBarTypes.value |
| | | : ["项目1", "项目2", "项目3", "项目4", "项目5", "项目6", "项目7"], |
| | | axisTick: { show: true, alignWithLabel: true }, |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | const barYAxis = [ |
| | | { |
| | | type: "value", |
| | | name: "单位: 元", |
| | | position: "left", |
| | | min: 0, |
| | | nameTextStyle: { |
| | | color: "#000", |
| | | fontSize: 14, |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | const barGrid = { |
| | | left: "3%", |
| | | right: "4%", |
| | | bottom: "3%", |
| | | containLabel: true, |
| | | }; |
| | | |
| | | const barLegend = { |
| | | show: true, |
| | | top: 10, |
| | | right: 10, |
| | | }; |
| | | |
| | | // 获取所有类型名称 |
| | | const allBarTypes = computed(() => { |
| | | const incomeTypes = (lineSeries0.value || []) |
| | | .map(item => item.name || item.typeName) |
| | | .filter(Boolean); |
| | | const expenseTypes = (lineSeries1.value || []) |
| | | .map(item => item.name || item.typeName) |
| | | .filter(Boolean); |
| | | return [...new Set([...incomeTypes, ...expenseTypes])]; |
| | | }); |
| | | |
| | | const barSeries = computed(() => { |
| | | if (allBarTypes.value.length === 0) { |
| | | return [ |
| | | { |
| | | name: "支出", |
| | | type: "bar", |
| | | data: [], |
| | | itemStyle: { color: "#1890FF" }, |
| | | }, |
| | | { |
| | | name: "收入", |
| | | type: "bar", |
| | | data: [], |
| | | itemStyle: { color: "#13C2C2" }, |
| | | }, |
| | | ]; |
| | | } |
| | | |
| | | // 计算每个项目的总收入(汇总所有月份) |
| | | const incomeData = allBarTypes.value.map(typeName => { |
| | | const incomeItem = (lineSeries0.value || []).find( |
| | | item => (item.name || item.typeName) === typeName |
| | | ); |
| | | if (incomeItem && incomeItem.data && Array.isArray(incomeItem.data)) { |
| | | return incomeItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0); |
| | | } |
| | | return 0; |
| | | }); |
| | | |
| | | // 计算每个项目的总支出(汇总所有月份) |
| | | const expenseData = allBarTypes.value.map(typeName => { |
| | | const expenseItem = (lineSeries1.value || []).find( |
| | | item => (item.name || item.typeName) === typeName |
| | | ); |
| | | if (expenseItem && expenseItem.data && Array.isArray(expenseItem.data)) { |
| | | return expenseItem.data.reduce((sum, val) => sum + (Number(val) || 0), 0); |
| | | } |
| | | return 0; |
| | | }); |
| | | |
| | | return [ |
| | | { |
| | | name: "支出", |
| | | type: "bar", |
| | | data: expenseData, |
| | | itemStyle: { color: "#1890FF" }, |
| | | }, |
| | | { |
| | | name: "收入", |
| | | type: "bar", |
| | | data: incomeData, |
| | | itemStyle: { color: "#13C2C2" }, |
| | | }, |
| | | ]; |
| | | }); |
| | | |
| | | const barTooltip = reactive({ |
| | | trigger: "axis", |
| | | axisPointer: { |
| | | type: "shadow", |
| | | }, |
| | | formatter: function (params) { |
| | | if (!params || !params.length) return ""; |
| | | const axisLabel = params[0].axisValueLabel || params[0].axisValue || ""; |
| | | const rows = params |
| | | .map(p => { |
| | | const colorDot = `<span style="display:inline-block;margin-right:6px;width:8px;height:8px;border-radius:50%;background:${p.color}"></span>`; |
| | | const value = |
| | | typeof p.value === "number" ? p.value.toFixed(2) : p.value; |
| | | return `${colorDot}${p.seriesName} ${value}`; |
| | | }) |
| | | .join("<br/>"); |
| | | return `<div>${axisLabel}</div><div>${rows}</div>`; |
| | | }, |
| | | }); |
| | | |
| | | // 营收趋势分析 |
| | | const trendLegend = { |
| | | show: true, |
| | | top: 10, |
| | | right: 10, |
| | | }; |
| | | |
| | | const trendYAxis = [ |
| | | { |
| | | type: "value", |
| | | name: "单位: 元", |
| | | position: "left", |
| | | min: 0, |
| | | nameTextStyle: { |
| | | color: "#000", |
| | | fontSize: 14, |
| | | }, |
| | | }, |
| | | ]; |
| | | |
| | | const trendSeries = computed(() => { |
| | | // 汇总所有支出类型的数据 |
| | | let expenseTrend = []; |
| | | if (lineSeries1.value.length > 0) { |
| | | const monthCount = Math.max( |
| | | ...lineSeries1.value.map(item => item.data?.length || 0) |
| | | ); |
| | | expenseTrend = Array(monthCount).fill(0); |
| | | lineSeries1.value.forEach(item => { |
| | | if (item.data && Array.isArray(item.data)) { |
| | | item.data.forEach((val, index) => { |
| | | if (index < monthCount) { |
| | | expenseTrend[index] += Number(val) || 0; |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | // 汇总所有收入类型的数据 |
| | | let incomeTrend = []; |
| | | if (lineSeries0.value.length > 0) { |
| | | const monthCount = Math.max( |
| | | ...lineSeries0.value.map(item => item.data?.length || 0) |
| | | ); |
| | | incomeTrend = Array(monthCount).fill(0); |
| | | lineSeries0.value.forEach(item => { |
| | | if (item.data && Array.isArray(item.data)) { |
| | | item.data.forEach((val, index) => { |
| | | if (index < monthCount) { |
| | | incomeTrend[index] += Number(val) || 0; |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | return [ |
| | | { |
| | | name: "支出", |
| | | type: "line", |
| | | data: expenseTrend, |
| | | itemStyle: { color: "#1890FF" }, |
| | | smooth: true, |
| | | data: monthlyTrendList.value.map(item => item.income || 0), |
| | | itemStyle: { color: "#4f46e5" }, |
| | | areaStyle: { opacity: 0.1 }, |
| | | }, |
| | | { |
| | | name: "收入", |
| | | name: "总支出", |
| | | type: "line", |
| | | data: incomeTrend, |
| | | itemStyle: { color: "#13C2C2" }, |
| | | smooth: true, |
| | | data: monthlyTrendList.value.map(item => item.expense || 0), |
| | | itemStyle: { color: "#f97316" }, |
| | | }, |
| | | ]; |
| | | }); |
| | | { |
| | | name: "净利润", |
| | | type: "line", |
| | | smooth: true, |
| | | data: monthlyTrendList.value.map(item => item.profit || 0), |
| | | lineStyle: { width: 4, type: "dashed" }, |
| | | itemStyle: { color: "#10b981" }, |
| | | }, |
| | | ]); |
| | | |
| | | // 获取最近六个月的范围 |
| | | const getLastSixMonths = () => { |
| | | const endMonth = dayjs().format("YYYY-MM"); |
| | | const startMonth = dayjs().subtract(5, "month").format("YYYY-MM"); |
| | | return [startMonth, endMonth]; |
| | | // --- 公用逻辑 --- |
| | | const formatMoney = val => { |
| | | return val; |
| | | }; |
| | | |
| | | const handleDateChange = val => { |
| | | if (val) getData(); |
| | | }; |
| | | |
| | | const resetDateRange = () => { |
| | | dateRange.value = [ |
| | | dayjs().subtract(5, "month").format("YYYY-MM"), |
| | | dayjs().format("YYYY-MM"), |
| | | ]; |
| | | getData(); |
| | | }; |
| | | |
| | | const disabledDate = time => dayjs(time).isAfter(dayjs(), "month"); |
| | | |
| | | const getData = async () => { |
| | | if ( |
| | | !dateRange.value || |
| | | !Array.isArray(dateRange.value) || |
| | | dateRange.value.length !== 2 |
| | | ) { |
| | | return; |
| | | } |
| | | const startDateStr = dateRange.value[0]; |
| | | const endDateStr = dateRange.value[1]; |
| | | if (!startDateStr || !endDateStr) { |
| | | return; |
| | | } |
| | | if (!dateRange.value || dateRange.value.length !== 2) return; |
| | | |
| | | // 验证日期格式并转换为完整日期 |
| | | const startDate = dayjs(startDateStr); |
| | | const endDate = dayjs(endDateStr); |
| | | if (!startDate.isValid() || !endDate.isValid()) { |
| | | console.error("无效的日期格式"); |
| | | return; |
| | | } |
| | | |
| | | // 更新 x 轴数据 |
| | | const monthLabels = generateMonthLabels(startDateStr, endDateStr); |
| | | xAxis0.value[0].data = monthLabels; |
| | | xAxis1.value[0].data = monthLabels; |
| | | |
| | | // 开始月份拼接第一天,结束月份拼接最后一天 |
| | | const entryDateStart = startDate.startOf("month").format("YYYY-MM-DD"); |
| | | const entryDateEnd = endDate.endOf("month").format("YYYY-MM-DD"); |
| | | const params = { |
| | | entryDateStart: dayjs(dateRange.value[0]) |
| | | .startOf("month") |
| | | .format("YYYY-MM-DD"), |
| | | entryDateEnd: dayjs(dateRange.value[1]).endOf("month").format("YYYY-MM-DD"), |
| | | }; |
| | | |
| | | try { |
| | | // const {code,data} = await reportForms({entryDateStart, entryDateEnd}); |
| | | // if(code === 200 && data) { |
| | | // pageInfo.value = data || {}; |
| | | // // 安全处理数据,过滤掉 null 或 undefined |
| | | // pieData0.value = (data.incomeType || []).filter(item => item && item.typeName).map(item=>({ |
| | | // name:item.typeName || '', |
| | | // value:item.account || 0, |
| | | // percent:`${((item.proportion || 0) * 100).toFixed(2)}%`, |
| | | // amount:`¥${(item.account || 0).toFixed(2)}` |
| | | // })) |
| | | // pieData1.value = (data.expenseType || []).filter(item => item && item.typeName).map(item=>({ |
| | | // name:item.typeName || '', |
| | | // value:item.account || 0, |
| | | // percent:`${((item.proportion || 0) * 100).toFixed(2)}%`, |
| | | // amount:`¥${(item.account || 0).toFixed(2)}` |
| | | // })) |
| | | // } |
| | | } catch (error) { |
| | | console.error("获取财务指标数据失败:", error); |
| | | const res = await accountStatementDetailsByMonth(params); |
| | | if (res.code === 200 && res.data) { |
| | | const data = res.data; |
| | | // 更新顶部汇总卡片数据 |
| | | pageInfo.totalIncome = data.totalIncome || 0; |
| | | pageInfo.totalExpense = data.totalExpense || 0; |
| | | pageInfo.totalReceivable = data.accountsReceivable || 0; |
| | | pageInfo.totalPayable = data.accountsPayable || 0; |
| | | pageInfo.netRevenue = data.netRevenue || 0; |
| | | |
| | | // 更新图表数据 |
| | | monthlyTrendList.value = data.monthlyTrendList || []; |
| | | receivablePayableList.value = data.receivablePayableList || []; |
| | | } |
| | | try { |
| | | // const { code, data } = await reportIncome({ entryDateStart, entryDateEnd }); |
| | | // if (code == 200 && data && Array.isArray(data)) { |
| | | // lineSeries0.value = data |
| | | // .filter(item => item && item.typeName) |
| | | // .map(item => ({ |
| | | // name: item.typeName || "", |
| | | // type: "line", |
| | | // data: (item.account || []).map(val => Number(val) || 0), |
| | | // })); |
| | | // } |
| | | } catch (error) { |
| | | console.error("获取财务指标数据失败:", error); |
| | | } |
| | | try { |
| | | // const { code, data } = await reportExpense({ |
| | | // entryDateStart, |
| | | // entryDateEnd, |
| | | // }); |
| | | // if (code == 200 && data && Array.isArray(data)) { |
| | | // lineSeries1.value = data |
| | | // .filter(item => item && item.typeName) |
| | | // .map(item => ({ |
| | | // name: item.typeName || "", |
| | | // type: "line", |
| | | // data: (item.account || []).map(val => Number(val) || 0), |
| | | // })); |
| | | // } |
| | | } catch (error) { |
| | | console.error("获取财务指标数据失败:", error); |
| | | console.error("获取财务报表数据失败:", error); |
| | | } |
| | | }; |
| | | |
| | | // 初始化 |
| | | onMounted(() => { |
| | | // 设置默认值为最近六个月 |
| | | const defaultRange = getLastSixMonths(); |
| | | dateRange.value = defaultRange; |
| | | // 使用 nextTick 确保组件完全渲染后再调用 |
| | | nextTick(() => { |
| | | getData(); |
| | | resetDateRange(); |
| | | }); |
| | | }); |
| | | |
| | | // 限制月份选择范围(最多12个月) |
| | | const disabledDate = time => { |
| | | // 如果没有选择开始月份,不禁用任何日期 |
| | | if ( |
| | | !dateRange.value || |
| | | !Array.isArray(dateRange.value) || |
| | | !dateRange.value[0] |
| | | ) { |
| | | return false; |
| | | } |
| | | |
| | | const startMonth = dayjs(dateRange.value[0]); |
| | | const currentMonth = dayjs(time); |
| | | |
| | | // 如果当前月份在开始月份之前,禁用 |
| | | if (currentMonth.isBefore(startMonth, "month")) { |
| | | return true; |
| | | } |
| | | |
| | | // 计算最大允许的月份(开始月份 + 11个月 = 12个月) |
| | | const maxMonth = startMonth.add(11, "month"); |
| | | |
| | | // 禁用超过12个月的月份 |
| | | return currentMonth.isAfter(maxMonth, "month"); |
| | | }; |
| | | |
| | | // 处理月份范围变化 |
| | | const handleDateChange = newRange => { |
| | | if (!newRange || !Array.isArray(newRange) || newRange.length !== 2) { |
| | | return; |
| | | } |
| | | |
| | | // 验证月份范围不超过12个月 |
| | | const startDate = dayjs(newRange[0]); |
| | | const endDate = dayjs(newRange[1]); |
| | | const monthDiff = endDate.diff(startDate, "month"); |
| | | |
| | | if (monthDiff > 11) { |
| | | proxy.$modal.msgWarning("最多只能选择12个月份"); |
| | | // 自动调整为12个月 |
| | | const adjustedEnd = startDate.add(11, "month").format("YYYY-MM"); |
| | | dateRange.value = [newRange[0], adjustedEnd]; |
| | | getData(); |
| | | return; |
| | | } |
| | | |
| | | dateRange.value = newRange; |
| | | getData(); |
| | | }; |
| | | |
| | | // 重置月份范围 |
| | | const resetDateRange = () => { |
| | | // 重置为最近六个月 |
| | | dateRange.value = getLastSixMonths(); |
| | | getData(); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | /* 基础样式补充 */ |
| | | :root { |
| | | --el-color-primary: #4f46e5; |
| | | } |
| | | |
| | | /* 统计卡片样式 */ |
| | | .stats-cards { |
| | | display: grid; |
| | | grid-template-columns: repeat(5, 1fr); |
| | | grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .stat-card { |
| | | background: #fff; |
| | | border: 1px solid #e4e7ed; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | border: 1px solid #edf2f7; |
| | | border-radius: 12px; |
| | | padding: 24px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 15px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | transition: all 0.3s; |
| | | gap: 16px; |
| | | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); |
| | | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| | | |
| | | &:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
| | | transform: translateY(-4px); |
| | | } |
| | | |
| | | .stat-icon { |
| | | width: 48px; |
| | | height: 48px; |
| | | flex-shrink: 0; |
| | | |
| | | width: 56px; |
| | | height: 56px; |
| | | background: #f7fafc; |
| | | border-radius: 12px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | img { |
| | | width: 100%; |
| | | height: 100%; |
| | | object-fit: contain; |
| | | width: 32px; |
| | | height: 32px; |
| | | } |
| | | } |
| | | |
| | | .stat-content { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | line-height: 1.2; |
| | | color: #718096; |
| | | margin-bottom: 4px; |
| | | } |
| | | |
| | | .stat-value { |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | line-height: 1.2; |
| | | } |
| | | |
| | | .stat-trend { |
| | | font-size: 12px; |
| | | line-height: 1.2; |
| | | |
| | | &.trend-up { |
| | | color: #f56c6c; |
| | | } |
| | | |
| | | &.trend-down { |
| | | color: #67c23a; |
| | | font-size: 20px; |
| | | font-weight: 700; |
| | | color: #2d3748; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 图表行布局 */ |
| | | .charts-row { |
| | | display: grid; |
| | | grid-template-columns: 1fr 1fr; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); |
| | | gap: 24px; |
| | | margin-bottom: 24px; |
| | | } |
| | | |
| | | .chart-card { |
| | | border: 1px solid #e4e7ed; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 20px !important; |
| | | } |
| | | } |
| | | |
| | | .trend-chart-card { |
| | | border: 1px solid #e4e7ed; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 20px !important; |
| | | } |
| | | } |
| | | |
| | | /* 饼图容器 */ |
| | | .pie-chart-container { |
| | | position: relative; |
| | | |
| | | .pie-stats { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 20px; |
| | | margin-top: 20px; |
| | | |
| | | .bar-stat-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 15px; |
| | | background: #f5f7fa; |
| | | border-radius: 6px; |
| | | flex: 1; |
| | | |
| | | .bar-stat-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .bar-stat-value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 柱状图头部统计 */ |
| | | .bar-chart-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | gap: 20px; |
| | | margin-bottom: 20px; |
| | | |
| | | .bar-stat-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 15px; |
| | | background: #f5f7fa; |
| | | border-radius: 6px; |
| | | flex: 1; |
| | | |
| | | .bar-stat-label { |
| | | font-size: 14px; |
| | | color: #666; |
| | | } |
| | | |
| | | .bar-stat-value { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /* 标题样式 */ |
| | | .section-title { |
| | | position: relative; |
| | | font-size: 18px; |
| | | color: #333; |
| | | padding-left: 12px; |
| | | margin-bottom: 20px; |
| | | font-weight: 700; |
| | | |
| | | &::before { |
| | | position: absolute; |
| | | left: 0; |
| | | top: 2px; |
| | | content: ""; |
| | | width: 4px; |
| | | height: 18px; |
| | | background-color: #002fa7; |
| | | border-radius: 2px; |
| | | } |
| | | } |
| | | |
| | | /* 响应式设计 */ |
| | | @media (max-width: 1400px) { |
| | | .stats-cards { |
| | | grid-template-columns: repeat(3, 1fr); |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 1024px) { |
| | | .stats-cards { |
| | | @media (min-width: 1200px) { |
| | | .charts-row { |
| | | grid-template-columns: repeat(2, 1fr); |
| | | } |
| | | } |
| | | |
| | | .charts-row { |
| | | grid-template-columns: 1fr; |
| | | .chart-card, |
| | | .trend-chart-card { |
| | | border-radius: 16px; |
| | | border: none; |
| | | box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05); |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | .header-title { |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #1a202c; |
| | | } |
| | | .el-icon { |
| | | color: #a0aec0; |
| | | cursor: help; |
| | | } |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 640px) { |
| | | .stats-cards { |
| | | grid-template-columns: 1fr; |
| | | .financial-overview-container { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | flex-wrap: nowrap; |
| | | gap: 10px; |
| | | padding: 20px 0; |
| | | width: 100%; |
| | | overflow: hidden; |
| | | |
| | | .overview-item { |
| | | flex: 1; |
| | | min-width: 0; // 允许在 flex 容器中缩写,防止内容撑开 |
| | | display: flex; |
| | | justify-content: center; |
| | | |
| | | .overview-box { |
| | | position: relative; |
| | | width: 100%; |
| | | max-width: 320px; |
| | | height: 110px; |
| | | background: #f8fafc; |
| | | border-radius: 12px; |
| | | padding: 12px 16px; |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 12px; |
| | | overflow: hidden; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | transform: translateY(-5px); |
| | | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .icon-circle { |
| | | flex-shrink: 0; |
| | | width: 42px; |
| | | height: 42px; |
| | | border-radius: 10px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | font-size: 20px; |
| | | z-index: 2; |
| | | } |
| | | |
| | | .data-content { |
| | | z-index: 2; |
| | | min-width: 0; |
| | | .label { |
| | | font-size: 13px; |
| | | color: #718096; |
| | | margin-bottom: 2px; |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | } |
| | | .value { |
| | | font-size: 18px; |
| | | font-weight: 800; |
| | | color: #1a202c; |
| | | line-height: 1.2; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | .unit { |
| | | font-size: 11px; |
| | | color: #a0aec0; |
| | | } |
| | | } |
| | | |
| | | .bg-decoration { |
| | | position: absolute; |
| | | right: -5px; |
| | | bottom: -5px; |
| | | font-size: 32px; |
| | | font-weight: 950; |
| | | color: rgba(0, 0, 0, 0.03); |
| | | font-style: italic; |
| | | user-select: none; |
| | | z-index: 1; |
| | | } |
| | | } |
| | | |
| | | &.income { |
| | | .icon-circle { |
| | | background: #eef2ff; |
| | | color: #4f46e5; |
| | | } |
| | | .overview-box { |
| | | border-left: 5px solid #4f46e5; |
| | | } |
| | | } |
| | | |
| | | &.expense { |
| | | .icon-circle { |
| | | background: #fff7ed; |
| | | color: #f97316; |
| | | } |
| | | .overview-box { |
| | | border-left: 5px solid #f97316; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .profit-indicator { |
| | | flex: 0 40%; // 固定宽度,不参与弹性缩放以保证仪表盘完整 |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | |
| | | .profit-gauge-wrapper { |
| | | position: relative; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | width: 100%; |
| | | // max-width: 180px; |
| | | |
| | | .profit-center-text { |
| | | position: absolute; |
| | | top: 50%; |
| | | left: 50%; |
| | | transform: translate(-50%, -50%); |
| | | text-align: center; |
| | | width: 100%; |
| | | |
| | | .label { |
| | | font-size: 12px; |
| | | color: #718096; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .value { |
| | | font-size: 20px; |
| | | font-weight: 800; |
| | | margin: 2px 0; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | |
| | | &.plus { |
| | | color: #10b981; |
| | | } |
| | | |
| | | &.minus { |
| | | color: #f43f5e; |
| | | } |
| | | } |
| | | |
| | | .rate { |
| | | font-size: 11px; |
| | | color: #a0aec0; |
| | | font-weight: 500; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | // 针对非常窄的屏幕进行整体缩放 |
| | | @media (max-width: 1400px) { |
| | | transform-origin: center; |
| | | // 如果容器太窄,通过缩小内部元素来适应 |
| | | // 这里不使用 transform: scale 因为会影响布局流,改用内部尺寸微调 |
| | | .overview-item .overview-box { |
| | | padding: 10px; |
| | | gap: 8px; |
| | | .value { |
| | | font-size: 16px; |
| | | } |
| | | .icon-circle { |
| | | width: 36px; |
| | | height: 36px; |
| | | font-size: 18px; |
| | | } |
| | | } |
| | | .profit-indicator { |
| | | flex: 0 40%; |
| | | .profit-gauge-wrapper .value { |
| | | font-size: 18px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |