feat: update VSCode settings for ESLint and Prettier integration
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 20s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s

chore: refactor ESLint configuration for improved linting rules and performance

fix: handle push event data parsing in service worker

style: adjust tabbar item properties for better readability in App.vue

refactor: remove unused functions and improve code clarity in TransactionDetail.vue

fix: ensure consistent event handling in CalendarView.vue

style: clean up component structure and formatting in various Vue files

chore: update launch script for better command execution

feat: add ESLint configuration file for consistent code style across the project

fix: resolve issues with button click events in multiple components
This commit is contained in:
孙诚
2026-01-07 14:33:30 +08:00
parent efdfe88155
commit b2339c1c5e
32 changed files with 380 additions and 241 deletions

198
.eslintrc.js Normal file
View File

@@ -0,0 +1,198 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
// "multiline": {
// "max": 1,
// "allowFirstLine": false
// }
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

4
.gitignore vendored
View File

@@ -401,4 +401,6 @@ FodyWeavers.xsd
.idea/ .idea/
Web/dist Web/dist
# ESLint
.eslintcache

13
.vscode/settings.json vendored
View File

@@ -1,3 +1,14 @@
{ {
"vue3snippets.enable-compile-vue-file-on-did-save-code": false "vue3snippets.enable-compile-vue-file-on-did-save-code": false,
"eslint.workingDirectories": [
"./Web"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue"
]
} }

File diff suppressed because one or more lines are too long

4
Web/.gitignore vendored
View File

@@ -399,4 +399,6 @@ FodyWeavers.xsd
.idea/ .idea/
# ESLint
.eslintcache

View File

@@ -6,8 +6,31 @@
"package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig" "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig"
}, },
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit" "source.fixAll": "explicit",
"source.fixAll.eslint": "explicit"
}, },
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"eslint.validate": [
"javascript",
"javascriptreact",
"vue"
],
"eslint.format.enable": false,
"prettier.documentSelectors": [
"**/*.vue",
"**/*.js",
"**/*.jsx",
"**/*.css",
"**/*.html"
]
} }

View File

@@ -1,26 +1,52 @@
import { defineConfig, globalIgnores } from 'eslint/config'
import globals from 'globals'
import js from '@eslint/js' import js from '@eslint/js'
import globals from 'globals'
import pluginVue from 'eslint-plugin-vue' import pluginVue from 'eslint-plugin-vue'
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
export default defineConfig([ export default [
{ {
name: 'app/files-to-lint', ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', '**/node_modules/**', '.nuxt/**'],
files: ['**/*.{js,mjs,jsx,vue}'],
}, },
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
{ {
files: ['**/*.{js,mjs,jsx}'],
languageOptions: { languageOptions: {
globals: { globals: {
...globals.browser, ...globals.browser,
}, },
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
},
rules: {
...js.configs.recommended.rules,
'indent': ['error', 2],
'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'],
},
},
...pluginVue.configs['flat/recommended'],
{
files: ['**/*.vue'],
rules: {
'vue/multi-word-component-names': 'off',
'vue/no-v-html': 'warn',
'indent': 'off',
}, },
}, },
js.configs.recommended,
...pluginVue.configs['flat/essential'],
skipFormatting, skipFormatting,
]) {
files: ['**/service-worker.js', '**/src/registerServiceWorker.js'],
languageOptions: {
globals: {
...globals.serviceworker,
...globals.browser,
},
},
},
]

View File

@@ -119,7 +119,7 @@ self.addEventListener('push', (event) => {
try { try {
const json = event.data.json(); const json = event.data.json();
data = { ...data, ...json }; data = { ...data, ...json };
} catch (e) { } catch {
data.body = event.data.text(); data.body = event.data.text();
} }
} }

View File

