fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 24s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 3s
This commit is contained in:
@@ -601,10 +601,8 @@ public class BudgetSavingsService(
|
||||
预算收入合计:
|
||||
<span class='expense-value'>
|
||||
<strong>
|
||||
{
|
||||
currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyIncomeItems.Sum(i => i.limit)
|
||||
:N0}
|
||||
{currentMonthlyIncomeItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyIncomeItems.Sum(i => i.limit):N0}
|
||||
</strong>
|
||||
</span>
|
||||
</p>
|
||||
@@ -714,10 +712,8 @@ public class BudgetSavingsService(
|
||||
支出预算合计:
|
||||
<span class='expense-value'>
|
||||
<strong>
|
||||
{
|
||||
currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyExpenseItems.Sum(i => i.limit)
|
||||
:N0}
|
||||
{currentMonthlyExpenseItems.Sum(i => i.limit * i.factor)
|
||||
+ currentYearlyExpenseItems.Sum(i => i.limit):N0}
|
||||
</strong>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
@@ -82,6 +82,7 @@ export const createTransaction = (data) => {
|
||||
* @param {number} data.balance - 交易后余额
|
||||
* @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
|
||||
* @param {string} data.classify - 交易分类
|
||||
* @param {string} [data.occurredAt] - 交易时间
|
||||
* @returns {Promise<{success: boolean}>}
|
||||
*/
|
||||
export const updateTransaction = (data) => {
|
||||
|
||||
184
Web/src/assets/theme.css
Normal file
184
Web/src/assets/theme.css
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* EmailBill 主题系统 - 根据 v2.pen 设计稿
|
||||
* 用于保持整个应用色彩和布局一致性
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ============ 颜色变量 - 浅色主题 ============ */
|
||||
|
||||
/* 背景色 */
|
||||
--bg-primary: #FFFFFF;
|
||||
--bg-secondary: #F6F7F8;
|
||||
--bg-tertiary: #F3F4F6;
|
||||
--bg-button: #F5F5F5;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #1A1A1A;
|
||||
--text-secondary: #6B7280;
|
||||
--text-tertiary: #9CA3AF;
|
||||
|
||||
/* 强调色 */
|
||||
--accent-primary: #FF6B6B;
|
||||
--accent-danger: #EF4444;
|
||||
--accent-warning: #D97706;
|
||||
--accent-warning-bg: #FFFBEB;
|
||||
--accent-success: #22C55E;
|
||||
--accent-success-bg: #F0FDF4;
|
||||
--accent-info: #6366F1;
|
||||
--accent-info-bg: #E0E7FF;
|
||||
|
||||
/* 图标色 */
|
||||
--icon-star: #FF6B6B;
|
||||
--icon-coffee: #FCD34D;
|
||||
|
||||
/* ============ 布局变量 ============ */
|
||||
|
||||
/* 间距 */
|
||||
--spacing-xs: 2px;
|
||||
--spacing-sm: 4px;
|
||||
--spacing-md: 8px
|
||||
--spacing-lg: 12px;
|
||||
--spacing-xl: 16px;
|
||||
--spacing-2xl: 20px;
|
||||
--spacing-3xl: 24px;
|
||||
|
||||
/* 圆角 */
|
||||
--radius-sm: 12px;
|
||||
--radius-md: 16px;
|
||||
--radius-lg: 20px;
|
||||
--radius-full: 22px;
|
||||
|
||||
/* 字体大小 */
|
||||
--font-xs: 9px;
|
||||
--font-sm: 11px;
|
||||
--font-base: 12px;
|
||||
--font-md: 13px;
|
||||
--font-lg: 15px;
|
||||
--font-xl: 18px;
|
||||
--font-2xl: 24px;
|
||||
--font-3xl: 32px;
|
||||
|
||||
/* 字体粗细 */
|
||||
--font-medium: 500;
|
||||
--font-semibold: 600;
|
||||
--font-bold: 700;
|
||||
--font-extrabold: 800;
|
||||
|
||||
/* 字体 */
|
||||
--font-primary: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-display: 'Bricolage Grotesque', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
|
||||
/* 阴影 (可选) */
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* ============ 深色主题 ============ */
|
||||
[data-theme="dark"] {
|
||||
/* 背景色 */
|
||||
--bg-primary: #09090B;
|
||||
--bg-secondary: #18181b;
|
||||
--bg-tertiary: #27272a;
|
||||
--bg-button: #27272a;
|
||||
|
||||
/* 文字颜色 */
|
||||
--text-primary: #f4f4f5;
|
||||
--text-secondary: #a1a1aa;
|
||||
--text-tertiary: #71717a;
|
||||
|
||||
/* 强调色 (深色主题调整) */
|
||||
--accent-primary: #FF6B6B;
|
||||
--accent-danger: #f87171;
|
||||
--accent-warning: #fbbf24;
|
||||
--accent-warning-bg: #451a03;
|
||||
--accent-success: #4ade80;
|
||||
--accent-success-bg: #064e3b;
|
||||
--accent-info: #818cf8;
|
||||
--accent-info-bg: #312e81;
|
||||
|
||||
/* 图标色 (深色主题) */
|
||||
--icon-star: #FF6B6B;
|
||||
--icon-coffee: #FCD34D;
|
||||
}
|
||||
|
||||
/* ============ 通用工具类 ============ */
|
||||
|
||||
/* 文字 */
|
||||
.text-primary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.text-tertiary {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: var(--accent-danger);
|
||||
}
|
||||
|
||||
/* 背景 */
|
||||
.bg-primary {
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
.bg-secondary {
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.bg-tertiary {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
/* 布局容器 */
|
||||
.container-fluid {
|
||||
width: 100%;
|
||||
max-width: 402px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Flex 布局 */
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 间距 */
|
||||
.gap-xs { gap: var(--spacing-xs); }
|
||||
.gap-sm { gap: var(--spacing-sm); }
|
||||
.gap-md { gap: var(--spacing-md); }
|
||||
.gap-lg { gap: var(--spacing-lg); }
|
||||
.gap-xl { gap: var(--spacing-xl); }
|
||||
.gap-2xl { gap: var(--spacing-2xl); }
|
||||
.gap-3xl { gap: var(--spacing-3xl); }
|
||||
|
||||
/* 内边距 */
|
||||
.p-sm { padding: var(--spacing-md); }
|
||||
.p-md { padding: var(--spacing-xl); }
|
||||
.p-lg { padding: var(--spacing-2xl); }
|
||||
.p-xl { padding: var(--spacing-3xl); }
|
||||
|
||||
/* 圆角 */
|
||||
.rounded-sm { border-radius: var(--radius-sm); }
|
||||
.rounded-md { border-radius: var(--radius-md); }
|
||||
.rounded-lg { border-radius: var(--radius-lg); }
|
||||
.rounded-full { border-radius: var(--radius-full); }
|
||||
@@ -275,7 +275,7 @@ const handleTypeChange = () => {
|
||||
const onConfirmDate = ({ selectedValues }) => {
|
||||
const dateStr = selectedValues.join('-')
|
||||
const timeStr = currentTime.value.join(':')
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss')
|
||||
showDatePicker.value = false
|
||||
// 接着选时间
|
||||
showTimePicker.value = true
|
||||
@@ -285,7 +285,7 @@ const onConfirmTime = ({ selectedValues }) => {
|
||||
currentTime.value = selectedValues
|
||||
const dateStr = currentDate.value.join('-')
|
||||
const timeStr = selectedValues.join(':')
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).toISOString()
|
||||
editForm.occurredAt = dayjs(`${dateStr} ${timeStr}`).format('YYYY-MM-DDTHH:mm:ss')
|
||||
showTimePicker.value = false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import './assets/main.css'
|
||||
import './assets/theme.css'
|
||||
import './styles/common.css'
|
||||
import './styles/rich-content.css'
|
||||
|
||||
|
||||
630
Web/src/views/CalendarV2.vue
Normal file
630
Web/src/views/CalendarV2.vue
Normal file
@@ -0,0 +1,630 @@
|
||||
<template>
|
||||
<div
|
||||
class="calendar-v2"
|
||||
:data-theme="theme"
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<header class="calendar-header">
|
||||
<div class="header-content">
|
||||
<h1 class="header-title">
|
||||
{{ currentMonth }}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
class="notif-btn"
|
||||
aria-label="通知"
|
||||
>
|
||||
<van-icon name="bell" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<!-- 日历容器 -->
|
||||
<div class="calendar-container">
|
||||
<!-- 星期标题 -->
|
||||
<div class="week-days">
|
||||
<span
|
||||
v-for="day in weekDays"
|
||||
:key="day"
|
||||
class="week-day"
|
||||
>{{ day }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 日历网格 -->
|
||||
<div class="calendar-grid">
|
||||
<div
|
||||
v-for="(week, weekIndex) in calendarWeeks"
|
||||
:key="weekIndex"
|
||||
class="calendar-week"
|
||||
>
|
||||
<div
|
||||
v-for="day in week"
|
||||
:key="day.date"
|
||||
class="day-cell"
|
||||
@click="onDayClick(day)"
|
||||
>
|
||||
<div
|
||||
class="day-number"
|
||||
:class="{
|
||||
'day-today': day.isToday,
|
||||
'day-selected': day.isSelected,
|
||||
'day-has-data': day.hasData,
|
||||
'day-over-limit': day.isOverLimit,
|
||||
'day-other-month': !day.isCurrentMonth
|
||||
}"
|
||||
>
|
||||
{{ day.dayNumber }}
|
||||
</div>
|
||||
<div
|
||||
v-if="day.amount"
|
||||
class="day-amount"
|
||||
:class="{ 'amount-over': day.isOverLimit }"
|
||||
>
|
||||
{{ day.amount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 每日统计 -->
|
||||
<div class="daily-stats">
|
||||
<div class="stats-header">
|
||||
<h2 class="stats-title">
|
||||
Daily Stats
|
||||
</h2>
|
||||
<span class="stats-date">{{ selectedDateFormatted }}</span>
|
||||
</div>
|
||||
<div class="stats-card">
|
||||
<div class="stats-row">
|
||||
<span class="stats-label">Total Spent</span>
|
||||
<div class="stats-badge">
|
||||
Daily Limit: {{ dailyLimit }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-value">
|
||||
¥ {{ totalSpent }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易列表 -->
|
||||
<div class="transactions">
|
||||
<div class="txn-header">
|
||||
<h2 class="txn-title">
|
||||
Transactions
|
||||
</h2>
|
||||
<div class="txn-actions">
|
||||
<div class="txn-badge badge-success">
|
||||
{{ transactionCount }} Items
|
||||
</div>
|
||||
<button class="smart-btn">
|
||||
<van-icon name="star-o" />
|
||||
<span>Smart</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易卡片 -->
|
||||
<div class="txn-list">
|
||||
<div
|
||||
v-for="txn in transactions"
|
||||
:key="txn.id"
|
||||
class="txn-card"
|
||||
@click="onTransactionClick(txn)"
|
||||
>
|
||||
<div
|
||||
class="txn-icon"
|
||||
:style="{ backgroundColor: txn.iconBg }"
|
||||
>
|
||||
<van-icon
|
||||
:name="txn.icon"
|
||||
:color="txn.iconColor"
|
||||
/>
|
||||
</div>
|
||||
<div class="txn-content">
|
||||
<div class="txn-name">
|
||||
{{ txn.name }}
|
||||
</div>
|
||||
<div class="txn-time">
|
||||
{{ txn.time }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="txn-amount">
|
||||
{{ txn.amount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部安全距离 -->
|
||||
<div class="bottom-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
|
||||
// 当前主题
|
||||
const theme = ref('light') // 'light' | 'dark'
|
||||
|
||||
// 星期标题
|
||||
const weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
||||
|
||||
// 当前日期
|
||||
const currentDate = ref(new Date())
|
||||
const selectedDate = ref(new Date())
|
||||
|
||||
// 当前月份格式化
|
||||
const currentMonth = computed(() => {
|
||||
return currentDate.value.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})
|
||||
})
|
||||
|
||||
// 选中日期格式化
|
||||
const selectedDateFormatted = computed(() => {
|
||||
return selectedDate.value.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
})
|
||||
|
||||
// 生成日历数据
|
||||
const calendarWeeks = computed(() => {
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth()
|
||||
|
||||
// 获取当月第一天
|
||||
const firstDay = new Date(year, month, 1)
|
||||
// 获取当月最后一天
|
||||
const lastDay = new Date(year, month + 1, 0)
|
||||
|
||||
// 获取第一天是星期几 (0=Sunday, 调整为 0=Monday)
|
||||
let startDayOfWeek = firstDay.getDay() - 1
|
||||
if (startDayOfWeek === -1) {startDayOfWeek = 6}
|
||||
|
||||
const weeks = []
|
||||
let currentWeek = []
|
||||
|
||||
// 填充上月日期
|
||||
for (let i = 0; i < startDayOfWeek; i++) {
|
||||
const date = new Date(year, month, -(startDayOfWeek - i - 1))
|
||||
currentWeek.push(createDayObject(date, false))
|
||||
}
|
||||
|
||||
// 填充当月日期
|
||||
for (let day = 1; day <= lastDay.getDate(); day++) {
|
||||
const date = new Date(year, month, day)
|
||||
currentWeek.push(createDayObject(date, true))
|
||||
|
||||
if (currentWeek.length === 7) {
|
||||
weeks.push(currentWeek)
|
||||
currentWeek = []
|
||||
}
|
||||
}
|
||||
|
||||
// 填充下月日期
|
||||
if (currentWeek.length > 0) {
|
||||
const remainingDays = 7 - currentWeek.length
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
const date = new Date(year, month + 1, i)
|
||||
currentWeek.push(createDayObject(date, false))
|
||||
}
|
||||
weeks.push(currentWeek)
|
||||
}
|
||||
|
||||
return weeks
|
||||
})
|
||||
|
||||
// 创建日期对象
|
||||
const createDayObject = (date, isCurrentMonth) => {
|
||||
const today = new Date()
|
||||
const isToday =
|
||||
date.getDate() === today.getDate() &&
|
||||
date.getMonth() === today.getMonth() &&
|
||||
date.getFullYear() === today.getFullYear()
|
||||
|
||||
const isSelected =
|
||||
date.getDate() === selectedDate.value.getDate() &&
|
||||
date.getMonth() === selectedDate.value.getMonth() &&
|
||||
date.getFullYear() === selectedDate.value.getFullYear()
|
||||
|
||||
// 模拟数据 - 实际应该从 API 获取
|
||||
const mockData = getMockDataForDate(date)
|
||||
|
||||
return {
|
||||
date: date.getTime(),
|
||||
dayNumber: date.getDate(),
|
||||
isCurrentMonth,
|
||||
isToday,
|
||||
isSelected,
|
||||
hasData: mockData.hasData,
|
||||
amount: mockData.amount,
|
||||
isOverLimit: mockData.isOverLimit
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟数据获取
|
||||
const getMockDataForDate = (date) => {
|
||||
const day = date.getDate()
|
||||
|
||||
// 模拟一些有数据的日期
|
||||
if (day >= 4 && day <= 28 && date.getMonth() === currentDate.value.getMonth()) {
|
||||
const amounts = [128, 45, 230, 12, 88, 223, 15, 34, 120, 56, 442]
|
||||
const amount = amounts[day % amounts.length]
|
||||
return {
|
||||
hasData: true,
|
||||
amount: amount || '',
|
||||
isOverLimit: amount > 200 // 超过限额标红
|
||||
}
|
||||
}
|
||||
|
||||
return { hasData: false, amount: '', isOverLimit: false }
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const dailyLimit = ref('2500')
|
||||
const totalSpent = ref('1,248.50')
|
||||
const transactionCount = computed(() => transactions.value.length)
|
||||
|
||||
// 交易列表数据
|
||||
const transactions = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Lunch',
|
||||
time: '12:30 PM',
|
||||
amount: '-58.00',
|
||||
icon: 'star',
|
||||
iconColor: '#FF6B6B',
|
||||
iconBg: '#FFFFFF'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Coffee',
|
||||
time: '08:15 AM',
|
||||
amount: '-24.50',
|
||||
icon: 'coffee-o',
|
||||
iconColor: '#FCD34D',
|
||||
iconBg: '#FFFFFF'
|
||||
}
|
||||
])
|
||||
|
||||
// 点击日期
|
||||
const onDayClick = (day) => {
|
||||
if (!day.isCurrentMonth) {return}
|
||||
selectedDate.value = new Date(day.date)
|
||||
// TODO: 加载选中日期的数据
|
||||
console.log('Selected date:', day)
|
||||
}
|
||||
|
||||
// 点击交易
|
||||
const onTransactionClick = (txn) => {
|
||||
console.log('Transaction clicked:', txn)
|
||||
// TODO: 打开交易详情
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
// 暴露切换主题方法供外部调用
|
||||
defineExpose({
|
||||
toggleTheme
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@import '@/assets/theme.css';
|
||||
|
||||
.calendar-v2 {
|
||||
min-height: 100vh;
|
||||
background-color: var(--bg-primary);
|
||||
font-family: var(--font-primary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ========== 头部 ========== */
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 24px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-xl);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notif-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--bg-button);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.notif-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ========== 日历容器 ========== */
|
||||
.calendar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.week-days {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.week-day {
|
||||
width: 44px;
|
||||
text-align: center;
|
||||
font-size: var(--font-base);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.calendar-week {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
width: 44px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.day-number.day-has-data {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.day-number.day-selected {
|
||||
background-color: var(--accent-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.day-number.day-other-month {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.day-amount {
|
||||
font-size: var(--font-xs);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.day-amount.amount-over {
|
||||
color: var(--accent-danger);
|
||||
}
|
||||
|
||||
/* ========== 统计卡片 ========== */
|
||||
.daily-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stats-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stats-date {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats-badge {
|
||||
padding: 6px 10px;
|
||||
background-color: var(--accent-warning-bg);
|
||||
color: var(--accent-warning);
|
||||
font-size: var(--font-sm);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.stats-value {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-3xl);
|
||||
font-weight: var(--font-extrabold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ========== 交易列表 ========== */
|
||||
.transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.txn-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.txn-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--font-xl);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.txn-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.txn-badge {
|
||||
padding: 6px 12px;
|
||||
font-size: var(--font-base);
|
||||
font-weight: var(--font-semibold);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: var(--accent-success-bg);
|
||||
color: var(--accent-success);
|
||||
}
|
||||
|
||||
.smart-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--accent-info-bg);
|
||||
color: var(--accent-info);
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-base);
|
||||
font-weight: var(--font-semibold);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.smart-btn:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.txn-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.txn-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.txn-card:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.txn-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius-full);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.txn-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.txn-name {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.txn-time {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.txn-amount {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--font-bold);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* 底部安全距离 */
|
||||
.bottom-spacer {
|
||||
height: calc(60px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
</style>
|
||||
@@ -214,6 +214,12 @@ var list = await transactionRepository.QueryAsync(
|
||||
transaction.Type = dto.Type;
|
||||
transaction.Classify = dto.Classify ?? string.Empty;
|
||||
|
||||
// 更新交易时间
|
||||
if (!string.IsNullOrEmpty(dto.OccurredAt) && DateTime.TryParse(dto.OccurredAt, out var occurredAt))
|
||||
{
|
||||
transaction.OccurredAt = occurredAt;
|
||||
}
|
||||
|
||||
// 清除待确认状态
|
||||
transaction.UnconfirmedClassify = null;
|
||||
transaction.UnconfirmedType = null;
|
||||
@@ -728,7 +734,8 @@ public record UpdateTransactionDto(
|
||||
decimal Amount,
|
||||
decimal Balance,
|
||||
TransactionType Type,
|
||||
string? Classify
|
||||
string? Classify,
|
||||
string? OccurredAt = null
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user