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:
@@ -46,9 +46,9 @@
|
|||||||
--spacing-3xl: 24px;
|
--spacing-3xl: 24px;
|
||||||
|
|
||||||
/* 圆角 */
|
/* 圆角 */
|
||||||
--radius-sm: 12px;
|
--radius-sm: 8px;
|
||||||
--radius-md: 16px;
|
--radius-md: 12px;
|
||||||
--radius-lg: 20px;
|
--radius-lg: 12px;
|
||||||
--radius-full: 22px;
|
--radius-full: 22px;
|
||||||
|
|
||||||
/* 字体大小 */
|
/* 字体大小 */
|
||||||
|
|||||||
@@ -26,26 +26,15 @@
|
|||||||
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
|
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
|
||||||
/>
|
/>
|
||||||
<div class="gauge-text-overlay">
|
<div class="gauge-text-overlay">
|
||||||
<div
|
<div class="balance-label">
|
||||||
class="remaining-label"
|
余额
|
||||||
>
|
|
||||||
{{
|
|
||||||
activeTab === BudgetCategory.Expense
|
|
||||||
? (
|
|
||||||
overallStats.month.current > overallStats.month.limit
|
|
||||||
? '超支'
|
|
||||||
: '余额'
|
|
||||||
)
|
|
||||||
: overallStats.month.current > overallStats.month.limit
|
|
||||||
? '超额'
|
|
||||||
: '差额'
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="remaining-value"
|
class="balance-value"
|
||||||
:style="{ color:
|
:style="{
|
||||||
|
color:
|
||||||
overallStats.month.current > overallStats.month.limit
|
overallStats.month.current > overallStats.month.limit
|
||||||
? activeTab === BudgetCategory.Expense ? 'var(--van-danger-color)' : 'var(--van-success-color)'
|
? 'var(--van-danger-color)'
|
||||||
: ''
|
: ''
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@@ -53,17 +42,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gauge-footer compact">
|
<div class="gauge-footer">
|
||||||
<div class="gauge-item">
|
<div class="gauge-item">
|
||||||
<span class="label">
|
<span class="label">已用</span>
|
||||||
{{ activeTab === BudgetCategory.Expense ? '已用' : '已收' }}
|
<span class="value">¥{{ formatMoney(overallStats.month.current) }}</span>
|
||||||
</span>
|
|
||||||
<span class="value expense">¥{{ formatMoney(overallStats.month.current) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gauge-item">
|
<div class="gauge-item">
|
||||||
<span class="label">
|
<span class="label">预算</span>
|
||||||
{{ activeTab === BudgetCategory.Expense ? '预算' : '目标' }}
|
|
||||||
</span>
|
|
||||||
<span class="value">¥{{ formatMoney(overallStats.month.limit) }}</span>
|
<span class="value">¥{{ formatMoney(overallStats.month.limit) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,30 +77,30 @@
|
|||||||
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
|
:style="{ transform: activeTab === BudgetCategory.Expense ? 'scaleX(-1)' : '' }"
|
||||||
/>
|
/>
|
||||||
<div class="gauge-text-overlay">
|
<div class="gauge-text-overlay">
|
||||||
<div
|
<div class="balance-label">
|
||||||
class="remaining-label"
|
余额
|
||||||
>
|
|
||||||
{{ activeTab === BudgetCategory.Expense ? (overallStats.year.current > overallStats.year.limit ? '超支' : '余额') : '差额' }}
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="remaining-value"
|
class="balance-value"
|
||||||
:style="{ color: activeTab === BudgetCategory.Expense && overallStats.year.current > overallStats.year.limit ? 'var(--van-danger-color)' : '' }"
|
:style="{
|
||||||
|
color:
|
||||||
|
activeTab === BudgetCategory.Expense &&
|
||||||
|
overallStats.year.current > overallStats.year.limit
|
||||||
|
? 'var(--van-danger-color)'
|
||||||
|
: ''
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
¥{{ formatMoney(Math.abs(overallStats.year.limit - overallStats.year.current)) }}
|
¥{{ formatMoney(Math.abs(overallStats.year.limit - overallStats.year.current)) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gauge-footer compact">
|
<div class="gauge-footer">
|
||||||
<div class="gauge-item">
|
<div class="gauge-item">
|
||||||
<span class="label">
|
<span class="label">已用</span>
|
||||||
{{ activeTab === BudgetCategory.Expense ? '已用' : '已收' }}
|
<span class="value">¥{{ formatMoney(overallStats.year.current) }}</span>
|
||||||
</span>
|
|
||||||
<span class="value expense">¥{{ formatMoney(overallStats.year.current) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="gauge-item">
|
<div class="gauge-item">
|
||||||
<span class="label">
|
<span class="label">预算</span>
|
||||||
{{ activeTab === BudgetCategory.Expense ? '预算' : '目标' }}
|
|
||||||
</span>
|
|
||||||
<span class="value">¥{{ formatMoney(overallStats.year.limit) }}</span>
|
<span class="value">¥{{ formatMoney(overallStats.year.limit) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,8 +131,7 @@
|
|||||||
<!-- 年度预算进度 -->
|
<!-- 年度预算进度 -->
|
||||||
<div
|
<div
|
||||||
v-if="budgets.length > 0"
|
v-if="budgets.length > 0"
|
||||||
class="chart-card"
|
class="chart-card chart-card-spacing"
|
||||||
style="margin-top: 12px"
|
|
||||||
>
|
>
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<div class="chart-title">
|
<div class="chart-title">
|
||||||
@@ -168,8 +152,7 @@
|
|||||||
<!-- 偏差分析 -->
|
<!-- 偏差分析 -->
|
||||||
<div
|
<div
|
||||||
v-if="budgets.length > 0"
|
v-if="budgets.length > 0"
|
||||||
class="chart-card"
|
class="chart-card chart-card-spacing"
|
||||||
style="margin-top: 12px"
|
|
||||||
>
|
>
|
||||||
<div class="chart-header">
|
<div class="chart-header">
|
||||||
<div class="chart-title">
|
<div class="chart-title">
|
||||||
@@ -207,9 +190,14 @@
|
|||||||
height="70%"
|
height="70%"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="rich-html-content"
|
class="rich-html-content popup-content-padding"
|
||||||
style="padding: 16px"
|
v-html="
|
||||||
v-html="activeDescTab === 'month' ? (overallStats.month?.description || '<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>') : (overallStats.year?.description || '<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>')"
|
activeDescTab === 'month'
|
||||||
|
? overallStats.month?.description ||
|
||||||
|
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>'
|
||||||
|
: overallStats.year?.description ||
|
||||||
|
'<p style=\'text-align:center;color:var(--van-text-color-3)\'>暂无数据</p>'
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
</PopupContainer>
|
</PopupContainer>
|
||||||
</template>
|
</template>
|
||||||
@@ -222,6 +210,10 @@ import PopupContainer from '@/components/PopupContainer.vue'
|
|||||||
import BaseChart from '@/components/Charts/BaseChart.vue'
|
import BaseChart from '@/components/Charts/BaseChart.vue'
|
||||||
import { useChartTheme } from '@/composables/useChartTheme'
|
import { useChartTheme } from '@/composables/useChartTheme'
|
||||||
import { chartjsGaugePlugin } from '@/plugins/chartjs-gauge-plugin'
|
import { chartjsGaugePlugin } from '@/plugins/chartjs-gauge-plugin'
|
||||||
|
import { Chart as ChartJS } from 'chart.js'
|
||||||
|
|
||||||
|
// 注册仪表盘插件
|
||||||
|
ChartJS.register(chartjsGaugePlugin)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
overallStats: {
|
overallStats: {
|
||||||
@@ -247,7 +239,7 @@ const showDescriptionPopup = ref(false)
|
|||||||
const activeDescTab = ref('month')
|
const activeDescTab = ref('month')
|
||||||
|
|
||||||
// Chart.js 相关
|
// Chart.js 相关
|
||||||
const { getChartOptions } = useChartTheme()
|
const { getChartOptions, getChartOptionsByType } = useChartTheme()
|
||||||
|
|
||||||
const formatMoney = (val) => {
|
const formatMoney = (val) => {
|
||||||
if (Math.abs(val) >= 10000) {
|
if (Math.abs(val) >= 10000) {
|
||||||
@@ -264,13 +256,15 @@ const monthGaugeData = computed(() => {
|
|||||||
// 防御性检查:如果数据未加载,返回默认结构
|
// 防御性检查:如果数据未加载,返回默认结构
|
||||||
if (!props.overallStats || !props.overallStats.month) {
|
if (!props.overallStats || !props.overallStats.month) {
|
||||||
return {
|
return {
|
||||||
datasets: [{
|
datasets: [
|
||||||
|
{
|
||||||
data: [0, 100],
|
data: [0, 100],
|
||||||
backgroundColor: [getCssVar('--chart-axis'), getCssVar('--chart-axis')],
|
backgroundColor: [getCssVar('--chart-axis'), getCssVar('--chart-axis')],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
circumference: 180,
|
circumference: 180,
|
||||||
rotation: 270
|
rotation: 270
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,8 +311,8 @@ const monthGaugeData = computed(() => {
|
|||||||
data: [displayRate, remaining],
|
data: [displayRate, remaining],
|
||||||
backgroundColor: [color, getCssVar('--chart-axis')],
|
backgroundColor: [color, getCssVar('--chart-axis')],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
circumference: 180,
|
circumference: 220,
|
||||||
rotation: 270
|
rotation: 250
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -328,14 +322,24 @@ const monthGaugeOptions = computed(() => {
|
|||||||
return {
|
return {
|
||||||
cutout: '75%',
|
cutout: '75%',
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false
|
enabled: false
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
gaugePlugin: {
|
||||||
|
centerText: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { display: false },
|
||||||
|
y: { display: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -345,13 +349,15 @@ const yearGaugeData = computed(() => {
|
|||||||
// 防御性检查:如果数据未加载,返回默认结构
|
// 防御性检查:如果数据未加载,返回默认结构
|
||||||
if (!props.overallStats || !props.overallStats.year) {
|
if (!props.overallStats || !props.overallStats.year) {
|
||||||
return {
|
return {
|
||||||
datasets: [{
|
datasets: [
|
||||||
|
{
|
||||||
data: [0, 100],
|
data: [0, 100],
|
||||||
backgroundColor: [getCssVar('--chart-axis'), getCssVar('--chart-axis')],
|
backgroundColor: [getCssVar('--chart-axis'), getCssVar('--chart-axis')],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
circumference: 180,
|
circumference: 180,
|
||||||
rotation: 270
|
rotation: 270
|
||||||
}]
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,8 +403,8 @@ const yearGaugeData = computed(() => {
|
|||||||
data: [displayRate, remaining],
|
data: [displayRate, remaining],
|
||||||
backgroundColor: [color, getCssVar('--chart-axis')],
|
backgroundColor: [color, getCssVar('--chart-axis')],
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
circumference: 180,
|
circumference: 220,
|
||||||
rotation: 270
|
rotation: 250
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -408,20 +414,32 @@ const yearGaugeOptions = computed(() => {
|
|||||||
return {
|
return {
|
||||||
cutout: '75%',
|
cutout: '75%',
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: true,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: false
|
enabled: false
|
||||||
|
},
|
||||||
|
datalabels: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
gaugePlugin: {
|
||||||
|
centerText: false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { display: false },
|
||||||
|
y: { display: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const calculateChartHeight = (budgets) => {
|
const calculateChartHeight = (budgets) => {
|
||||||
if (!budgets) { return 100 }
|
if (!budgets) {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
const dataCount = budgets.length
|
const dataCount = budgets.length
|
||||||
const minHeight = 100
|
const minHeight = 100
|
||||||
const heightPerItem = 30
|
const heightPerItem = 30
|
||||||
@@ -435,7 +453,7 @@ const varianceChartData = computed(() => {
|
|||||||
return { labels: [], datasets: [] }
|
return { labels: [], datasets: [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = props.budgets.map(b => {
|
const data = props.budgets.map((b) => {
|
||||||
const limit = b.limit || 0
|
const limit = b.limit || 0
|
||||||
const current = b.current || 0
|
const current = b.current || 0
|
||||||
const diff = current - limit
|
const diff = current - limit
|
||||||
@@ -448,8 +466,8 @@ const varianceChartData = computed(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const monthlyData = data.filter(item => item.type === BudgetPeriodType.Month)
|
const monthlyData = data.filter((item) => item.type === BudgetPeriodType.Month)
|
||||||
const annualData = data.filter(item => item.type === BudgetPeriodType.Year)
|
const annualData = data.filter((item) => item.type === BudgetPeriodType.Year)
|
||||||
|
|
||||||
monthlyData.sort((a, b) => Math.abs(b.value) - Math.abs(a.value))
|
monthlyData.sort((a, b) => Math.abs(b.value) - Math.abs(a.value))
|
||||||
annualData.sort((a, b) => Math.abs(b.value) - Math.abs(a.value))
|
annualData.sort((a, b) => Math.abs(b.value) - Math.abs(a.value))
|
||||||
@@ -457,16 +475,20 @@ const varianceChartData = computed(() => {
|
|||||||
const sortedData = [...annualData, ...monthlyData]
|
const sortedData = [...annualData, ...monthlyData]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: sortedData.map(item => item.name),
|
labels: sortedData.map((item) => item.name),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: '偏差',
|
label: '偏差',
|
||||||
data: sortedData.map(item => item.value),
|
data: sortedData.map((item) => item.value),
|
||||||
backgroundColor: sortedData.map(item => {
|
backgroundColor: sortedData.map((item) => {
|
||||||
if (props.activeTab === BudgetCategory.Expense) {
|
if (props.activeTab === BudgetCategory.Expense) {
|
||||||
return item.value > 0 ? getCssVar('--van-danger-color') : getCssVar('--van-success-color')
|
return item.value > 0
|
||||||
|
? getCssVar('--van-danger-color')
|
||||||
|
: getCssVar('--van-success-color')
|
||||||
} else {
|
} else {
|
||||||
return item.value > 0 ? getCssVar('--van-success-color') : getCssVar('--van-danger-color')
|
return item.value > 0
|
||||||
|
? getCssVar('--van-success-color')
|
||||||
|
: getCssVar('--van-danger-color')
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
@@ -478,17 +500,21 @@ const varianceChartData = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const varianceChartOptions = computed(() => {
|
const varianceChartOptions = computed(() => {
|
||||||
return getChartOptions({
|
return getChartOptionsByType('bar', {
|
||||||
indexAxis: 'y',
|
indexAxis: 'y',
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
padding: 12,
|
||||||
|
cornerRadius: 8,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const item = context.dataset._meta[context.dataIndex]
|
const item = context.dataset._meta[context.dataIndex]
|
||||||
const diffText = item.value > 0
|
const diffText =
|
||||||
|
item.value > 0
|
||||||
? `超支: ¥${formatMoney(item.value)}`
|
? `超支: ¥${formatMoney(item.value)}`
|
||||||
: `结余: ¥${formatMoney(Math.abs(item.value))}`
|
: `结余: ¥${formatMoney(Math.abs(item.value))}`
|
||||||
return [
|
return [
|
||||||
@@ -505,8 +531,15 @@ const varianceChartOptions = computed(() => {
|
|||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
autoSkip: false,
|
autoSkip: false,
|
||||||
|
font: {
|
||||||
|
family: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
size: 11
|
||||||
|
},
|
||||||
callback: function (value, index) {
|
callback: function (value, index) {
|
||||||
const label = this.getLabelForValue(index)
|
const label = this.getLabelForValue(index)
|
||||||
return label.length > 10 ? label.substring(0, 10) + '...' : label
|
return label.length > 10 ? label.substring(0, 10) + '...' : label
|
||||||
@@ -564,7 +597,7 @@ const burndownChartData = computed(() => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (i <= currentDay && totalBudget > 0) {
|
if (i <= currentDay && totalBudget > 0) {
|
||||||
const actualRemaining = totalBudget - (currentExpense * i / currentDay)
|
const actualRemaining = totalBudget - (currentExpense * i) / currentDay
|
||||||
actualBurndown.push(Math.round(actualRemaining))
|
actualBurndown.push(Math.round(actualRemaining))
|
||||||
} else {
|
} else {
|
||||||
actualBurndown.push(null)
|
actualBurndown.push(null)
|
||||||
@@ -583,7 +616,7 @@ const burndownChartData = computed(() => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (i <= currentDay && totalBudget > 0) {
|
if (i <= currentDay && totalBudget > 0) {
|
||||||
const actualAccumulated = Math.min(totalBudget, currentExpense * i / currentDay)
|
const actualAccumulated = Math.min(totalBudget, (currentExpense * i) / currentDay)
|
||||||
actualBurndown.push(Math.round(actualAccumulated))
|
actualBurndown.push(Math.round(actualAccumulated))
|
||||||
} else {
|
} else {
|
||||||
actualBurndown.push(null)
|
actualBurndown.push(null)
|
||||||
@@ -622,13 +655,21 @@ const burndownChartData = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const burndownChartOptions = computed(() => {
|
const burndownChartOptions = computed(() => {
|
||||||
return getChartOptions({
|
return getChartOptionsByType('line', {
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
position: 'top'
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: 'line',
|
||||||
|
boxWidth: 20
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
padding: 12,
|
||||||
|
cornerRadius: 8,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const value = context.parsed.y
|
const value = context.parsed.y
|
||||||
@@ -641,6 +682,18 @@ const burndownChartOptions = computed(() => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
maxTicksLimit: 10,
|
||||||
|
font: {
|
||||||
|
family: '"PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif',
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => {
|
callback: (value) => {
|
||||||
@@ -648,6 +701,9 @@ const burndownChartOptions = computed(() => {
|
|||||||
return (value / 10000).toFixed(0) + 'w'
|
return (value / 10000).toFixed(0) + 'w'
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -700,7 +756,7 @@ const yearBurndownChartData = computed(() => {
|
|||||||
const isFuture = year > currentYear || (year === currentYear && i > currentMonth)
|
const isFuture = year > currentYear || (year === currentYear && i > currentMonth)
|
||||||
if (!isFuture && totalBudget > 0) {
|
if (!isFuture && totalBudget > 0) {
|
||||||
const progress = (i + 1) / 12
|
const progress = (i + 1) / 12
|
||||||
const actualRemaining = totalBudget - (currentExpense * progress)
|
const actualRemaining = totalBudget - currentExpense * progress
|
||||||
actualBurndown.push(Math.round(actualRemaining))
|
actualBurndown.push(Math.round(actualRemaining))
|
||||||
} else {
|
} else {
|
||||||
actualBurndown.push(null)
|
actualBurndown.push(null)
|
||||||
@@ -760,13 +816,21 @@ const yearBurndownChartData = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const yearBurndownChartOptions = computed(() => {
|
const yearBurndownChartOptions = computed(() => {
|
||||||
return getChartOptions({
|
return getChartOptionsByType('line', {
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
position: 'top'
|
position: 'top',
|
||||||
|
labels: {
|
||||||
|
usePointStyle: true,
|
||||||
|
pointStyle: 'line',
|
||||||
|
boxWidth: 20
|
||||||
|
}
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
padding: 12,
|
||||||
|
cornerRadius: 8,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const value = context.parsed.y
|
const value = context.parsed.y
|
||||||
@@ -779,6 +843,16 @@ const yearBurndownChartOptions = computed(() => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
y: {
|
y: {
|
||||||
ticks: {
|
ticks: {
|
||||||
callback: (value) => {
|
callback: (value) => {
|
||||||
@@ -786,6 +860,9 @@ const yearBurndownChartOptions = computed(() => {
|
|||||||
return (value / 10000).toFixed(0) + 'w'
|
return (value / 10000).toFixed(0) + 'w'
|
||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
size: 10
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -796,65 +873,77 @@ const yearBurndownChartOptions = computed(() => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.chart-analysis-container {
|
.chart-analysis-container {
|
||||||
padding: 12px;
|
padding: var(--spacing-lg, 12px);
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauges-row {
|
.gauges-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: var(--spacing-xl, 16px);
|
||||||
margin-bottom: 12px;
|
margin-bottom: var(--spacing-xl, 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card {
|
.chart-card {
|
||||||
background: var(--van-background-2);
|
background: var(--van-background-2);
|
||||||
border-radius: 12px;
|
border-radius: var(--radius-lg, 12px);
|
||||||
padding: 16px;
|
padding: var(--spacing-xl, 16px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
box-shadow: var(--shadow-sm, 0 2px 8px rgba(0, 0, 0, 0.04));
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-card {
|
.gauge-card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
/* 防止 flex 子项溢出 */
|
/* 防止 flex 子项溢出 */
|
||||||
padding: 12px;
|
padding: var(--spacing-lg, 12px);
|
||||||
/* 减小内边距 */
|
/* 减小内边距 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-card-spacing {
|
||||||
|
margin-top: var(--spacing-lg, 12px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content-padding {
|
||||||
|
padding: var(--spacing-xl, 16px);
|
||||||
|
}
|
||||||
|
|
||||||
.gauge-wrapper {
|
.gauge-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 120px;
|
height: 120px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-text-overlay {
|
.gauge-text-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translate(-50%, -50%);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.remaining-value {
|
.balance-label {
|
||||||
font-size: 24px;
|
font-size: 11px;
|
||||||
font-weight: bold;
|
|
||||||
font-family: DIN Alternate, system-ui;
|
|
||||||
color: var(--van-text-color);
|
|
||||||
line-height: 1;
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remaining-label {
|
|
||||||
font-size: 10px;
|
|
||||||
color: var(--van-text-color-2);
|
color: var(--van-text-color-2);
|
||||||
margin-top: 4px;
|
letter-spacing: 1px;
|
||||||
font-family: system-ui;
|
}
|
||||||
transform-origin: center;
|
|
||||||
|
.balance-value {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family:
|
||||||
|
DIN Alternate,
|
||||||
|
system-ui;
|
||||||
|
color: var(--van-text-color);
|
||||||
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-header {
|
.chart-header {
|
||||||
@@ -896,37 +985,33 @@ const yearBurndownChartOptions = computed(() => {
|
|||||||
|
|
||||||
.gauge-footer {
|
.gauge-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-between;
|
||||||
/* 分散对齐 */
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: -20px;
|
padding: 0 8px 6px;
|
||||||
position: relative;
|
margin-top: -6px;
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-item {
|
.gauge-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-item .label {
|
.gauge-item .label {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: var(--van-text-color-2);
|
color: var(--van-text-color-2);
|
||||||
transform: scale(0.9);
|
margin-bottom: 4px;
|
||||||
/* 视觉上更小 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-item .value {
|
.gauge-item .value {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-family: DIN Alternate, system-ui;
|
font-family:
|
||||||
|
DIN Alternate,
|
||||||
|
system-ui;
|
||||||
color: var(--van-text-color);
|
color: var(--van-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-item .value.expense {
|
|
||||||
color: var(--van-primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* expand styles removed as they are no longer used */
|
/* expand styles removed as they are no longer used */
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -114,9 +114,9 @@ const selectedDateFormatted = computed(() => {
|
|||||||
.daily-stats {
|
.daily-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl, 16px);
|
||||||
padding: var(--spacing-3xl);
|
padding: var(--spacing-xl, 16px);
|
||||||
padding-top: 8px;
|
padding-top: var(--spacing-md, 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-header {
|
.stats-header {
|
||||||
@@ -137,9 +137,10 @@ const selectedDateFormatted = computed(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--spacing-lg);
|
gap: var(--spacing-lg);
|
||||||
padding: var(--spacing-2xl);
|
padding: var(--spacing-xl, 16px);
|
||||||
background-color: var(--bg-secondary);
|
background-color: var(--bg-secondary);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg, 12px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats-dual-row {
|
.stats-dual-row {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- 支出分类统计 -->
|
<!-- 支出分类统计 -->
|
||||||
<div
|
<div
|
||||||
class="common-card"
|
class="common-card expense-category-card"
|
||||||
style="padding-bottom: 10px"
|
style="padding: 12px;"
|
||||||
>
|
>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">
|
<h3 class="card-title">
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
type="doughnut"
|
type="doughnut"
|
||||||
:data="chartData"
|
:data="chartData"
|
||||||
:options="chartOptions"
|
:options="chartOptions"
|
||||||
|
:plugins="[pieCenterTextPlugin, pieLabelLinePlugin]"
|
||||||
:loading="false"
|
:loading="false"
|
||||||
@chart:render="onChartRender"
|
@chart:render="onChartRender"
|
||||||
/>
|
/>
|
||||||
@@ -87,6 +88,7 @@ import { getCssVar } from '@/utils/theme'
|
|||||||
import ModernEmpty from '@/components/ModernEmpty.vue'
|
import ModernEmpty from '@/components/ModernEmpty.vue'
|
||||||
import BaseChart from '@/components/Charts/BaseChart.vue'
|
import BaseChart from '@/components/Charts/BaseChart.vue'
|
||||||
import { useChartTheme } from '@/composables/useChartTheme'
|
import { useChartTheme } from '@/composables/useChartTheme'
|
||||||
|
import { pieCenterTextPlugin } from '@/plugins/chartjs-pie-center-plugin'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
categories: {
|
categories: {
|
||||||
@@ -108,9 +110,105 @@ defineEmits(['category-click'])
|
|||||||
const showAllExpense = ref(false)
|
const showAllExpense = ref(false)
|
||||||
|
|
||||||
// Chart.js 相关
|
// Chart.js 相关
|
||||||
const { getChartOptions } = useChartTheme()
|
const { getChartOptionsByType } = useChartTheme()
|
||||||
let _chartJSInstance = null
|
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) => {
|
const formatMoney = (value) => {
|
||||||
if (!value && value !== 0) {
|
if (!value && value !== 0) {
|
||||||
@@ -192,21 +290,45 @@ const chartData = computed(() => {
|
|||||||
backgroundColor: data.map((item) => item.color),
|
backgroundColor: data.map((item) => item.color),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
borderColor: getCssVar('--van-background-2') || '#fff',
|
borderColor: getCssVar('--van-background-2') || '#fff',
|
||||||
hoverOffset: 4
|
hoverOffset: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
radius: '88%' // 拉大半径,减少上下留白
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算总金额
|
||||||
|
const totalAmount = computed(() => {
|
||||||
|
return props.totalExpense || 0
|
||||||
|
})
|
||||||
|
|
||||||
// Chart.js 配置
|
// Chart.js 配置
|
||||||
const chartOptions = computed(() => {
|
const chartOptions = computed(() => {
|
||||||
return getChartOptions({
|
const isDarkMode = document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
cutout: '50%',
|
|
||||||
|
return getChartOptionsByType('doughnut', {
|
||||||
|
cutout: '65%',
|
||||||
|
layout: {
|
||||||
|
padding: {
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: 2,
|
||||||
|
right: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
tooltip: {
|
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: {
|
callbacks: {
|
||||||
label: (context) => {
|
label: (context) => {
|
||||||
const label = context.label || ''
|
const label = context.label || ''
|
||||||
@@ -216,12 +338,22 @@ const chartOptions = computed(() => {
|
|||||||
return `${label}: ¥${formatMoney(value)} (${percentage}%)`
|
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) => {
|
// 悬停效果增强
|
||||||
// 点击饼图扇区时,触发跳转到分类详情
|
hoverOffset: 8
|
||||||
// 注意:这个功能在 BaseChart 中不会自动触发,需要后续完善
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -236,12 +368,20 @@ const onChartRender = (chart) => {
|
|||||||
|
|
||||||
.common-card {
|
.common-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg, 12px);
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl, 16px);
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl, 16px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.expense-category-card .card-header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.expense-category-card .chart-container {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -263,8 +403,27 @@ const onChartRender = (chart) => {
|
|||||||
.ring-chart {
|
.ring-chart {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 170px;
|
||||||
margin: 0 auto;
|
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 {
|
.category-list {
|
||||||
@@ -340,7 +499,8 @@ const onChartRender = (chart) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 0;
|
padding-top: 10px;
|
||||||
|
padding-bottom: 0;
|
||||||
color: var(--van-text-color-3);
|
color: var(--van-text-color-3);
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -156,9 +156,9 @@ const noneCategories = computed(() => {
|
|||||||
// 通用卡片样式
|
// 通用卡片样式
|
||||||
.common-card {
|
.common-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg, 12px);
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl, 16px);
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl, 16px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,15 +179,15 @@ const noneCategories = computed(() => {
|
|||||||
/* 并列显示卡片 */
|
/* 并列显示卡片 */
|
||||||
.side-by-side-cards {
|
.side-by-side-cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: var(--spacing-lg, 12px);
|
||||||
margin: 0 12px 16px;
|
margin: 0 var(--spacing-lg, 12px) var(--spacing-xl, 16px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-by-side-cards .common-card {
|
.side-by-side-cards .common-card {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0; /* 允许内部元素缩小 */
|
min-width: 0; /* 允许内部元素缩小 */
|
||||||
padding: 12px;
|
padding: var(--spacing-lg, 12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Chart.js 相关
|
// Chart.js 相关
|
||||||
const { getChartOptions } = useChartTheme()
|
const { getChartOptionsByType, colors } = useChartTheme()
|
||||||
|
|
||||||
// 计算结余样式类
|
// 计算结余样式类
|
||||||
const balanceClass = computed(() => ({
|
const balanceClass = computed(() => ({
|
||||||
@@ -99,7 +99,34 @@ const prepareChartData = () => {
|
|||||||
let xAxisLabels = []
|
let xAxisLabels = []
|
||||||
|
|
||||||
if (props.period === 'week') {
|
if (props.period === 'week') {
|
||||||
chartData = [...props.trendData].sort((a, b) => new Date(a.date) - new Date(b.date))
|
// 获取当前周的日期范围
|
||||||
|
const current = props.currentDate
|
||||||
|
const weekStart = new Date(current)
|
||||||
|
const day = weekStart.getDay()
|
||||||
|
const diff = weekStart.getDate() - day + (day === 0 ? -6 : 1) // 调整为周一开始
|
||||||
|
weekStart.setDate(diff)
|
||||||
|
weekStart.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
today.setHours(0, 0, 0, 0)
|
||||||
|
|
||||||
|
// 判断是否在当前周
|
||||||
|
const weekEnd = new Date(weekStart)
|
||||||
|
weekEnd.setDate(weekStart.getDate() + 6)
|
||||||
|
const isCurrentWeek = today >= weekStart && today <= weekEnd
|
||||||
|
|
||||||
|
// 过滤到当前日期(如果是当前周)
|
||||||
|
chartData = [...props.trendData]
|
||||||
|
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
||||||
|
.filter((item) => {
|
||||||
|
const itemDate = new Date(item.date)
|
||||||
|
itemDate.setHours(0, 0, 0, 0)
|
||||||
|
if (isCurrentWeek) {
|
||||||
|
return itemDate <= today
|
||||||
|
}
|
||||||
|
return true // 历史周显示完整数据
|
||||||
|
})
|
||||||
|
|
||||||
xAxisLabels = chartData.map((item) => {
|
xAxisLabels = chartData.map((item) => {
|
||||||
const date = new Date(item.date)
|
const date = new Date(item.date)
|
||||||
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
@@ -111,6 +138,11 @@ const prepareChartData = () => {
|
|||||||
const month = currentDate.getMonth() + 1
|
const month = currentDate.getMonth() + 1
|
||||||
const daysInMonth = getDaysInMonth(year, month)
|
const daysInMonth = getDaysInMonth(year, month)
|
||||||
|
|
||||||
|
// 获取今天的日期
|
||||||
|
const today = new Date()
|
||||||
|
const isCurrentMonth = today.getFullYear() === year && today.getMonth() + 1 === month
|
||||||
|
const currentDay = isCurrentMonth ? today.getDate() : daysInMonth
|
||||||
|
|
||||||
const allDays = Array.from({ length: daysInMonth }, (_, i) => {
|
const allDays = Array.from({ length: daysInMonth }, (_, i) => {
|
||||||
const day = i + 1
|
const day = i + 1
|
||||||
const paddedDay = day.toString().padStart(2, '0')
|
const paddedDay = day.toString().padStart(2, '0')
|
||||||
@@ -124,7 +156,11 @@ const prepareChartData = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
chartData = allDays.map((date) => {
|
// 只获取到当前日期的数据(历史月份则展示完整月份)
|
||||||
|
const daysToShow = isCurrentMonth ? currentDay : daysInMonth
|
||||||
|
const daysToDisplay = allDays.slice(0, daysToShow)
|
||||||
|
|
||||||
|
chartData = daysToDisplay.map((date) => {
|
||||||
const dayData = dataMap.get(date)
|
const dayData = dataMap.get(date)
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
@@ -136,9 +172,21 @@ const prepareChartData = () => {
|
|||||||
|
|
||||||
xAxisLabels = chartData.map((_, index) => (index + 1).toString())
|
xAxisLabels = chartData.map((_, index) => (index + 1).toString())
|
||||||
} else if (props.period === 'year') {
|
} else if (props.period === 'year') {
|
||||||
|
const currentYear = props.currentDate.getFullYear()
|
||||||
|
const today = new Date()
|
||||||
|
const isCurrentYear = today.getFullYear() === currentYear
|
||||||
|
const currentMonth = isCurrentYear ? today.getMonth() + 1 : 12
|
||||||
|
|
||||||
|
// 过滤到当前月份(如果是当前年)
|
||||||
chartData = [...props.trendData]
|
chartData = [...props.trendData]
|
||||||
.filter((item) => item && item.date)
|
.filter((item) => item && item.date)
|
||||||
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
.sort((a, b) => new Date(a.date) - new Date(b.date))
|
||||||
|
.filter((item) => {
|
||||||
|
const itemDate = new Date(item.date)
|
||||||
|
const itemMonth = itemDate.getMonth() + 1
|
||||||
|
return itemMonth <= currentMonth
|
||||||
|
})
|
||||||
|
|
||||||
xAxisLabels = chartData.map((item) => {
|
xAxisLabels = chartData.map((item) => {
|
||||||
const date = new Date(item.date)
|
const date = new Date(item.date)
|
||||||
return `${date.getMonth() + 1}月`
|
return `${date.getMonth() + 1}月`
|
||||||
@@ -176,6 +224,10 @@ const prepareChartData = () => {
|
|||||||
return { chartData, xAxisLabels, expenseData, incomeData }
|
return { chartData, xAxisLabels, expenseData, incomeData }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 使用主题颜色
|
||||||
|
const expenseColor = computed(() => colors.value.danger)
|
||||||
|
const incomeColor = computed(() => colors.value.success)
|
||||||
|
|
||||||
// Chart.js 数据
|
// Chart.js 数据
|
||||||
const chartData = computed(() => {
|
const chartData = computed(() => {
|
||||||
const { xAxisLabels, expenseData, incomeData } = prepareChartData()
|
const { xAxisLabels, expenseData, incomeData } = prepareChartData()
|
||||||
@@ -186,33 +238,39 @@ const chartData = computed(() => {
|
|||||||
{
|
{
|
||||||
label: '支出',
|
label: '支出',
|
||||||
data: expenseData,
|
data: expenseData,
|
||||||
borderColor: '#ff6b6b',
|
borderColor: expenseColor.value,
|
||||||
backgroundColor: (context) => {
|
backgroundColor: (context) => {
|
||||||
const chart = context.chart
|
const chart = context.chart
|
||||||
const { ctx, chartArea } = chart
|
const { ctx, chartArea } = chart
|
||||||
if (!chartArea) {return 'rgba(255, 107, 107, 0.1)'}
|
if (!chartArea) {
|
||||||
return createGradient(ctx, chartArea, '#ff6b6b')
|
return 'rgba(255, 107, 107, 0.1)'
|
||||||
|
}
|
||||||
|
return createGradient(ctx, chartArea, expenseColor.value)
|
||||||
},
|
},
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
pointHoverRadius: 4,
|
pointHoverRadius: 6,
|
||||||
|
hitRadius: 20,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '收入',
|
label: '收入',
|
||||||
data: incomeData,
|
data: incomeData,
|
||||||
borderColor: '#4ade80',
|
borderColor: incomeColor.value,
|
||||||
backgroundColor: (context) => {
|
backgroundColor: (context) => {
|
||||||
const chart = context.chart
|
const chart = context.chart
|
||||||
const { ctx, chartArea } = chart
|
const { ctx, chartArea } = chart
|
||||||
if (!chartArea) {return 'rgba(74, 222, 128, 0.1)'}
|
if (!chartArea) {
|
||||||
return createGradient(ctx, chartArea, '#4ade80')
|
return 'rgba(74, 222, 128, 0.1)'
|
||||||
|
}
|
||||||
|
return createGradient(ctx, chartArea, incomeColor.value)
|
||||||
},
|
},
|
||||||
fill: true,
|
fill: true,
|
||||||
tension: 0.4,
|
tension: 0.4,
|
||||||
pointRadius: 0,
|
pointRadius: 0,
|
||||||
pointHoverRadius: 4,
|
pointHoverRadius: 6,
|
||||||
|
hitRadius: 20,
|
||||||
borderWidth: 2
|
borderWidth: 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -223,24 +281,23 @@ const chartData = computed(() => {
|
|||||||
const chartOptions = computed(() => {
|
const chartOptions = computed(() => {
|
||||||
const { chartData: rawData } = prepareChartData()
|
const { chartData: rawData } = prepareChartData()
|
||||||
|
|
||||||
return getChartOptions({
|
return getChartOptionsByType('line', {
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: { display: false },
|
||||||
display: false
|
y: { display: false }
|
||||||
},
|
|
||||||
y: {
|
|
||||||
display: false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: { display: false },
|
||||||
display: false
|
|
||||||
},
|
|
||||||
tooltip: {
|
tooltip: {
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
padding: 12,
|
||||||
|
cornerRadius: 8,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: (context) => {
|
title: (context) => {
|
||||||
const index = context[0].dataIndex
|
const index = context[0].dataIndex
|
||||||
if (!rawData[index]) {return ''}
|
if (!rawData[index]) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
const date = rawData[index].date
|
const date = rawData[index].date
|
||||||
if (props.period === 'week') {
|
if (props.period === 'week') {
|
||||||
@@ -262,7 +319,9 @@ const chartOptions = computed(() => {
|
|||||||
label: (context) => {
|
label: (context) => {
|
||||||
const index = context.dataIndex
|
const index = context.dataIndex
|
||||||
const item = rawData[index]
|
const item = rawData[index]
|
||||||
if (!item) {return ''}
|
if (!item) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
let dailyExpense = 0
|
let dailyExpense = 0
|
||||||
let dailyIncome = 0
|
let dailyIncome = 0
|
||||||
@@ -280,16 +339,14 @@ const chartOptions = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const value = context.dataset.label === '支出' ? dailyExpense : dailyIncome
|
const value = context.dataset.label === '支出' ? dailyExpense : dailyIncome
|
||||||
if (value === 0) {return null}
|
if (value === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return `${context.dataset.label}: ¥${value.toFixed(2)}`
|
return `${context.dataset.label}: ¥${value.toFixed(2)}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
interaction: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -300,9 +357,9 @@ const chartOptions = computed(() => {
|
|||||||
|
|
||||||
.common-card {
|
.common-card {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg, 12px);
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl, 16px);
|
||||||
margin-bottom: var(--spacing-xl);
|
margin-bottom: var(--spacing-xl, 16px);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,6 +426,8 @@ const chartOptions = computed(() => {
|
|||||||
|
|
||||||
.trend-chart {
|
.trend-chart {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 180px;
|
height: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user