todo
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 28s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
This commit is contained in:
@@ -44,9 +44,8 @@ COPY Service/ ./Service/
|
||||
COPY WebApi/ ./WebApi/
|
||||
|
||||
# 构建并发布
|
||||
# 使用 -m:1 限制 CPU/内存并行度,减少容器构建崩溃风险
|
||||
RUN dotnet publish WebApi/WebApi.csproj -c Release -o /app/publish --no-restore -m:1
|
||||
|
||||
# 使用 /m:1 限制 CPU/内存并行度,减少容器构建崩溃风险
|
||||
RUN dotnet publish WebApi/WebApi.csproj -c Release -o /app/publish --no-restore /m:1
|
||||
# 将前端构建产物复制到后端的 wwwroot 目录
|
||||
COPY --from=frontend-build /app/frontend/dist /app/publish/wwwroot
|
||||
|
||||
|
||||
130
Web/VERSION_SWITCH_SUMMARY.md
Normal file
130
Web/VERSION_SWITCH_SUMMARY.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 版本切换功能实现总结
|
||||
|
||||
## 实现概述
|
||||
|
||||
在设置的开发者选项中添加了版本切换功能,用户可以在 V1 和 V2 版本之间切换。
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. Web/src/stores/version.js (新增)
|
||||
- 创建 Pinia store 管理版本状态
|
||||
- 使用 localStorage 持久化版本选择
|
||||
- 提供 `setVersion()` 和 `isV2()` 方法
|
||||
|
||||
### 2. Web/src/views/SettingView.vue (修改)
|
||||
- 在开发者选项中添加"切换版本"选项
|
||||
- 显示当前版本(V1/V2)
|
||||
- 实现版本切换对话框
|
||||
- 实现版本切换后的路由跳转逻辑
|
||||
|
||||
### 3. Web/src/router/index.js (修改)
|
||||
- 引入 version store
|
||||
- 在路由守卫中添加版本路由重定向逻辑
|
||||
- V2 模式下自动跳转到 V2 路由(如果存在)
|
||||
- V1 模式下自动跳转到 V1 路由(如果在 V2 路由)
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **版本选择界面**
|
||||
- 设置页面显示当前版本
|
||||
- 点击弹出对话框,选择 V1 或 V2
|
||||
- 切换成功后显示提示信息
|
||||
|
||||
2. **智能路由跳转**
|
||||
- 选择 V2 后,如果当前路由有 V2 版本,自动跳转
|
||||
- 选择 V1 后,如果当前在 V2 路由,自动跳转到 V1
|
||||
- 没有对应版本时,保持当前路由不变
|
||||
|
||||
3. **路由守卫保护**
|
||||
- 每次路由跳转时检查版本设置
|
||||
- 自动重定向到正确版本的路由
|
||||
- 保留 query 和 params 参数
|
||||
|
||||
4. **状态持久化**
|
||||
- 版本选择保存在 localStorage
|
||||
- 刷新页面后版本设置保持不变
|
||||
|
||||
## V2 路由命名规范
|
||||
|
||||
V2 路由必须遵循命名规范:`原路由名-v2`
|
||||
|
||||
示例:
|
||||
- V1: `calendar` → V2: `calendar-v2`
|
||||
- V1: `budget` → V2: `budget-v2`
|
||||
|
||||
## 当前支持的 V2 路由
|
||||
|
||||
- `calendar` → `calendar-v2` (CalendarV2.vue)
|
||||
|
||||
## 测试验证
|
||||
|
||||
- ✅ ESLint 检查通过(无错误)
|
||||
- ✅ 构建成功(pnpm build)
|
||||
- ✅ 所有修改文件符合项目代码规范
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 用户操作流程
|
||||
|
||||
1. 进入"设置"页面
|
||||
2. 滚动到"开发者"分组
|
||||
3. 点击"切换版本"(当前版本显示在右侧)
|
||||
4. 选择"V1"或"V2"
|
||||
5. 系统自动跳转到对应版本的路由
|
||||
|
||||
### 开发者添加新 V2 路由
|
||||
|
||||
```javascript
|
||||
// router/index.js
|
||||
{
|
||||
path: '/xxx-v2',
|
||||
name: 'xxx-v2',
|
||||
component: () => import('../views/XxxViewV2.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
```
|
||||
|
||||
添加后即可自动支持版本切换。
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 版本检测逻辑
|
||||
|
||||
```javascript
|
||||
// 在路由守卫中
|
||||
if (versionStore.isV2()) {
|
||||
// 尝试跳转到 V2 路由
|
||||
const v2RouteName = `${routeName}-v2`
|
||||
if (存在 v2Route) {
|
||||
跳转到 v2Route
|
||||
} else {
|
||||
保持当前路由
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 版本状态管理
|
||||
|
||||
```javascript
|
||||
// stores/version.js
|
||||
const currentVersion = ref(localStorage.getItem('app-version') || 'v1')
|
||||
|
||||
const setVersion = (version) => {
|
||||
currentVersion.value = version
|
||||
localStorage.setItem('app-version', version)
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. V2 路由必须按照 `xxx-v2` 命名规范
|
||||
2. 如果页面没有 V2 版本,切换后会保持在 V1 版本
|
||||
3. 路由守卫会自动处理所有版本相关的路由跳转
|
||||
4. 版本状态持久化在 localStorage 中
|
||||
|
||||
## 后续改进建议
|
||||
|
||||
1. 可以在 UI 上添加更明显的版本标识
|
||||
2. 可以在无 V2 路由时给出提示
|
||||
3. 可以添加版本切换的动画效果
|
||||
4. 可以为不同版本设置不同的主题样式
|
||||
143
Web/VERSION_SWITCH_TEST.md
Normal file
143
Web/VERSION_SWITCH_TEST.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# 版本切换功能测试文档
|
||||
|
||||
## 功能说明
|
||||
|
||||
在设置的开发者选项中添加了版本切换功能,用户可以在 V1 和 V2 版本之间切换。当选择 V2 时,如果有对应的 V2 路由则自动跳转,否则保持当前路由。
|
||||
|
||||
## 实现文件
|
||||
|
||||
1. **Store**: `Web/src/stores/version.js` - 版本状态管理
|
||||
2. **View**: `Web/src/views/SettingView.vue` - 设置页面添加版本切换入口
|
||||
3. **Router**: `Web/src/router/index.js` - 路由守卫实现版本路由重定向
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 版本状态持久化存储(localStorage)
|
||||
- ✅ 设置页面显示当前版本(V1/V2)
|
||||
- ✅ 点击弹出对话框选择版本
|
||||
- ✅ 自动检测并跳转到对应版本路由
|
||||
- ✅ 如果没有对应版本路由,保持当前路由
|
||||
- ✅ 路由守卫自动处理版本路由
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 1. 基础功能测试
|
||||
|
||||
1. 启动应用并登录
|
||||
2. 进入"设置"页面
|
||||
3. 找到"开发者"分组下的"切换版本"选项
|
||||
4. 当前版本应显示为 "V1"(首次使用)
|
||||
|
||||
### 2. 切换到 V2 测试
|
||||
|
||||
1. 点击"切换版本"
|
||||
2. 弹出对话框,显示"选择版本"标题
|
||||
3. 对话框有两个按钮:"V1"(取消按钮)和"V2"(确认按钮)
|
||||
4. 点击"V2"按钮
|
||||
5. 应显示提示"已切换到 V2"
|
||||
6. "切换版本"选项的值应更新为 "V2"
|
||||
|
||||
### 3. V2 路由跳转测试
|
||||
|
||||
#### 测试有 V2 路由的情况(日历页面)
|
||||
|
||||
1. 确保当前版本为 V2
|
||||
2. 点击导航栏的"日历"(路由名:`calendar`)
|
||||
3. 应自动跳转到 `calendar-v2`(CalendarV2.vue)
|
||||
4. 地址栏 URL 应为 `/calendar-v2`
|
||||
|
||||
#### 测试没有 V2 路由的情况
|
||||
|
||||
1. 确保当前版本为 V2
|
||||
2. 点击导航栏的"账单分析"(路由名:`bill-analysis`)
|
||||
3. 应保持在 `bill-analysis` 路由(没有 v2 版本)
|
||||
4. 地址栏 URL 应为 `/bill-analysis`
|
||||
|
||||
### 4. 切换回 V1 测试
|
||||
|
||||
1. 当前版本为 V2,在 `calendar-v2` 页面
|
||||
2. 进入"设置"页面,点击"切换版本"
|
||||
3. 点击"V1"按钮
|
||||
4. 应显示提示"已切换到 V1"
|
||||
5. 如果当前在 V2 路由(如 `calendar-v2`),应自动跳转到 V1 路由(`calendar`)
|
||||
6. 地址栏 URL 应为 `/calendar`
|
||||
|
||||
### 5. 持久化测试
|
||||
|
||||
1. 切换到 V2 版本
|
||||
2. 刷新页面
|
||||
3. 重新登录后,进入"设置"页面
|
||||
4. "切换版本"选项应仍显示 "V2"
|
||||
5. 访问有 V2 路由的页面,应自动跳转到 V2 版本
|
||||
|
||||
### 6. 路由守卫测试
|
||||
|
||||
#### 直接访问 V2 路由(V1 模式下)
|
||||
|
||||
1. 确保当前版本为 V1
|
||||
2. 在地址栏直接输入 `/calendar-v2`
|
||||
3. 应自动重定向到 `/calendar`
|
||||
|
||||
#### 直接访问 V1 路由(V2 模式下)
|
||||
|
||||
1. 确保当前版本为 V2
|
||||
2. 在地址栏直接输入 `/calendar`
|
||||
3. 应自动重定向到 `/calendar-v2`
|
||||
|
||||
## 当前支持 V2 的路由
|
||||
|
||||
- `calendar` → `calendar-v2` (CalendarV2.vue)
|
||||
|
||||
## 代码验证
|
||||
|
||||
### 版本 Store 检查
|
||||
|
||||
```javascript
|
||||
// 打开浏览器控制台
|
||||
const versionStore = useVersionStore()
|
||||
console.log(versionStore.currentVersion) // 应输出 'v1' 或 'v2'
|
||||
console.log(versionStore.isV2()) // 应输出 true 或 false
|
||||
```
|
||||
|
||||
### LocalStorage 检查
|
||||
|
||||
```javascript
|
||||
// 打开浏览器控制台
|
||||
console.log(localStorage.getItem('app-version')) // 应输出 'v1' 或 'v2'
|
||||
```
|
||||
|
||||
## 预期结果
|
||||
|
||||
- ✅ 所有路由跳转正常
|
||||
- ✅ 版本切换提示正常显示
|
||||
- ✅ 版本状态持久化正常
|
||||
- ✅ 路由守卫正常工作
|
||||
- ✅ 没有控制台错误
|
||||
- ✅ UI 响应流畅
|
||||
|
||||
## 潜在问题
|
||||
|
||||
1. 如果用户在 V2 路由页面直接切换到 V1,可能会出现短暂的页面重载
|
||||
2. 某些页面可能没有 V2 版本,切换后会保持在 V1 版本
|
||||
|
||||
## 后续扩展
|
||||
|
||||
如需添加更多 V2 路由,只需:
|
||||
|
||||
1. 创建新的 Vue 组件(如 `XXXViewV2.vue`)
|
||||
2. 在 `router/index.js` 中添加路由,命名格式为 `原路由名-v2`
|
||||
3. 路由守卫会自动处理版本切换逻辑
|
||||
|
||||
## 示例:添加新的 V2 路由
|
||||
|
||||
```javascript
|
||||
// router/index.js
|
||||
{
|
||||
path: '/budget-v2',
|
||||
name: 'budget-v2',
|
||||
component: () => import('../views/BudgetViewV2.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
```
|
||||
|
||||
添加后,当用户选择 V2 版本并访问 `/budget` 时,会自动跳转到 `/budget-v2`。
|
||||
@@ -4,7 +4,17 @@
|
||||
class="app-provider"
|
||||
>
|
||||
<div class="app-root">
|
||||
<RouterView />
|
||||
<router-view v-slot="{ Component }">
|
||||
<keep-alive
|
||||
:include="cachedViews"
|
||||
:max="8"
|
||||
>
|
||||
<component
|
||||
:is="Component"
|
||||
:key="route.name"
|
||||
/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
<van-tabbar
|
||||
v-show="showTabbar"
|
||||
v-model="active"
|
||||
@@ -79,6 +89,15 @@ import '@/styles/common.css'
|
||||
|
||||
const messageStore = useMessageStore()
|
||||
|
||||
// 定义需要缓存的页面组件名称
|
||||
const cachedViews = ref([
|
||||
'CalendarV2', // 日历V2页面
|
||||
'CalendarView', // 日历V1页面
|
||||
'StatisticsView', // 统计页面
|
||||
'BalanceView', // 账单页面
|
||||
'BudgetView' // 预算页面
|
||||
])
|
||||
|
||||
const updateVh = () => {
|
||||
const vh = window.innerHeight
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`)
|
||||
@@ -122,6 +141,7 @@ const showTabbar = computed(() => {
|
||||
return (
|
||||
route.path === '/' ||
|
||||
route.path === '/calendar' ||
|
||||
route.path === '/calendar-v2' ||
|
||||
route.path === '/message' ||
|
||||
route.path === '/setting' ||
|
||||
route.path === '/balance' ||
|
||||
@@ -136,6 +156,8 @@ const theme = ref('light')
|
||||
const updateTheme = () => {
|
||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
theme.value = isDark ? 'dark' : 'light'
|
||||
// 在文档根元素上设置 data-theme 属性,使 CSS 变量生效
|
||||
document.documentElement.setAttribute('data-theme', theme.value)
|
||||
}
|
||||
|
||||
// 监听系统主题变化
|
||||
@@ -165,6 +187,7 @@ const setActive = (path) => {
|
||||
active.value = (() => {
|
||||
switch (path) {
|
||||
case '/calendar':
|
||||
case '/calendar-v2':
|
||||
return 'ccalendar'
|
||||
case '/balance':
|
||||
case '/message':
|
||||
@@ -180,7 +203,7 @@ const setActive = (path) => {
|
||||
}
|
||||
|
||||
const isShowAddBill = computed(() => {
|
||||
return route.path === '/' || route.path === '/balance' || route.path === '/message' || route.path === '/calendar'
|
||||
return route.path === '/' || route.path === '/balance' || route.path === '/message' || route.path === '/calendar' || route.path === '/calendar-v2'
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useVersionStore } from '@/stores/version'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -34,6 +35,12 @@ const router = createRouter({
|
||||
component: () => import('../views/CalendarView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/calendar-v2',
|
||||
name: 'calendar-v2',
|
||||
component: () => import('../views/CalendarV2.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/smart-classification',
|
||||
name: 'smart-classification',
|
||||
@@ -113,6 +120,7 @@ const router = createRouter({
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
const versionStore = useVersionStore()
|
||||
const requiresAuth = to.meta.requiresAuth !== false // 默认需要认证
|
||||
|
||||
if (requiresAuth && !authStore.isAuthenticated) {
|
||||
@@ -122,6 +130,33 @@ router.beforeEach((to, from, next) => {
|
||||
// 已登录用户访问登录页,跳转到首页
|
||||
next({ name: 'transactions' })
|
||||
} else {
|
||||
// 版本路由处理
|
||||
if (versionStore.isV2()) {
|
||||
// 如果当前选择 V2,尝试跳转到 V2 路由
|
||||
const routeName = to.name?.toString()
|
||||
if (routeName && !routeName.endsWith('-v2')) {
|
||||
const v2RouteName = `${routeName}-v2`
|
||||
const v2Route = router.getRoutes().find(route => route.name === v2RouteName)
|
||||
|
||||
if (v2Route) {
|
||||
next({ name: v2RouteName, query: to.query, params: to.params })
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果当前选择 V1,且访问的是 V2 路由,跳转到 V1
|
||||
const routeName = to.name?.toString()
|
||||
if (routeName && routeName.endsWith('-v2')) {
|
||||
const v1RouteName = routeName.replace(/-v2$/, '')
|
||||
const v1Route = router.getRoutes().find(route => route.name === v1RouteName)
|
||||
|
||||
if (v1Route) {
|
||||
next({ name: v1RouteName, query: to.query, params: to.params })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
19
Web/src/stores/version.js
Normal file
19
Web/src/stores/version.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const useVersionStore = defineStore('version', () => {
|
||||
const currentVersion = ref(localStorage.getItem('app-version') || 'v1')
|
||||
|
||||
const setVersion = (version) => {
|
||||
currentVersion.value = version
|
||||
localStorage.setItem('app-version', version)
|
||||
}
|
||||
|
||||
const isV2 = () => currentVersion.value === 'v2'
|
||||
|
||||
return {
|
||||
currentVersion,
|
||||
setVersion,
|
||||
isV2
|
||||
}
|
||||
})
|
||||
@@ -1,18 +1,30 @@
|
||||
<template>
|
||||
<div
|
||||
class="calendar-v2"
|
||||
:data-theme="theme"
|
||||
>
|
||||
<div class="calendar-v2">
|
||||
<!-- 头部 -->
|
||||
<header class="calendar-header">
|
||||
<button
|
||||
class="month-nav-btn"
|
||||
aria-label="上一月"
|
||||
@click="changeMonth(-1)"
|
||||
>
|
||||
<van-icon name="arrow-left" />
|
||||
</button>
|
||||
<div class="header-content">
|
||||
<h1 class="header-title">
|
||||
{{ currentMonth }}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
class="month-nav-btn"
|
||||
aria-label="下一月"
|
||||
@click="changeMonth(1)"
|
||||
>
|
||||
<van-icon name="arrow" />
|
||||
</button>
|
||||
<button
|
||||
class="notif-btn"
|
||||
aria-label="通知"
|
||||
@click="onNotificationClick"
|
||||
>
|
||||
<van-icon name="bell" />
|
||||
</button>
|
||||
@@ -70,19 +82,22 @@
|
||||
<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>
|
||||
<span class="stats-label">
|
||||
{{ isToday ? '今日支出' : '当日支出' }}
|
||||
</span>
|
||||
<div class="stats-badge">
|
||||
Daily Limit: {{ dailyLimit }}
|
||||
{{ isToday ? '今日预算' : '当日预算' }}
|
||||
: ¥{{ dailyBudget }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stats-value">
|
||||
¥ {{ totalSpent }}
|
||||
¥ {{ selectedDayExpense.toFixed(2) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -91,21 +106,52 @@
|
||||
<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" />
|
||||
<button
|
||||
class="smart-btn"
|
||||
@click="onSmartClick"
|
||||
>
|
||||
<van-icon name="fire" />
|
||||
<span>Smart</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 交易卡片 -->
|
||||
<div class="txn-list">
|
||||
<van-loading
|
||||
v-if="transactionsLoading"
|
||||
class="txn-loading"
|
||||
size="24px"
|
||||
vertical
|
||||
>
|
||||
加载中...
|
||||
</van-loading>
|
||||
<div
|
||||
v-else-if="transactions.length === 0"
|
||||
class="txn-empty"
|
||||
>
|
||||
<div class="empty-icon">
|
||||
<van-icon
|
||||
name="balance-list-o"
|
||||
size="48"
|
||||
/>
|
||||
</div>
|
||||
<div class="empty-text">
|
||||
当天暂无交易记录
|
||||
</div>
|
||||
<div class="empty-hint">
|
||||
轻松享受无消费的一天 ✨
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="txn-list"
|
||||
>
|
||||
<div
|
||||
v-for="txn in transactions"
|
||||
:key="txn.id"
|
||||
@@ -142,33 +188,54 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast, showLoadingToast, closeToast } from 'vant'
|
||||
import { getDailyStatistics } from '@/api/statistics'
|
||||
import { getTransactionsByDate } from '@/api/transactionRecord'
|
||||
import { getBudgetList } from '@/api/budget'
|
||||
|
||||
// 当前主题
|
||||
const theme = ref('light') // 'light' | 'dark'
|
||||
// 定义组件名称(keep-alive 需要通过 name 识别)
|
||||
defineOptions({
|
||||
name: 'CalendarV2'
|
||||
})
|
||||
|
||||
// 星期标题
|
||||
const weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']
|
||||
// 路由
|
||||
const router = useRouter()
|
||||
|
||||
// 星期标题(中文)
|
||||
const weekDays = ['一', '二', '三', '四', '五', '六', '日']
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
const dailyStatsMap = ref({}) // 每日统计数据 Map: { '2026-01-15': { count, expense, income } }
|
||||
|
||||
// 当前日期
|
||||
const currentDate = ref(new Date())
|
||||
const selectedDate = ref(new Date())
|
||||
|
||||
// 当前月份格式化
|
||||
// 当前月份格式化(中文)
|
||||
const currentMonth = computed(() => {
|
||||
return currentDate.value.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long'
|
||||
})
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth() + 1
|
||||
return `${year}年${month}月`
|
||||
})
|
||||
|
||||
// 选中日期格式化
|
||||
const isToday = computed(() => {
|
||||
const today = new Date()
|
||||
return (
|
||||
selectedDate.value.getDate() === today.getDate() &&
|
||||
selectedDate.value.getMonth() === today.getMonth() &&
|
||||
selectedDate.value.getFullYear() === today.getFullYear()
|
||||
)
|
||||
})
|
||||
|
||||
// 选中日期格式化(中文)
|
||||
const selectedDateFormatted = computed(() => {
|
||||
return selectedDate.value.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
})
|
||||
const year = selectedDate.value.getFullYear()
|
||||
const month = selectedDate.value.getMonth() + 1
|
||||
const day = selectedDate.value.getDate()
|
||||
return `${year}年${month}月${day}日`
|
||||
})
|
||||
|
||||
// 生成日历数据
|
||||
@@ -231,8 +298,13 @@ const createDayObject = (date, isCurrentMonth) => {
|
||||
date.getMonth() === selectedDate.value.getMonth() &&
|
||||
date.getFullYear() === selectedDate.value.getFullYear()
|
||||
|
||||
// 模拟数据 - 实际应该从 API 获取
|
||||
const mockData = getMockDataForDate(date)
|
||||
// 从 API 数据获取
|
||||
const dateKey = formatDateKey(date)
|
||||
const dayStats = dailyStatsMap.value[dateKey] || {}
|
||||
|
||||
// 计算净支出(支出 - 收入)
|
||||
const netAmount = (dayStats.expense || 0) - (dayStats.income || 0)
|
||||
const hasData = dayStats.count > 0
|
||||
|
||||
return {
|
||||
date: date.getTime(),
|
||||
@@ -240,79 +312,237 @@ const createDayObject = (date, isCurrentMonth) => {
|
||||
isCurrentMonth,
|
||||
isToday,
|
||||
isSelected,
|
||||
hasData: mockData.hasData,
|
||||
amount: mockData.amount,
|
||||
isOverLimit: mockData.isOverLimit
|
||||
hasData,
|
||||
amount: hasData ? Math.abs(netAmount).toFixed(0) : '',
|
||||
isOverLimit: netAmount > (dailyBudget.value || 0) // 超过每日预算标红
|
||||
}
|
||||
}
|
||||
|
||||
// 模拟数据获取
|
||||
const getMockDataForDate = (date) => {
|
||||
const day = date.getDate()
|
||||
// 格式化日期为 key (yyyy-MM-dd)
|
||||
const formatDateKey = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
// 模拟一些有数据的日期
|
||||
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 // 超过限额标红
|
||||
}
|
||||
}
|
||||
// 获取月度每日统计数据
|
||||
const fetchDailyStats = async (year, month) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await getDailyStatistics({ year, month })
|
||||
|
||||
return { hasData: false, amount: '', isOverLimit: false }
|
||||
if (response.success && response.data) {
|
||||
// 构建日期 Map
|
||||
const statsMap = {}
|
||||
response.data.forEach(item => {
|
||||
statsMap[item.date] = {
|
||||
count: item.count,
|
||||
expense: item.expense,
|
||||
income: item.income
|
||||
}
|
||||
})
|
||||
dailyStatsMap.value = { ...dailyStatsMap.value, ...statsMap }
|
||||
}
|
||||
} catch (_error) {
|
||||
showToast('获取日历数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
const dailyLimit = ref('2500')
|
||||
const totalSpent = ref('1,248.50')
|
||||
const dailyBudget = ref(0) // 每日预算限额
|
||||
const selectedDayExpense = ref(0) // 选中日期的支出
|
||||
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 transactions = ref([])
|
||||
const transactionsLoading = ref(false)
|
||||
|
||||
// 获取预算数据
|
||||
const fetchBudgetData = async () => {
|
||||
try {
|
||||
const response = await getBudgetList()
|
||||
if (response.success && response.data && response.data.length > 0) {
|
||||
// 假设取第一个预算的月度限额除以30作为每日预算
|
||||
const monthlyBudget = response.data[0].limit || 0
|
||||
dailyBudget.value = Math.floor(monthlyBudget / 30)
|
||||
}
|
||||
])
|
||||
} catch (error) {
|
||||
console.error('获取预算失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中日期的交易列表
|
||||
const fetchDayTransactions = async (date) => {
|
||||
try {
|
||||
transactionsLoading.value = true
|
||||
const dateKey = formatDateKey(date)
|
||||
const response = await getTransactionsByDate(dateKey)
|
||||
|
||||
if (response.success && response.data) {
|
||||
// 转换为界面需要的格式
|
||||
transactions.value = response.data.map(txn => ({
|
||||
id: txn.id,
|
||||
name: txn.reason || '未知交易',
|
||||
time: formatTime(txn.occurredAt),
|
||||
amount: formatAmount(txn.amount, txn.type),
|
||||
icon: getIconByClassify(txn.classify),
|
||||
iconColor: getColorByType(txn.type),
|
||||
iconBg: '#FFFFFF',
|
||||
classify: txn.classify,
|
||||
type: txn.type
|
||||
}))
|
||||
|
||||
// 计算当日支出
|
||||
selectedDayExpense.value = response.data
|
||||
.filter(t => t.type === 0) // 只统计支出
|
||||
.reduce((sum, t) => sum + t.amount, 0)
|
||||
}
|
||||
} catch (_error) {
|
||||
showToast('获取交易记录失败')
|
||||
} finally {
|
||||
transactionsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时间(HH:MM)
|
||||
const formatTime = (dateTimeStr) => {
|
||||
const date = new Date(dateTimeStr)
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
// 格式化金额
|
||||
const formatAmount = (amount, type) => {
|
||||
const sign = type === 1 ? '+' : '-' // 1=收入, 0=支出
|
||||
return `${sign}${amount.toFixed(2)}`
|
||||
}
|
||||
|
||||
// 根据分类获取图标
|
||||
const getIconByClassify = (classify) => {
|
||||
const iconMap = {
|
||||
'餐饮': 'food',
|
||||
'购物': 'shopping',
|
||||
'交通': 'transport',
|
||||
'娱乐': 'play',
|
||||
'医疗': 'medical',
|
||||
'工资': 'money',
|
||||
'红包': 'red-packet'
|
||||
}
|
||||
return iconMap[classify] || 'star'
|
||||
}
|
||||
|
||||
// 根据类型获取颜色
|
||||
const getColorByType = (type) => {
|
||||
return type === 1 ? '#22C55E' : '#FF6B6B' // 收入绿色,支出红色
|
||||
}
|
||||
|
||||
// 点击日期
|
||||
const onDayClick = (day) => {
|
||||
const onDayClick = async (day) => {
|
||||
if (!day.isCurrentMonth) {return}
|
||||
selectedDate.value = new Date(day.date)
|
||||
// TODO: 加载选中日期的数据
|
||||
console.log('Selected date:', day)
|
||||
// 加载选中日期的交易数据
|
||||
await fetchDayTransactions(selectedDate.value)
|
||||
}
|
||||
|
||||
// 点击交易
|
||||
// 点击交易卡片 - 跳转到详情页
|
||||
const onTransactionClick = (txn) => {
|
||||
console.log('Transaction clicked:', txn)
|
||||
// TODO: 打开交易详情
|
||||
// 跳转到交易详情(假设有详情页路由)
|
||||
router.push({
|
||||
path: '/transaction-detail',
|
||||
query: { id: txn.id }
|
||||
})
|
||||
}
|
||||
|
||||
// 切换主题
|
||||
const toggleTheme = () => {
|
||||
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
||||
// 点击通知按钮
|
||||
const onNotificationClick = () => {
|
||||
router.push('/message')
|
||||
}
|
||||
|
||||
// 暴露切换主题方法供外部调用
|
||||
defineExpose({
|
||||
toggleTheme
|
||||
// 点击 Smart 按钮 - 跳转到智能分类页面
|
||||
const onSmartClick = () => {
|
||||
if (transactions.value.length === 0) {
|
||||
showToast('当天没有交易记录')
|
||||
return
|
||||
}
|
||||
router.push({
|
||||
path: '/smart-classification',
|
||||
query: {
|
||||
date: formatDateKey(selectedDate.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换月份
|
||||
const changeMonth = async (offset) => {
|
||||
const newDate = new Date(currentDate.value)
|
||||
newDate.setMonth(newDate.getMonth() + offset)
|
||||
currentDate.value = newDate
|
||||
|
||||
// 重新加载月度数据
|
||||
await fetchDailyStats(newDate.getFullYear(), newDate.getMonth() + 1)
|
||||
}
|
||||
|
||||
// 监听当前月份变化
|
||||
watch(() => currentDate.value, async (newDate) => {
|
||||
await fetchDailyStats(newDate.getFullYear(), newDate.getMonth() + 1)
|
||||
}, { immediate: false })
|
||||
|
||||
// 监听选中日期变化
|
||||
watch(() => selectedDate.value, async (newDate) => {
|
||||
await fetchDayTransactions(newDate)
|
||||
}, { immediate: false })
|
||||
|
||||
// 组件挂载
|
||||
onMounted(async () => {
|
||||
showLoadingToast({
|
||||
message: '加载中...',
|
||||
forbidClick: true,
|
||||
duration: 0
|
||||
})
|
||||
|
||||
try {
|
||||
// 并行加载预算和当月统计数据
|
||||
await Promise.all([
|
||||
fetchBudgetData(),
|
||||
fetchDailyStats(currentDate.value.getFullYear(), currentDate.value.getMonth() + 1)
|
||||
])
|
||||
|
||||
// 加载今天的交易数据
|
||||
await fetchDayTransactions(selectedDate.value)
|
||||
} finally {
|
||||
closeToast()
|
||||
}
|
||||
|
||||
// 监听交易变更事件(来自全局添加账单)
|
||||
window.addEventListener('transactions-changed', handleTransactionsChanged)
|
||||
})
|
||||
|
||||
// 页面激活时的钩子(从缓存恢复时触发)
|
||||
onActivated(() => {
|
||||
// 当前依赖全局事件 'transactions-changed' 来刷新数据
|
||||
// 如果需要在页面激活时强制刷新,可以在这里调用相关函数
|
||||
})
|
||||
|
||||
// 页面失活时的钩子(被缓存时触发)
|
||||
onDeactivated(() => {
|
||||
// 如果有定时器、WebSocket 等,应该在这里清理
|
||||
// 目前 CalendarV2 没有这些资源,所以无需处理
|
||||
})
|
||||
|
||||
// 处理交易变更事件
|
||||
const handleTransactionsChanged = async () => {
|
||||
// 重新加载当前月和当天数据
|
||||
await fetchDailyStats(currentDate.value.getFullYear(), currentDate.value.getMonth() + 1)
|
||||
await fetchDayTransactions(selectedDate.value)
|
||||
}
|
||||
|
||||
// 组件卸载前清理
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('transactions-changed', handleTransactionsChanged)
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -343,9 +573,9 @@ defineExpose({
|
||||
|
||||
.header-title {
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-xl);
|
||||
font-size: var(--font-2xl);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -366,12 +596,35 @@ defineExpose({
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.month-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.month-nav-btn:active {
|
||||
background-color: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.txn-loading {
|
||||
padding: var(--spacing-3xl);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ========== 日历容器 ========== */
|
||||
.calendar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
gap: var(--spacing-xl);
|
||||
padding: var(--spacing-3xl);
|
||||
}
|
||||
|
||||
.week-days {
|
||||
@@ -390,19 +643,20 @@ defineExpose({
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.calendar-week {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 56px; /* 固定最小高度:32px(day-number) + 16px(day-amount) + 8px(gap) */
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
gap: var(--spacing-xs);
|
||||
width: 44px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -422,15 +676,18 @@ defineExpose({
|
||||
|
||||
.day-number.day-has-data {
|
||||
background-color: var(--bg-tertiary);
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
.day-number.day-selected {
|
||||
background-color: var(--accent-primary);
|
||||
color: #FFFFFF;
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
|
||||
.day-number.day-other-month {
|
||||
opacity: 0.3;
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.day-amount {
|
||||
@@ -447,8 +704,9 @@ defineExpose({
|
||||
.daily-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
gap: var(--spacing-xl);
|
||||
padding: var(--spacing-3xl);
|
||||
padding-top: 0
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
@@ -474,8 +732,8 @@ defineExpose({
|
||||
.stats-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-2xl);
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
@@ -512,8 +770,9 @@ defineExpose({
|
||||
.transactions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 24px;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-3xl);
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.txn-header {
|
||||
@@ -533,7 +792,7 @@ defineExpose({
|
||||
.txn-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.txn-badge {
|
||||
@@ -551,6 +810,7 @@ defineExpose({
|
||||
.smart-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 6px;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
background-color: var(--accent-info-bg);
|
||||
@@ -570,14 +830,14 @@ defineExpose({
|
||||
.txn-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.txn-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
padding: var(--spacing-xl);
|
||||
background-color: var(--bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
@@ -601,7 +861,7 @@ defineExpose({
|
||||
.txn-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: var(--spacing-xs);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -623,6 +883,42 @@ defineExpose({
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ========== 空状态 ========== */
|
||||
.txn-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 240px;
|
||||
padding: var(--spacing-4xl) var(--spacing-2xl);
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: var(--font-lg);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.empty-hint {
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-tertiary);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 底部安全距离 */
|
||||
.bottom-spacer {
|
||||
height: calc(60px + env(safe-area-inset-bottom, 0px));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<div class="page-container-flex">
|
||||
<van-nav-bar
|
||||
title="设置"
|
||||
@@ -115,6 +115,12 @@
|
||||
is-link
|
||||
@click="handleScheduledTasks"
|
||||
/>
|
||||
<van-cell
|
||||
title="切换版本"
|
||||
is-link
|
||||
:value="versionStore.currentVersion.toUpperCase()"
|
||||
@click="handleVersionSwitch"
|
||||
/>
|
||||
</van-cell-group>
|
||||
|
||||
<div
|
||||
@@ -139,14 +145,16 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showLoadingToast, showSuccessToast, showToast, closeToast, showConfirmDialog } from 'vant'
|
||||
import { showLoadingToast, showSuccessToast, showToast, closeToast, showConfirmDialog, showDialog } from 'vant'
|
||||
import { uploadBillFile } from '@/api/billImport'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useVersionStore } from '@/stores/version'
|
||||
import { getVapidPublicKey, subscribe, testNotification } from '@/api/notification'
|
||||
import { updateServiceWorker } from '@/registerServiceWorker'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const versionStore = useVersionStore()
|
||||
const fileInputRef = ref(null)
|
||||
const currentType = ref('')
|
||||
const notificationEnabled = ref(false)
|
||||
@@ -381,6 +389,64 @@ const handleReloadFromNetwork = async () => {
|
||||
const handleScheduledTasks = () => {
|
||||
router.push({ name: 'scheduled-tasks' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理版本切换
|
||||
*/
|
||||
const handleVersionSwitch = async () => {
|
||||
try {
|
||||
await showDialog({
|
||||
title: '选择版本',
|
||||
message: '请选择要使用的版本',
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'V2',
|
||||
cancelButtonText: 'V1'
|
||||
}).then(() => {
|
||||
// 选择 V2
|
||||
versionStore.setVersion('v2')
|
||||
showSuccessToast('已切换到 V2')
|
||||
// 尝试跳转到当前路由的 V2 版本
|
||||
redirectToVersionRoute()
|
||||
}).catch(() => {
|
||||
// 选择 V1
|
||||
versionStore.setVersion('v1')
|
||||
showSuccessToast('已切换到 V1')
|
||||
// 尝试跳转到当前路由的 V1 版本
|
||||
redirectToVersionRoute()
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('版本切换失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据当前版本重定向路由
|
||||
*/
|
||||
const redirectToVersionRoute = () => {
|
||||
const currentRoute = router.currentRoute.value
|
||||
const currentRouteName = currentRoute.name
|
||||
|
||||
if (versionStore.isV2()) {
|
||||
// 尝试跳转到 V2 路由
|
||||
const v2RouteName = `${currentRouteName}-v2`
|
||||
const v2Route = router.getRoutes().find(route => route.name === v2RouteName)
|
||||
|
||||
if (v2Route) {
|
||||
router.push({ name: v2RouteName })
|
||||
}
|
||||
// 如果没有 V2 路由,保持当前路由
|
||||
} else {
|
||||
// V1 版本:如果当前在 V2 路由,跳转到 V1
|
||||
if (currentRouteName && currentRouteName.toString().endsWith('-v2')) {
|
||||
const v1RouteName = currentRouteName.toString().replace(/-v2$/, '')
|
||||
const v1Route = router.getRoutes().find(route => route.name === v1RouteName)
|
||||
|
||||
if (v1Route) {
|
||||
router.push({ name: v1RouteName })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace WebApi.Controllers;
|
||||
using Service.AI;
|
||||
|
||||
namespace WebApi.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]/[action]")]
|
||||
@@ -224,7 +226,7 @@ public class TransactionCategoryController(
|
||||
4. 使用单色,fill=""currentColor""
|
||||
5. 简洁的设计,适合作为应用图标";
|
||||
|
||||
var userPrompt = $"为分类"{category.Name}"({(category.Type == TransactionType.Expense ? "支出" : category.Type == TransactionType.Income ? "收入" : "不计收支")})生成一个SVG图标";
|
||||
var userPrompt = $"为分类\"{category.Name}\"({(category.Type == TransactionType.Expense ? "支出" : category.Type == TransactionType.Income ? "收入" : "不计收支")})生成一个SVG图标";
|
||||
|
||||
var svgContent = await openAiService.ChatAsync(systemPrompt, userPrompt, timeoutSeconds: 30);
|
||||
if (string.IsNullOrWhiteSpace(svgContent))
|
||||
@@ -261,7 +263,7 @@ public class TransactionCategoryController(
|
||||
return "更新分类图标失败".Fail<string>();
|
||||
}
|
||||
|
||||
return svg.Ok();
|
||||
return svg.Ok<string>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user