style: unify card styles across calendar, statistics, and budget pages

- 调整 theme.css 中的 --radius-lg 为 12px 以符合设计标准
- 统一日历页面卡片样式(padding, border-radius, shadow)
- 统一统计页面所有卡片组件的样式
- 统一预算页面图表卡片样式,替换硬编码值为 CSS 变量
- 为关键样式添加 fallback 值以防止变量未定义
- 所有卡片现在使用统一的样式变量:
  - padding: var(--spacing-xl, 16px)
  - border-radius: var(--radius-lg, 12px)
  - box-shadow: var(--shadow-sm)
  - background: var(--bg-secondary)
This commit is contained in:
SunCheng
2026-02-18 20:44:58 +08:00
parent c1e2adacea
commit 61aa19b3d2
6 changed files with 491 additions and 186 deletions

View File

@@ -1,8 +1,8 @@
<template>
<!-- 支出分类统计 -->
<div
class="common-card"
style="padding-bottom: 10px"
class="common-card expense-category-card"
style="padding: 12px;"
>
<div class="card-header">
<h3 class="card-title">
@@ -25,6 +25,7 @@
type="doughnut"
:data="chartData"
:options="chartOptions"
:plugins="[pieCenterTextPlugin, pieLabelLinePlugin]"
:loading="false"
@chart:render="onChartRender"
/>
@@ -87,6 +88,7 @@ import { getCssVar } from '@/utils/theme'
import ModernEmpty from '@/components/ModernEmpty.vue'
import BaseChart from '@/components/Charts/BaseChart.vue'
import { useChartTheme } from '@/composables/useChartTheme'
import { pieCenterTextPlugin } from '@/plugins/chartjs-pie-center-plugin'
const props = defineProps({
categories: {
@@ -108,9 +110,105 @@ defineEmits(['category-click'])
const showAllExpense = ref(false)
// Chart.js 相关
const { getChartOptions } = useChartTheme()
const { getChartOptionsByType } = useChartTheme()
let _chartJSInstance = null
// 饼图标签引导线
const pieLabelLinePlugin = {
id: 'pieLabelLine',
afterDraw: (chart) => {
const ctx = chart.ctx
const meta = chart.getDatasetMeta(0)
if (!meta?.data?.length) {return}
const labels = chart.data.labels || []
const centerX = (chart.chartArea.left + chart.chartArea.right) / 2
const lineColor = getCssVar('--van-text-color-2') || '#8a8a8a'
const textColor = getCssVar('--van-text-color') || '#323233'
const strokeColor = getCssVar('--van-background-2') || '#ffffff'
const minSpacing = 12
const labelOffset = 18
const lineOffset = 8
const yPadding = 6
const items = meta.data
.map((arc, index) => {
const label = labels[index]
if (!label) {return null}
const props = arc.getProps(['startAngle', 'endAngle', 'outerRadius', 'x', 'y'], true)
const angle = (props.startAngle + props.endAngle) / 2
const rawX = props.x + Math.cos(angle) * props.outerRadius
const rawY = props.y + Math.sin(angle) * props.outerRadius
const isRight = rawX >= centerX
return {
arc: props,
label,
angle,
isRight,
y: rawY
}
})
.filter(Boolean)
const left = items.filter((item) => !item.isRight).sort((a, b) => a.y - b.y)
const right = items.filter((item) => item.isRight).sort((a, b) => a.y - b.y)
const spread = (list) => {
for (let i = 1; i < list.length; i++) {
if (list[i].y - list[i - 1].y < minSpacing) {
list[i].y = list[i - 1].y + minSpacing
}
}
}
const topLimit = chart.chartArea.top + yPadding
const bottomLimit = chart.chartArea.bottom - yPadding
const clampY = (value) => Math.min(bottomLimit, Math.max(topLimit, value))
spread(left)
spread(right)
left.forEach((item) => { item.y = clampY(item.y) })
right.forEach((item) => { item.y = clampY(item.y) })
ctx.save()
ctx.strokeStyle = lineColor
ctx.lineWidth = 1
ctx.fillStyle = textColor
ctx.textBaseline = 'middle'
ctx.font = 'bold 10px "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif'
const drawItem = (item) => {
const cos = Math.cos(item.angle)
const sin = Math.sin(item.angle)
const startX = item.arc.x + cos * (item.arc.outerRadius + 2)
const startY = item.arc.y + sin * (item.arc.outerRadius + 2)
const midX = item.arc.x + cos * (item.arc.outerRadius + lineOffset)
const midY = item.arc.y + sin * (item.arc.outerRadius + lineOffset)
const endX = item.arc.x + (item.isRight ? 1 : -1) * (item.arc.outerRadius + labelOffset)
const endY = item.y
ctx.strokeStyle = lineColor
ctx.lineWidth = 1
ctx.beginPath()
ctx.moveTo(startX, startY)
ctx.lineTo(midX, midY)
ctx.lineTo(endX, endY)
ctx.stroke()
const textX = endX + (item.isRight ? 6 : -6)
ctx.textAlign = item.isRight ? 'left' : 'right'
ctx.fillStyle = textColor
ctx.fillText(item.label, textX, endY)
}
left.forEach(drawItem)
right.forEach(drawItem)
ctx.restore()
}
}
// 格式化金额
const formatMoney = (value) => {
if (!value && value !== 0) {
@@ -192,21 +290,45 @@ const chartData = computed(() => {
backgroundColor: data.map((item) => item.color),
borderWidth: 2,
borderColor: getCssVar('--van-background-2') || '#fff',
hoverOffset: 4
hoverOffset: 8,
borderRadius: 4,
radius: '88%' // 拉大半径,减少上下留白
}
]
}
})
// 计算总金额
const totalAmount = computed(() => {
return props.totalExpense || 0
})
// Chart.js 配置
const chartOptions = computed(() => {
return getChartOptions({
cutout: '50%',
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark'
return getChartOptionsByType('doughnut', {
cutout: '65%',
layout: {
padding: {
top: 0,
bottom: 0,
left: 2,
right: 2
}
},
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: getCssVar('--van-background-2'),
titleColor: getCssVar('--van-text-color'),
bodyColor: getCssVar('--van-text-color'),
borderColor: getCssVar('--van-border-color'),
borderWidth: 1,
padding: 12,
cornerRadius: 8,
callbacks: {
label: (context) => {
const label = context.label || ''
@@ -216,12 +338,22 @@ const chartOptions = computed(() => {
return `${label}: ¥${formatMoney(value)} (${percentage}%)`
}
}
},
pieCenterText: {
text: `¥${formatMoney(totalAmount.value)}`,
subtext: '总支出',
textColor: isDarkMode ? '#ffffff' : '#323233',
subtextColor: isDarkMode ? '#969799' : '#969799',
fontSize: 24,
subFontSize: 12
},
// 扇区外侧显示分类名称
datalabels: {
display: true
}
},
onClick: (_event, _elements) => {
// 点击饼图扇区时,触发跳转到分类详情
// 注意:这个功能在 BaseChart 中不会自动触发,需要后续完善
}
// 悬停效果增强
hoverOffset: 8
})
})
@@ -236,12 +368,20 @@ const onChartRender = (chart) => {
.common-card {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
border-radius: var(--radius-lg, 12px);
padding: var(--spacing-xl, 16px);
margin-bottom: var(--spacing-xl, 16px);
box-shadow: var(--shadow-sm);
}
.expense-category-card .card-header {
margin-bottom: 0;
}
.expense-category-card .chart-container {
padding-bottom: 0;
}
.card-header {
display: flex;
align-items: center;
@@ -263,8 +403,27 @@ const onChartRender = (chart) => {
.ring-chart {
position: relative;
width: 100%;
height: 200px;
margin: 0 auto;
height: 170px;
margin: 0px auto 0;
overflow: visible;
}
.ring-chart :deep(.chartjs-size-monitor),
.ring-chart :deep(.chartjs-size-monitor-expand),
.ring-chart :deep(.chartjs-size-monitor-shrink) {
display: none !important;
}
.ring-chart :deep(.base-chart) {
height: 100%;
min-height: 0;
align-items: stretch;
justify-content: flex-start;
}
.ring-chart :deep(canvas) {
height: 100% !important;
width: 100% !important;
}
.category-list {
@@ -340,7 +499,8 @@ const onChartRender = (chart) => {
display: flex;
justify-content: center;
align-items: center;
padding-top: 0;
padding-top: 10px;
padding-bottom: 0;
color: var(--van-text-color-3);
font-size: 20px;
cursor: pointer;