@@ -2,7 +2,7 @@
<van-config-provider :theme="theme" class="app-provider"> <van-config-provider :theme="theme" class="app-provider">
<div class="app-root"> <div class="app-root">
<RouterView /> <RouterView />
<van-tabbar v-model="active" v-show="showTabbar"> <van-tabbar v-show="showTabbar" v-model="active">
<van-tabbar-item name="ccalendar" icon="notes" to="/calendar"> <van-tabbar-item name="ccalendar" icon="notes" to="/calendar">
日历 日历
</van-tabbar-item> </van-tabbar-item>
@@ -13,8 +13,8 @@
name="balance" name="balance"
icon="balance-list" icon="balance-list"
:to="messageStore.unreadCount > 0 ? '/balance?tab=message' : '/balance'" :to="messageStore.unreadCount > 0 ? '/balance?tab=message' : '/balance'"
@click="handleTabClick('/balance')" :badge="messageStore.unreadCount || null"
:badge="messageStore.unreadCount || null" @click="handleTabClick('/balance')"
> >
账单 账单
</van-tabbar-item> </van-tabbar-item>
@@ -151,48 +151,6 @@ const handleAddTransactionSuccess = () => {
window.dispatchEvent(event) window.dispatchEvent(event)
} }
// 辅助函数:将 Base64 字符串转换为 Uint8Array
const urlBase64ToUint8Array = (base64String) => {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
const subscribeToPush = async () => {
if (!('serviceWorker' in navigator)) return;
// 1. 获取 VAPID 公钥
const response = await fetch('/api/notification/vapid-public-key');
const { publicKey } = await response.json();
// 2. 等待 Service Worker 准备就绪
const registration = await navigator.serviceWorker.ready;
// 3. 请求订阅
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey)
});
// 4. 将订阅信息发送给后端
// 注意:后端 PushSubscriptionEntity 字段首字母大写,这里需要转换或让后端兼容
const subJson = subscription.toJSON();
await fetch('/api/notification/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
endpoint: subJson.endpoint,
p256dh: subJson.keys.p256dh,
auth: subJson.keys.auth
})
});
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -53,13 +53,14 @@ request.interceptors.response.use(
case 400: case 400:
message = data?.message || '请求参数错误' message = data?.message || '请求参数错误'
break break
case 401: case 401: {
message = '未授权,请重新登录' message = '未授权,请重新登录'
// 清除登录状态并跳转到登录页 // 清除登录状态并跳转到登录页
const authStore = useAuthStore() const authStore = useAuthStore()
authStore.logout() authStore.logout()
router.push({ name: 'login', query: { redirect: router.currentRoute.value.fullPath } }) router.push({ name: 'login', query: { redirect: router.currentRoute.value.fullPath } })
break break
}
case 403: case 403:
message = '拒绝访问' message = '拒绝访问'
break break

View File

@@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div class="input-section" v-if="!parseResult" style="margin: 12px 12px 0 16px;"> <div v-if="!parseResult" class="input-section" style="margin: 12px 12px 0 16px;">
<van-field <van-field
v-model="text" v-model="text"
type="textarea" type="textarea"
@@ -14,9 +14,9 @@
type="primary" type="primary"
round round
block block
@click="handleParse" :loading="parsing"
:loading="parsing"
:disabled="!text.trim()" :disabled="!text.trim()"
@click="handleParse"
> >
智能解析 智能解析
</van-button> </van-button>
@@ -35,8 +35,8 @@
plain plain
round round
block block
@click="parseResult = null" class="mt-2"
class="mt-2" @click="parseResult = null"
> >
重新输入 重新输入
</van-button> </van-button>

View File

@@ -1,4 +1,5 @@
<template> <!-- eslint-disable vue/no-v-html -->
<template>
<van-popup <van-popup
v-model:show="visible" v-model:show="visible"
position="bottom" position="bottom"
@@ -17,7 +18,7 @@
<slot name="header-actions"></slot> <slot name="header-actions"></slot>
</div> </div>
</div> </div>
<!-- 子标题/统计信息 --> <!-- 子标题/统计信息 -->
<div v-if="subtitle" class="header-stats"> <div v-if="subtitle" class="header-stats">
<span class="stats-text" v-html="subtitle" /> <span class="stats-text" v-html="subtitle" />
@@ -45,24 +46,24 @@ import { computed, useSlots } from 'vue'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Boolean, type: Boolean,
required: true required: true,
}, },
title: { title: {
type: String, type: String,
default: '' default: '',
}, },
subtitle: { subtitle: {
type: String, type: String,
default: '' default: '',
}, },
height: { height: {
type: String, type: String,
default: '80%' default: '80%',
}, },
closeable: { closeable: {
type: Boolean, type: Boolean,
default: true default: true,
} },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@@ -72,7 +73,7 @@ const slots = useSlots()
// 双向绑定 // 双向绑定
const visible = computed({ const visible = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (value) => emit('update:modelValue', value) set: (value) => emit('update:modelValue', value),
}) })
// 判断是否有操作按钮 // 判断是否有操作按钮
@@ -121,10 +122,9 @@ const hasActions = computed(() => !!slots['header-actions'])
text-align: center; text-align: center;
color: var(--van-text-color, #323233); color: var(--van-text-color, #323233);
/*超出长度*/ /*超出长度*/
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.header-stats { .header-stats {

View File

@@ -40,7 +40,7 @@
{{ group.sampleClassify }} {{ group.sampleClassify }}
</van-tag> </van-tag>
<span class="count-text">{{ group.count }} </span> <span class="count-text">{{ group.count }} </span>
<span class="amount-text" v-if="group.totalAmount"> <span v-if="group.totalAmount" class="amount-text">
¥{{ Math.abs(group.totalAmount).toFixed(2) }} ¥{{ Math.abs(group.totalAmount).toFixed(2) }}
</span> </span>
</div> </div>

View File

@@ -5,8 +5,8 @@
size="small" size="small"
:loading="loading || saving" :loading="loading || saving"
:disabled="loading || saving" :disabled="loading || saving"
@click="handleClick"
class="smart-classify-btn" class="smart-classify-btn"
@click="handleClick"
> >
<template v-if="!loading && !saving"> <template v-if="!loading && !saving">
<van-icon :name="buttonIcon" /> <van-icon :name="buttonIcon" />

View File

@@ -292,16 +292,6 @@ const clearClassify = () => {
showToast('已清空分类') showToast('已清空分类')
} }
// 获取交易类型名称
const getTypeName = (type) => {
const typeMap = {
0: '支出',
1: '收入',
2: '不计入收支'
}
return typeMap[type] || '未知'
}
// 格式化日期 // 格式化日期
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return '' if (!dateString) return ''

View File

@@ -15,8 +15,8 @@
<van-checkbox <van-checkbox
v-if="showCheckbox" v-if="showCheckbox"
:model-value="isSelected(transaction.id)" :model-value="isSelected(transaction.id)"
@update:model-value="toggleSelection(transaction)"
class="checkbox-col" class="checkbox-col"
@update:model-value="toggleSelection(transaction)"
/> />
<div <div
class="transaction-card" class="transaction-card"
@@ -66,10 +66,10 @@
<div :class="['amount', getAmountClass(transaction.type)]"> <div :class="['amount', getAmountClass(transaction.type)]">
{{ formatAmount(transaction.amount, transaction.type) }} {{ formatAmount(transaction.amount, transaction.type) }}
</div> </div>
<div class="balance" v-if="transaction.balance && transaction.balance > 0"> <div v-if="transaction.balance && transaction.balance > 0" class="balance">
余额: {{ formatMoney(transaction.balance) }} 余额: {{ formatMoney(transaction.balance) }}
</div> </div>
<div class="balance" v-if="transaction.refundAmount && transaction.refundAmount > 0"> <div v-if="transaction.refundAmount && transaction.refundAmount > 0" class="balance">
退款: {{ formatMoney(transaction.refundAmount) }} 退款: {{ formatMoney(transaction.refundAmount) }}
</div> </div>
</div> </div>
@@ -77,7 +77,7 @@
</div> </div>
</div> </div>
</div> </div>
<template #right v-if="showDelete"> <template v-if="showDelete" #right>
<van-button <van-button
square square
type="danger" type="danger"
@@ -161,6 +161,7 @@ const handleDeleteClick = async (transaction) => {
window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transaction.id })) window.dispatchEvent(new CustomEvent('transaction-deleted', { detail: transaction.id }))
} catch (e) { } catch (e) {
// ignore in non-browser environment // ignore in non-browser environment
console.log('非浏览器环境,跳过派发 transaction-deleted 事件', e)
} }
} else { } else {
showToast(response.message || '删除失败') showToast(response.message || '删除失败')

View File

@@ -1,5 +1,6 @@
<template> <!-- eslint-disable vue/no-v-html -->
<div class="page-container-flex"> <template>
<div class="page-container-flex">
<!-- 顶部导航栏 --> <!-- 顶部导航栏 -->
<van-nav-bar <van-nav-bar
title="智能分析" title="智能分析"
@@ -11,8 +12,8 @@
<van-icon <van-icon
name="question-o" name="question-o"
size="20" size="20"
@click="onClickPrompt"
style="cursor: pointer; padding-right: 12px;" style="cursor: pointer; padding-right: 12px;"
@click="onClickPrompt"
/> />
</template> </template>
</van-nav-bar> </van-nav-bar>
@@ -39,8 +40,8 @@
type="primary" type="primary"
plain plain
size="medium" size="medium"
@click="selectQuestion(q)"
class="quick-tag" class="quick-tag"
@click="selectQuestion(q)"
> >
{{ q }} {{ q }}
</van-tag> </van-tag>
@@ -52,26 +53,26 @@
round round
:loading="analyzing" :loading="analyzing"
loading-text="分析中..." loading-text="分析中..."
@click="startAnalysis"
:disabled="!userInput.trim()" :disabled="!userInput.trim()"
@click="startAnalysis"
> >
开始分析 开始分析
</van-button> </van-button>
</div> </div>
<!-- 结果区域 --> <!-- 结果区域 -->
<div class="result-section" v-if="showResult"> <div v-if="showResult" class="result-section">
<div class="result-header"> <div class="result-header">
<h3>分析结果</h3> <h3>分析结果</h3>
<van-icon <van-icon
v-if="!analyzing"
name="delete-o" name="delete-o"
size="18" size="18"
@click="clearResult" @click="clearResult"
v-if="!analyzing"
/> />
</div> </div>
<div class="result-content" ref="resultContainer"> <div ref="resultContainer" class="result-content">
<div v-html="resultHtml"></div> <div v-html="resultHtml"></div>
<van-loading v-if="analyzing" class="result-loading"> <van-loading v-if="analyzing" class="result-loading">
AI正在分析中... AI正在分析中...
@@ -202,7 +203,7 @@ const startAnalysis = async () => {
resultHtml.value = '' resultHtml.value = ''
try { try {
var baseUrl = import.meta.env.VITE_API_BASE_URL || '' const baseUrl = import.meta.env.VITE_API_BASE_URL || ''
const response = await fetch(`${baseUrl}/TransactionRecord/AnalyzeBill`, { const response = await fetch(`${baseUrl}/TransactionRecord/AnalyzeBill`, {
method: 'POST', method: 'POST',
headers: { headers: {

View File

@@ -45,7 +45,7 @@
<van-tag v-else type="success" size="small" plain>进行中</van-tag> <van-tag v-else type="success" size="small" plain>进行中</van-tag>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<van-button icon="replay" size="mini" plain @click="handleSync(budget)" :loading="budget.syncing" /> <van-button icon="replay" size="mini" plain :loading="budget.syncing" @click="handleSync(budget)" />
<van-button :icon="budget.isStopped ? 'play' : 'pause'" size="mini" plain @click="handleToggleStop(budget)" /> <van-button :icon="budget.isStopped ? 'play' : 'pause'" size="mini" plain @click="handleToggleStop(budget)" />
</div> </div>
</div> </div>
@@ -139,7 +139,7 @@
<van-tag v-else type="success" size="small" plain>进行中</van-tag> <van-tag v-else type="success" size="small" plain>进行中</van-tag>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<van-button icon="replay" size="mini" plain round @click="handleSync(budget)" :loading="budget.syncing" /> <van-button icon="replay" size="mini" plain round :loading="budget.syncing" @click="handleSync(budget)" />
<van-button :icon="budget.isStopped ? 'play' : 'pause'" size="mini" plain round @click="handleToggleStop(budget)" /> <van-button :icon="budget.isStopped ? 'play' : 'pause'" size="mini" plain round @click="handleToggleStop(budget)" />
</div> </div>
</div> </div>
@@ -233,7 +233,7 @@
<van-tag v-else type="success" size="small" plain>积累中</van-tag> <van-tag v-else type="success" size="small" plain>积累中</van-tag>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<van-button icon="replay" size="mini" plain round @click="handleSync(budget)" :loading="budget.syncing" /> <van-button icon="replay" size="mini" plain round :loading="budget.syncing" @click="handleSync(budget)" />
<van-button :icon="budget.isStopped ? 'play' : 'pause'" size="mini" plain round @click="handleToggleStop(budget)" /> <van-button :icon="budget.isStopped ? 'play' : 'pause'" size="mini" plain round @click="handleToggleStop(budget)" />
</div> </div>
</div> </div>

View File

@@ -260,7 +260,7 @@ const now = new Date();
fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1); fetchDailyStatistics(now.getFullYear(), now.getMonth() + 1);
// 全局删除事件监听,确保日历页面数据一致 // 全局删除事件监听,确保日历页面数据一致
const onGlobalTransactionDeleted = (e) => { const onGlobalTransactionDeleted = () => {
if (selectedDate.value) { if (selectedDate.value) {
fetchDateTransactions(selectedDate.value) fetchDateTransactions(selectedDate.value)
} }
@@ -275,7 +275,7 @@ onBeforeUnmount(() => {
}) })
// 当有交易被新增/修改/批量更新时刷新 // 当有交易被新增/修改/批量更新时刷新
const onGlobalTransactionsChanged = (e) => { const onGlobalTransactionsChanged = () => {
if (selectedDate.value) { if (selectedDate.value) {
fetchDateTransactions(selectedDate.value) fetchDateTransactions(selectedDate.value)
} }

View File

@@ -4,8 +4,8 @@
title="批量分类" title="批量分类"
left-text="返回" left-text="返回"
left-arrow left-arrow
@click-left="handleBack" placeholder
placeholder @click-left="handleBack"
/> />
<div class="scroll-content"> <div class="scroll-content">
@@ -65,7 +65,7 @@ const loadUnclassifiedCount = async () => {
} }
// 处理数据加载完成 // 处理数据加载完成
const handleDataLoaded = ({ groups, total, finished: isFinished }) => { const handleDataLoaded = ({ groups, finished: isFinished }) => {
hasData.value = groups.length > 0 hasData.value = groups.length > 0
finished.value = isFinished finished.value = isFinished
} }

View File

@@ -4,8 +4,8 @@
:title="navTitle" :title="navTitle"
left-text="返回" left-text="返回"
left-arrow left-arrow
@click-left="handleBack" placeholder
placeholder @click-left="handleBack"
/> />
<div class="scroll-content"> <div class="scroll-content">
@@ -29,8 +29,8 @@
<van-tag <van-tag
type="primary" type="primary"
closeable closeable
@close="handleBackToRoot"
style="margin-left: 16px;" style="margin-left: 16px;"
@close="handleBackToRoot"
> >
{{ currentTypeName }} {{ currentTypeName }}
</van-tag> </van-tag>

View File

@@ -102,9 +102,9 @@
:finished="true" :finished="true"
:show-checkbox="true" :show-checkbox="true"
:selected-ids="selectedIds" :selected-ids="selectedIds"
:show-delete="false"
@update:selected-ids="updateSelectedIds" @update:selected-ids="updateSelectedIds"
@click="handleRecordClick" @click="handleRecordClick"
:show-delete="false"
/> />
</div> </div>
</div> </div>

View File

@@ -33,8 +33,8 @@
:loading="classifying" :loading="classifying"
:disabled="selectedCount === 0" :disabled="selectedCount === 0"
round round
@click="startClassify"
class="action-btn" class="action-btn"
@click="startClassify"
> >
{{ classifying ? '分类中...' : `开始分类 (${selectedCount}组)` }} {{ classifying ? '分类中...' : `开始分类 (${selectedCount}组)` }}
</van-button> </van-button>
@@ -43,8 +43,8 @@
type="success" type="success"
:disabled="!hasChanges || classifying" :disabled="!hasChanges || classifying"
round round
@click="saveClassifications"
class="action-btn" class="action-btn"
@click="saveClassifications"
> >
保存分类 保存分类
</van-button> </van-button>
@@ -91,7 +91,7 @@ const loadUnclassifiedCount = async () => {
} }
// 处理数据加载完成 // 处理数据加载完成
const handleDataLoaded = ({ groups, total }) => { const handleDataLoaded = ({ total }) => {
totalGroups.value = total totalGroups.value = total
// 默认全选所有分组 // 默认全选所有分组
if (groupListRef.value) { if (groupListRef.value) {

View File

@@ -1,4 +1,5 @@
<template> <!-- eslint-disable vue/no-v-html -->
<template>
<div class="page-container-flex"> <div class="page-container-flex">
<!-- 下拉刷新区域 --> <!-- 下拉刷新区域 -->
<van-pull-refresh v-model="refreshing" @refresh="onRefresh"> <van-pull-refresh v-model="refreshing" @refresh="onRefresh">
@@ -27,7 +28,7 @@
<template #value> <template #value>
<div class="email-info"> <div class="email-info">
<div class="email-date">{{ formatDate(email.receivedDate) }}</div> <div class="email-date">{{ formatDate(email.receivedDate) }}</div>
<div class="bill-count" v-if="email.transactionCount > 0"> <div v-if="email.transactionCount > 0" class="bill-count">
<span style="font-size: 12px;">已解析{{ email.transactionCount }}条账单</span> <span style="font-size: 12px;">已解析{{ email.transactionCount }}条账单</span>
</div> </div>
@@ -79,19 +80,19 @@
<van-cell title="接收时间" :value="formatDate(currentEmail.ReceivedDate || currentEmail.receivedDate)" /> <van-cell title="接收时间" :value="formatDate(currentEmail.ReceivedDate || currentEmail.receivedDate)" />
<van-cell title="记录时间" :value="formatDate(currentEmail.CreateTime || currentEmail.createTime)" /> <van-cell title="记录时间" :value="formatDate(currentEmail.CreateTime || currentEmail.createTime)" />
<van-cell <van-cell
v-if="(currentEmail.TransactionCount || currentEmail.transactionCount || 0) > 0"
title="已解析账单数" title="已解析账单数"
:value="`${currentEmail.TransactionCount || currentEmail.transactionCount || 0}条`" :value="`${currentEmail.TransactionCount || currentEmail.transactionCount || 0}条`"
is-link is-link
@click="viewTransactions" @click="viewTransactions"
v-if="(currentEmail.TransactionCount || currentEmail.transactionCount || 0) > 0"
/> />
</van-cell-group> </van-cell-group>
<div class="email-content"> <div class="email-content">
<h4 style="margin-left: 10px;">邮件内容</h4> <h4 style="margin-left: 10px;">邮件内容</h4>
<div <div
v-if="currentEmail.htmlBody" v-if="currentEmail.htmlBody"
v-html="currentEmail.htmlBody" class="content-body html-content"
class="content-body html-content" v-html="currentEmail.htmlBody"
></div> ></div>
<div <div
v-else-if="currentEmail.body" v-else-if="currentEmail.body"
@@ -453,6 +454,8 @@ const formatDate = (dateString) => {
}) })
} }
onMounted(() => { onMounted(() => {
loadData(true) loadData(true)
}) })

View File

@@ -4,8 +4,8 @@
title="查看日志" title="查看日志"
left-text="返回" left-text="返回"
left-arrow left-arrow
@click-left="handleBack" placeholder
placeholder @click-left="handleBack"
/> />
<div class="scroll-content"> <div class="scroll-content">
@@ -38,8 +38,8 @@
v-model:loading="loading" v-model:loading="loading"
:finished="finished" :finished="finished"
finished-text="没有更多了" finished-text="没有更多了"
@load="onLoad"
class="log-list" class="log-list"
@load="onLoad"
> >
<div <div
v-for="(log, index) in logList" v-for="(log, index) in logList"

View File

@@ -21,8 +21,8 @@
block block
round round
:loading="loading" :loading="loading"
@click="handleLogin"
class="login-button" class="login-button"
@click="handleLogin"
> >
登录 登录
</van-button> </van-button>

View File

@@ -1,4 +1,5 @@
<template> <!-- eslint-disable vue/no-v-html -->
<template>
<div class="page-container-flex"> <div class="page-container-flex">
<van-nav-bar v-if="!isComponent" title="消息中心"> <van-nav-bar v-if="!isComponent" title="消息中心">
<template #right> <template #right>
@@ -49,7 +50,11 @@
height="50%" height="50%"
:closeable="true" :closeable="true"
> >
<div v-if="currentMessage.messageType === 2" class="detail-content" v-html="currentMessage.content"> <div
v-if="currentMessage.messageType === 2"
class="detail-content"
v-html="currentMessage.content"
>
</div> </div>
<div v-else class="detail-content"> <div v-else class="detail-content">
{{ currentMessage.content }} {{ currentMessage.content }}
@@ -207,12 +212,6 @@ const handleMarkAllRead = () => {
onMounted(() => { onMounted(() => {
// onLoad 会由 van-list 自动触发 // onLoad 会由 van-list 自动触发
}); });
const props = defineProps({
isComponent: {
type: Boolean,
default: false
}
});
defineExpose({ defineExpose({
handleMarkAllRead handleMarkAllRead

View File

@@ -4,8 +4,8 @@
:title="navTitle" :title="navTitle"
left-text="返回" left-text="返回"
left-arrow left-arrow
@click-left="handleBack" placeholder
placeholder @click-left="handleBack"
/> />
<!-- 下拉刷新区域 --> <!-- 下拉刷新区域 -->
@@ -20,10 +20,10 @@
v-model:loading="loading" v-model:loading="loading"
:finished="finished" :finished="finished"
finished-text="没有更多了" finished-text="没有更多了"
@load="onLoad"
class="periodic-list" class="periodic-list"
@load="onLoad"
> >
<van-cell-group inset v-for="item in periodicList" :key="item.id" class="periodic-item"> <van-cell-group v-for="item in periodicList" :key="item.id" inset class="periodic-item">
<van-swipe-cell> <van-swipe-cell>
<div @click="editPeriodic(item)"> <div @click="editPeriodic(item)">
<van-cell :title="item.reason || '无摘要'" :label="getPeriodicTypeText(item)"> <van-cell :title="item.reason || '无摘要'" :label="getPeriodicTypeText(item)">
@@ -99,8 +99,8 @@
name="periodicType" name="periodicType"
label="周期" label="周期"
placeholder="请选择周期类型" placeholder="请选择周期类型"
@click="showPeriodicTypePicker = true"
:rules="[{ required: true, message: '请选择周期类型' }]" :rules="[{ required: true, message: '请选择周期类型' }]"
@click="showPeriodicTypePicker = true"
/> />
<!-- 每周配置 --> <!-- 每周配置 -->
@@ -112,8 +112,8 @@
name="weekdays" name="weekdays"
label="星期" label="星期"
placeholder="请选择星期几" placeholder="请选择星期几"
@click="showWeekdaysPicker = true"
:rules="[{ required: true, message: '请选择星期几' }]" :rules="[{ required: true, message: '请选择星期几' }]"
@click="showWeekdaysPicker = true"
/> />
<!-- 每月配置 --> <!-- 每月配置 -->
@@ -125,8 +125,8 @@
name="monthDays" name="monthDays"
label="日期" label="日期"
placeholder="请选择每月的日期" placeholder="请选择每月的日期"
@click="showMonthDaysPicker = true"
:rules="[{ required: true, message: '请选择日期' }]" :rules="[{ required: true, message: '请选择日期' }]"
@click="showMonthDaysPicker = true"
/> />
<!-- 每季度配置 --> <!-- 每季度配置 -->
@@ -225,7 +225,7 @@
</van-cell-group> </van-cell-group>
</van-form> </van-form>
<template #footer> <template #footer>
<van-button round block type="primary" @click="submit" :loading="submitting"> <van-button round block type="primary" :loading="submitting" @click="submit">
{{ isEdit ? '更新' : '确认添加' }} {{ isEdit ? '更新' : '确认添加' }}
</van-button> </van-button>
</template> </template>
@@ -267,13 +267,11 @@
</template> </template>
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast, showConfirmDialog } from 'vant' import { showToast, showConfirmDialog } from 'vant'
import { import {
getPeriodicList, getPeriodicList,
createPeriodic,
updatePeriodic,
deletePeriodic as deletePeriodicApi, deletePeriodic as deletePeriodicApi,
togglePeriodicEnabled togglePeriodicEnabled
} from '@/api/transactionPeriodic' } from '@/api/transactionPeriodic'
@@ -643,80 +641,6 @@ const handleAddClassify = async (categoryName) => {
} }
} }
// 提交表单
const onSubmit = async () => {
try {
submitting.value = true
// 构建周期配置
let periodicConfig = ''
switch (form.periodicType) {
case 1: // 每周
if (!form.weekdays.length) {
showToast('请选择星期几')
return
}
periodicConfig = form.weekdays.join(',')
break
case 2: // 每月
if (!form.monthDays.length) {
showToast('请选择日期')
return
}
periodicConfig = form.monthDays.join(',')
break
case 3: // 每季度
if (!form.quarterDay) {
showToast('请输入季度开始后第几天')
return
}
periodicConfig = form.quarterDay
break
case 4: // 每年
if (!form.yearDay) {
showToast('请输入年开始后第几天')
return
}
periodicConfig = form.yearDay
break
}
const data = {
periodicType: form.periodicType,
periodicConfig: periodicConfig,
amount: parseFloat(form.amount),
type: form.type,
classify: form.classify || '',
reason: form.reason || ''
}
let response
if (isEdit.value) {
data.id = form.id
data.isEnabled = true
response = await updatePeriodic(data)
} else {
response = await createPeriodic(data)
}
if (response.success) {
showToast(isEdit.value ? '更新成功' : '添加成功')
dialogVisible.value = false
loadData(true)
} else {
showToast(response.message || (isEdit.value ? '更新失败' : '添加失败'))
}
} catch (error) {
console.error('提交出错:', error)
showToast((isEdit.value ? '更新' : '添加') + '失败')
} finally {
submitting.value = false
}
}
onMounted(() => {
// van-list 会自动触发 onLoad
})
</script> </script>
<style scoped> <style scoped>

View File

@@ -30,10 +30,10 @@
<van-cell-group inset> <van-cell-group inset>
<van-cell title="开启消息通知"> <van-cell title="开启消息通知">
<template #right-icon> <template #right-icon>
<van-switch v-model="notificationEnabled" @change="handleNotificationToggle" size="24" :loading="notificationLoading" /> <van-switch v-model="notificationEnabled" size="24" :loading="notificationLoading" @change="handleNotificationToggle" />
</template> </template>
</van-cell> </van-cell>
<van-cell title="测试通知" is-link @click="handleTestNotification" v-if="notificationEnabled" /> <van-cell v-if="notificationEnabled" title="测试通知" is-link @click="handleTestNotification" />
</van-cell-group> </van-cell-group>
<div class="detail-header" style="padding-bottom: 5px;"> <div class="detail-header" style="padding-bottom: 5px;">

View File

@@ -23,8 +23,8 @@
icon="arrow" icon="arrow"
plain plain
size="small" size="small"
@click="changeMonth(1)"
:disabled="isCurrentMonth" :disabled="isCurrentMonth"
@click="changeMonth(1)"
/> />
</div> </div>
@@ -69,7 +69,7 @@
</div> </div>
<!-- 环形图区域 --> <!-- 环形图区域 -->
<div class="chart-container" v-if="expenseCategoriesView.length > 0"> <div v-if="expenseCategoriesView.length > 0" class="chart-container">
<div class="ring-chart"> <div class="ring-chart">
<svg viewBox="0 0 200 200" class="ring-svg"> <svg viewBox="0 0 200 200" class="ring-svg">
<circle <circle
@@ -95,7 +95,7 @@
</div> </div>
<!-- 分类列表 --> <!-- 分类列表 -->
<div class="category-list" v-if="expenseCategoriesView.length > 0"> <div v-if="expenseCategoriesView.length > 0" class="category-list">
<div <div
v-for="(category) in expenseCategoriesView" v-for="(category) in expenseCategoriesView"
:key="category.classify" :key="category.classify"
@@ -125,7 +125,7 @@
</div> </div>
<!-- 收入分类统计 --> <!-- 收入分类统计 -->
<div class="common-card" v-if="incomeCategoriesView.length > 0"> <div v-if="incomeCategoriesView.length > 0" class="common-card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">收入分类统计</h3> <h3 class="card-title">收入分类统计</h3>
<van-tag type="success" size="medium">{{ incomeCategoriesView.length }}</van-tag> <van-tag type="success" size="medium">{{ incomeCategoriesView.length }}</van-tag>
@@ -155,7 +155,7 @@
</div> </div>
<!-- 不计收支分类统计 --> <!-- 不计收支分类统计 -->
<div class="common-card" v-if="noneCategoriesView.length > 0"> <div v-if="noneCategoriesView.length > 0" class="common-card">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">不计收支分类统计</h3> <h3 class="card-title">不计收支分类统计</h3>
<van-tag type="info" size="medium">{{ noneCategoriesView.length }}</van-tag> <van-tag type="info" size="medium">{{ noneCategoriesView.length }}</van-tag>
@@ -202,7 +202,7 @@
class="bar expense-bar" class="bar expense-bar"
:style="{ height: getBarHeight(item.expense, maxTrendValue) }" :style="{ height: getBarHeight(item.expense, maxTrendValue) }"
> >
<div class="bar-value" v-if="item.expense > 0"> <div v-if="item.expense > 0" class="bar-value">
{{ formatShortMoney(item.expense) }} {{ formatShortMoney(item.expense) }}
</div> </div>
</div> </div>
@@ -210,7 +210,7 @@
class="bar income-bar" class="bar income-bar"
:style="{ height: getBarHeight(item.income, maxTrendValue) }" :style="{ height: getBarHeight(item.income, maxTrendValue) }"
> >
<div class="bar-value" v-if="item.income > 0"> <div v-if="item.income > 0" class="bar-value">
{{ formatShortMoney(item.income) }} {{ formatShortMoney(item.income) }}
</div> </div>
</div> </div>
@@ -285,10 +285,10 @@
> >
<template #header-actions> <template #header-actions>
<SmartClassifyButton <SmartClassifyButton
ref="smartClassifyButtonRef"
v-if="isUnclassified" v-if="isUnclassified"
ref="smartClassifyButtonRef"
:transactions="categoryBills" :transactions="categoryBills"
:onBeforeClassify="beforeSmartClassify" :on-before-classify="beforeSmartClassify"
@save="onSmartClassifySave" @save="onSmartClassifySave"
@notify-doned-transaction-id="handleNotifiedTransactionId" @notify-doned-transaction-id="handleNotifiedTransactionId"
/> />

View File

@@ -41,9 +41,9 @@
<van-search <van-search
v-model="searchKeyword" v-model="searchKeyword"
placeholder="搜索交易摘要、来源、卡号、分类" placeholder="搜索交易摘要、来源、卡号、分类"
shape="round"
@update:model-value="onSearchChange" @update:model-value="onSearchChange"
@clear="onSearchClear" @clear="onSearchClear"
shape="round"
/> />
</div> </div>
</div> </div>

View File

@@ -1,2 +1,2 @@
start cd ./Web/; pnpm i ;pnpm dev; start cmd /k "cd Web && pnpm i && pnpm dev"
start cd ./WebApi/; dotnet watch run; start cmd /k "cd WebApi && dotnet watch run"