diff --git a/Repository/TransactionRecordRepository.cs b/Repository/TransactionRecordRepository.cs index 403fa15..fc7cd30 100644 --- a/Repository/TransactionRecordRepository.cs +++ b/Repository/TransactionRecordRepository.cs @@ -58,7 +58,7 @@ public interface ITransactionRecordRepository : IBaseRepository年份 /// 月份 /// 每天的消费笔数和金额详情 - Task> GetDailyStatisticsAsync(int year, int month); + Task> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null); /// /// 获取指定日期范围内的每日统计 @@ -66,7 +66,7 @@ public interface ITransactionRecordRepository : IBaseRepository开始日期 /// 结束日期 /// 每天的消费笔数和金额详情 - Task> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate); + Task> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null); /// /// 获取指定日期范围内的交易记录 @@ -345,15 +345,15 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository t.Classify); } - public async Task> GetDailyStatisticsAsync(int year, int month) + public async Task> GetDailyStatisticsAsync(int year, int month, string? savingClassify = null) { var startDate = new DateTime(year, month, 1); var endDate = startDate.AddMonths(1); - return await GetDailyStatisticsByRangeAsync(startDate, endDate); + return await GetDailyStatisticsByRangeAsync(startDate, endDate, savingClassify); } - public async Task> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate) + public async Task> GetDailyStatisticsByRangeAsync(DateTime startDate, DateTime endDate, string? savingClassify = null) { var records = await FreeSql.Select() .Where(t => t.OccurredAt >= startDate && t.OccurredAt < endDate) @@ -368,7 +368,14 @@ public class TransactionRecordRepository(IFreeSql freeSql) : BaseRepository t.Type == TransactionType.Income).Sum(t => Math.Abs(t.Amount)); var expense = g.Where(t => t.Type == TransactionType.Expense).Sum(t => Math.Abs(t.Amount)); - return (count: g.Count(), expense: expense, income: income); + + var saving = 0m; + if(!string.IsNullOrEmpty(savingClassify)) + { + saving = g.Where(t => savingClassify.Split(',').Contains(t.Classify)).Sum(t => Math.Abs(t.Amount)); + } + + return (count: g.Count(), expense, income, saving); } ); diff --git a/Web/.prettierrc.json b/Web/.prettierrc.json index 29a2402..d7bf21d 100644 --- a/Web/.prettierrc.json +++ b/Web/.prettierrc.json @@ -2,5 +2,6 @@ "$schema": "https://json.schemastore.org/prettierrc", "semi": false, "singleQuote": true, - "printWidth": 100 + "printWidth": 100, + "trailingComma": "none" } diff --git a/Web/eslint.config.js b/Web/eslint.config.js index 5687451..fbf8299 100644 --- a/Web/eslint.config.js +++ b/Web/eslint.config.js @@ -1,52 +1,82 @@ -import js from '@eslint/js' +import js from '@eslint/js' import globals from 'globals' import pluginVue from 'eslint-plugin-vue' -import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' export default [ { - ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/node_modules/**', '.nuxt/**'], + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/node_modules/**', '.nuxt/**'] }, + // Load Vue recommended rules first (sets up parser etc.) + ...pluginVue.configs['flat/recommended'], + + // General Configuration for all JS/Vue files { - files: ['**/*.{js,mjs,jsx}'], + files: ['**/*.{js,mjs,jsx,vue}'], languageOptions: { globals: { - ...globals.browser, - }, - parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', + ...globals.browser }, + ecmaVersion: 'latest', + sourceType: 'module' }, rules: { + // Import standard JS recommended rules ...js.configs.recommended.rules, - 'indent': ['error', 2], + + // --- Logic & Best Practices --- + 'no-unused-vars': ['warn', { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_' + }], + 'no-undef': 'error', + 'no-console': ['warn', { allow: ['warn', 'error', 'info'] }], + 'no-debugger': 'warn', + 'eqeqeq': ['error', 'always', { null: 'ignore' }], + 'curly': ['error', 'all'], + 'prefer-const': 'warn', + 'no-var': 'error', + + // --- Formatting & Style (User requested warnings) --- + 'indent': ['error', 2, { SwitchCase: 1 }], 'quotes': ['error', 'single', { avoidEscape: true }], 'semi': ['error', 'never'], - 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], 'comma-dangle': ['error', 'never'], 'no-trailing-spaces': 'error', 'no-multiple-empty-lines': ['error', { max: 1 }], 'space-before-function-paren': ['error', 'always'], - }, + 'object-curly-spacing': ['error', 'always'], + 'array-bracket-spacing': ['error', 'never'] + } }, - ...pluginVue.configs['flat/recommended'], + + // Vue Specific Overrides { files: ['**/*.vue'], rules: { 'vue/multi-word-component-names': 'off', 'vue/no-v-html': 'warn', + + // Turn off standard indent for Vue files to avoid conflicts with vue/html-indent + // or script indentation issues. Vue plugin handles this better. 'indent': 'off', - }, + // Ensure Vue's own indentation rules are active (they are in 'recommended' but let's be explicit if needed) + 'vue/html-indent': ['error', 2], + 'vue/script-indent': ['error', 2, { + baseIndent: 0, + switchCase: 1, + ignores: [] + }] + } }, - skipFormatting, + + // Service Worker specific globals { files: ['**/service-worker.js', '**/src/registerServiceWorker.js'], languageOptions: { globals: { - ...globals.serviceworker, - ...globals.browser, - }, - }, - }, + ...globals.serviceworker + } + } + } ] diff --git a/Web/package.json b/Web/package.json index 96fa2ed..be1767c 100644 --- a/Web/package.json +++ b/Web/package.json @@ -1,4 +1,4 @@ -{ +{ "name": "email-bill", "version": "1.0.0", "private": true, @@ -11,7 +11,7 @@ "build": "vite build", "preview": "vite preview", "lint": "eslint . --fix --cache", - "format": "prettier --write --experimental-cli src/" + "format": "prettier --write src/" }, "dependencies": { "axios": "^1.13.2", diff --git a/Web/public/service-worker.js b/Web/public/service-worker.js index 6781abe..667b72d 100644 --- a/Web/public/service-worker.js +++ b/Web/public/service-worker.js @@ -1,56 +1,56 @@ -const VERSION = '1.0.0'; // Build Time: 2026-01-07 15:59:36 -const CACHE_NAME = `emailbill-${VERSION}`; +const VERSION = '1.0.0' // Build Time: 2026-01-07 15:59:36 +const CACHE_NAME = `emailbill-${VERSION}` const urlsToCache = [ '/', '/index.html', '/favicon.ico', '/manifest.json' -]; +] // 安装 Service Worker self.addEventListener('install', (event) => { - console.log('[Service Worker] 安装中...'); + console.log('[Service Worker] 安装中...') event.waitUntil( caches.open(CACHE_NAME) .then((cache) => { - console.log('[Service Worker] 缓存文件'); - return cache.addAll(urlsToCache); + console.log('[Service Worker] 缓存文件') + return cache.addAll(urlsToCache) }) - ); -}); + ) +}) // 监听跳过等待消息 self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { - self.skipWaiting(); + self.skipWaiting() } -}); +}) // 激活 Service Worker self.addEventListener('activate', (event) => { - console.log('[Service Worker] 激活中...'); + console.log('[Service Worker] 激活中...') event.waitUntil( caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME) { - console.log('[Service Worker] 删除旧缓存:', cacheName); - return caches.delete(cacheName); + console.log('[Service Worker] 删除旧缓存:', cacheName) + return caches.delete(cacheName) } }) - ); + ) }).then(() => self.clients.claim()) - ); -}); + ) +}) // 拦截请求 self.addEventListener('fetch', (event) => { - const { request } = event; - const url = new URL(request.url); + const { request } = event + const url = new URL(request.url) // 跳过跨域请求 if (url.origin !== location.origin) { - return; + return } // API请求使用网络优先策略 @@ -60,19 +60,19 @@ self.addEventListener('fetch', (event) => { .then((response) => { // 只针对成功的GET请求进行缓存 if (request.method === 'GET' && response.status === 200) { - const responseClone = response.clone(); + const responseClone = response.clone() caches.open(CACHE_NAME).then((cache) => { - cache.put(request, responseClone); - }); + cache.put(request, responseClone) + }) } - return response; + return response }) .catch(() => { // 网络失败时尝试从缓存获取 - return caches.match(request); + return caches.match(request) }) - ); - return; + ) + return } // 页面请求使用网络优先策略,确保能获取到最新的 index.html @@ -80,17 +80,17 @@ self.addEventListener('fetch', (event) => { event.respondWith( fetch(request) .then((response) => { - const responseClone = response.clone(); + const responseClone = response.clone() caches.open(CACHE_NAME).then((cache) => { - cache.put(request, responseClone); - }); - return response; + cache.put(request, responseClone) + }) + return response }) .catch(() => { - return caches.match('/index.html') || caches.match(request); + return caches.match('/index.html') || caches.match(request) }) - ); - return; + ) + return } // 其他静态资源使用缓存优先策略 @@ -98,50 +98,50 @@ self.addEventListener('fetch', (event) => { caches.match(request) .then((response) => { if (response) { - return response; + return response } return fetch(request).then((response) => { // 检查是否是有效响应 if (!response || response.status !== 200 || response.type !== 'basic') { - return response; + return response } - const responseClone = response.clone(); + const responseClone = response.clone() caches.open(CACHE_NAME).then((cache) => { - cache.put(request, responseClone); - }); + cache.put(request, responseClone) + }) - return response; - }); + return response + }) }) .catch(() => { // 返回离线页面或默认内容 if (request.destination === 'document') { - return caches.match('/index.html'); + return caches.match('/index.html') } }) - ); -}); + ) +}) // 后台同步 self.addEventListener('sync', (event) => { - console.log('[Service Worker] 后台同步:', event.tag); + console.log('[Service Worker] 后台同步:', event.tag) if (event.tag === 'sync-data') { - event.waitUntil(syncData()); + event.waitUntil(syncData()) } -}); +}) // 推送通知 self.addEventListener('push', (event) => { - console.log('[Service Worker] 收到推送消息'); - let data = { title: '账单管理', body: '您有新的消息', url: '/', icon: '/icons/icon-192x192.png' }; - + console.log('[Service Worker] 收到推送消息') + let data = { title: '账单管理', body: '您有新的消息', url: '/', icon: '/icons/icon-192x192.png' } + if (event.data) { try { - const json = event.data.json(); - data = { ...data, ...json }; + const json = event.data.json() + data = { ...data, ...json } } catch { - data.body = event.data.text(); + data.body = event.data.text() } } @@ -153,41 +153,41 @@ self.addEventListener('push', (event) => { tag: 'emailbill-notification', requireInteraction: false, data: { url: data.url } - }; + } event.waitUntil( self.registration.showNotification(data.title, options) - ); -}); + ) +}) // 通知点击 self.addEventListener('notificationclick', (event) => { - console.log('[Service Worker] 通知被点击'); - event.notification.close(); - const urlToOpen = event.notification.data?.url || '/'; + console.log('[Service Worker] 通知被点击') + event.notification.close() + const urlToOpen = event.notification.data?.url || '/' event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => { // 如果已经打开了该 URL,则聚焦 for (let i = 0; i < windowClients.length; i++) { - const client = windowClients[i]; + const client = windowClients[i] if (client.url === urlToOpen && 'focus' in client) { - return client.focus(); + return client.focus() } } // 否则打开新窗口 if (clients.openWindow) { - return clients.openWindow(urlToOpen); + return clients.openWindow(urlToOpen) } }) - ); -}); + ) +}) // 数据同步函数 -async function syncData() { +async function syncData () { try { // 这里添加需要同步的逻辑 - console.log('[Service Worker] 执行数据同步'); + console.log('[Service Worker] 执行数据同步') } catch (error) { - console.error('[Service Worker] 同步失败:', error); + console.error('[Service Worker] 同步失败:', error) } } diff --git a/Web/src/App.vue b/Web/src/App.vue index b984c94..110223e 100644 --- a/Web/src/App.vue +++ b/Web/src/App.vue @@ -3,30 +3,36 @@
- - 日历 - - + 日历 + 统计 - 账单 - + 预算 - - 设置 - + 设置 - - + +
新版本可用,点击刷新 @@ -85,12 +91,14 @@ onUnmounted(() => { const route = useRoute() // 根据路由判断是否显示Tabbar const showTabbar = computed(() => { - return route.path === '/' || + return ( + route.path === '/' || route.path === '/calendar' || route.path === '/message' || route.path === '/setting' || route.path === '/balance' || route.path === '/budget' + ) }) const active = ref('') @@ -116,11 +124,14 @@ setInterval(() => { }, 60 * 1000) // 每60秒更新一次未读消息数 // 监听路由变化调整 -watch(() => route.path, (newPath) => { - setActive(newPath) +watch( + () => route.path, + (newPath) => { + setActive(newPath) - messageStore.updateUnreadCount() -}) + messageStore.updateUnreadCount() + } +) const setActive = (path) => { active.value = (() => { @@ -142,9 +153,7 @@ const setActive = (path) => { } const isShowAddBill = computed(() => { - return route.path === '/' - || route.path === '/balance' - || route.path === '/message' + return route.path === '/' || route.path === '/balance' || route.path === '/message' }) onUnmounted(() => { @@ -165,7 +174,6 @@ const handleAddTransactionSuccess = () => { const event = new Event('transactions-changed') window.dispatchEvent(event) } - \ No newline at end of file + diff --git a/Web/src/components/Global/GlobalAddBill.vue b/Web/src/components/Global/GlobalAddBill.vue index c855f7f..707e5e2 100644 --- a/Web/src/components/Global/GlobalAddBill.vue +++ b/Web/src/components/Global/GlobalAddBill.vue @@ -6,11 +6,7 @@
- + diff --git a/Web/src/components/PopupContainer.vue b/Web/src/components/PopupContainer.vue index 5b30533..7462f8a 100644 --- a/Web/src/components/PopupContainer.vue +++ b/Web/src/components/PopupContainer.vue @@ -13,10 +13,12 @@
@@ -47,24 +49,24 @@ import { computed, useSlots } from 'vue' const props = defineProps({ modelValue: { type: Boolean, - required: true, + required: true }, title: { type: String, - default: '', + default: '' }, subtitle: { type: String, - default: '', + default: '' }, height: { type: String, - default: '80%', + default: '80%' }, closeable: { type: Boolean, - default: true, - }, + default: true + } }) const emit = defineEmits(['update:modelValue']) @@ -74,7 +76,7 @@ const slots = useSlots() // 双向绑定 const visible = computed({ get: () => props.modelValue, - set: (value) => emit('update:modelValue', value), + set: (value) => emit('update:modelValue', value) }) // 判断是否有操作按钮 diff --git a/Web/src/components/ReasonGroupList.vue b/Web/src/components/ReasonGroupList.vue index e0bca28..7878df9 100644 --- a/Web/src/components/ReasonGroupList.vue +++ b/Web/src/components/ReasonGroupList.vue @@ -1,10 +1,16 @@