-
+
日历
-
+
+ 统计
+
+
账单
-
+
邮件
-
+
设置
@@ -53,7 +56,8 @@ const showTabbar = computed(() => {
return route.path === '/' ||
route.path === '/calendar' ||
route.path === '/email' ||
- route.path === '/setting'
+ route.path === '/setting' ||
+ route.path === '/statistics'
})
const active = ref(0)
diff --git a/Web/src/api/transactionCategory.js b/Web/src/api/transactionCategory.js
index 1c998fa..4736c33 100644
--- a/Web/src/api/transactionCategory.js
+++ b/Web/src/api/transactionCategory.js
@@ -1,44 +1,18 @@
import request from './request'
/**
- * 获取分类树(支持按类型筛选)
+ * 获取分类列表(支持按类型筛选)
* @param {string|null} type - 交易类型(Expense=0/Income=1),null表示获取全部
* @returns {Promise<{success: boolean, data: Array}>}
*/
-export const getCategoryTree = (type = null) => {
+export const getCategoryList = (type = null) => {
return request({
- url: '/TransactionCategory/GetTree',
+ url: '/TransactionCategory/GetList',
method: 'get',
params: type !== null ? { type } : {}
})
}
-/**
- * 获取顶级分类列表(按类型)
- * @param {number} type - 交易类型(Expense=0/Income=1)
- * @returns {Promise<{success: boolean, data: Array}>}
- */
-export const getTopLevelCategories = (type) => {
- return request({
- url: '/TransactionCategory/GetTopLevel',
- method: 'get',
- params: { type }
- })
-}
-
-/**
- * 获取子分类列表
- * @param {number} parentId - 父分类ID
- * @returns {Promise<{success: boolean, data: Array}>}
- */
-export const getChildCategories = (parentId) => {
- return request({
- url: '/TransactionCategory/GetChildren',
- method: 'get',
- params: { parentId }
- })
-}
-
/**
* 根据ID获取分类详情
* @param {number} id - 分类ID
@@ -53,7 +27,7 @@ export const getCategoryById = (id) => {
/**
* 创建分类
- * @param {object} data - 分类数据
+ * @param {object} data - 分类数据 { name, type }
* @returns {Promise<{success: boolean, data: number}>} 返回新创建的分类ID
*/
export const createCategory = (data) => {
@@ -66,7 +40,7 @@ export const createCategory = (data) => {
/**
* 更新分类
- * @param {object} data - 分类数据
+ * @param {object} data - 分类数据 { id, name }
* @returns {Promise<{success: boolean}>}
*/
export const updateCategory = (data) => {
@@ -92,7 +66,7 @@ export const deleteCategory = (id) => {
/**
* 批量创建分类(用于初始化)
- * @param {Array} dataList - 分类数据数组
+ * @param {Array} dataList - 分类数据数组 [{ name, type }, ...]
* @returns {Promise<{success: boolean, data: number}>} 返回创建的数量
*/
export const batchCreateCategories = (dataList) => {
diff --git a/Web/src/api/transactionRecord.js b/Web/src/api/transactionRecord.js
index 1e716b3..6d552e0 100644
--- a/Web/src/api/transactionRecord.js
+++ b/Web/src/api/transactionRecord.js
@@ -41,7 +41,6 @@ export const getTransactionDetail = (id) => {
* @param {number} data.balance - 交易后余额
* @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
* @param {string} data.classify - 交易分类
- * @param {string} data.subClassify - 交易子分类
* @returns {Promise<{success: boolean}>}
*/
export const createTransaction = (data) => {
@@ -60,7 +59,6 @@ export const createTransaction = (data) => {
* @param {number} data.balance - 交易后余额
* @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
* @param {string} data.classify - 交易分类
- * @param {string} data.subClassify - 交易子分类
* @returns {Promise<{success: boolean}>}
*/
export const updateTransaction = (data) => {
@@ -99,7 +97,7 @@ export const getTransactionsByDate = (date) => {
// 注意:分类相关的API已迁移到 transactionCategory.js
-// 请使用 getCategoryTree 等新接口
+// 请使用 getCategoryList 等新接口
/**
* 获取未分类的账单数量
@@ -127,13 +125,13 @@ export const getUnclassified = (pageSize = 10) => {
/**
* 智能分类 - 使用AI对账单进行分类(EventSource流式响应)
- * @param {number} pageSize - 每次分类的账单数量
- * @returns {EventSource} 返回EventSource对象用于接收流式数据
+ * @param {Array
} transactionIds - 要分类的账单ID列表
+ * @returns {Promise} 返回响应对象用于接收流式数据
*/
-export const smartClassify = (pageSize = 10) => {
- const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
+export const smartClassify = (transactionIds = []) => {
+ const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:5071/api'
const token = localStorage.getItem('token')
- const url = `${baseURL}/api/TransactionRecord/SmartClassify`
+ const url = `${baseURL}/TransactionRecord/SmartClassify`
return fetch(url, {
method: 'POST',
@@ -141,7 +139,7 @@ export const smartClassify = (pageSize = 10) => {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
- body: JSON.stringify({ pageSize })
+ body: JSON.stringify({ transactionIds })
})
}
@@ -149,8 +147,7 @@ export const smartClassify = (pageSize = 10) => {
* 批量更新账单分类
* @param {Array} items - 要更新的账单分类数据数组
* @param {number} items[].id - 账单ID
- * @param {string} items[].classify - 一级分类
- * @param {string} items[].subClassify - 子分类
+ * @param {string} items[].classify - 分类
* @returns {Promise<{success: boolean, message: string}>}
*/
export const batchUpdateClassify = (items) => {
@@ -160,3 +157,46 @@ export const batchUpdateClassify = (items) => {
data: items
})
}
+
+/**
+ * 获取按交易摘要分组的统计信息(支持分页)
+ * @param {number} pageIndex - 页码,从1开始
+ * @param {number} pageSize - 每页数量,默认20
+ * @returns {Promise<{success: boolean, data: Array, total: number}>}
+ */
+export const getReasonGroups = (pageIndex = 1, pageSize = 20) => {
+ return request({
+ url: '/TransactionRecord/GetReasonGroups',
+ method: 'get',
+ params: { pageIndex, pageSize }
+ })
+}
+
+/**
+ * 按摘要批量更新分类
+ * @param {Object} data - 批量更新数据
+ * @param {string} data.reason - 交易摘要
+ * @param {number} data.type - 交易类型 (0:支出, 1:收入, 2:不计入收支)
+ * @param {string} data.classify - 分类名称
+ * @returns {Promise<{success: boolean, data: number, message: string}>}
+ */
+export const batchUpdateByReason = (data) => {
+ return request({
+ url: '/TransactionRecord/BatchUpdateByReason',
+ method: 'post',
+ data
+ })
+}
+
+/**
+ * NLP分析 - 根据用户自然语言输入查询交易记录并预设分类
+ * @param {string} userInput - 用户的自然语言输入
+ * @returns {Promise<{success: boolean, data: Object}>}
+ */
+export const nlpAnalysis = (userInput) => {
+ return request({
+ url: '/TransactionRecord/NlpAnalysis',
+ method: 'post',
+ data: { userInput }
+ })
+}
diff --git a/Web/src/components/TransactionDetail.vue b/Web/src/components/TransactionDetail.vue
index 40782b9..4475a0b 100644
--- a/Web/src/components/TransactionDetail.vue
+++ b/Web/src/components/TransactionDetail.vue
@@ -57,24 +57,43 @@
@click="showTypePicker = true"
:rules="[{ required: true, message: '请选择交易类型' }]"
/>
-
-
+
+
+ 请选择交易分类
+ {{ editForm.classify }}
+
+
+
+
+
+
+ {{ item.text }}
+
+
+ + 新增
+
+
+ 清空
+
+
@@ -96,42 +115,6 @@
/>
-
-
-
-
-
- 清空
- 新增
- 确认
-
-
-
-
-
-
-
-
-
-
- 清空
- 新增
- 确认
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/Web/src/views/ClassificationBatch.vue b/Web/src/views/ClassificationBatch.vue
new file mode 100644
index 0000000..1267385
--- /dev/null
+++ b/Web/src/views/ClassificationBatch.vue
@@ -0,0 +1,515 @@
+
+
+
+
+
+
+ 未分类账单数: {{ unclassifiedCount }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ group.reason }}
+
+
+
+
+
+ {{ getTypeName(group.sampleType) }}
+
+
+ {{ group.sampleClassify }}
+
+ {{ group.count }} 条记录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 请选择分类
+ {{ form.classify }}
+
+
+
+
+
+
+ {{ item.text }}
+
+
+ + 新增
+
+
+ 清空
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web/src/views/ClassificationEdit.vue b/Web/src/views/ClassificationEdit.vue
new file mode 100644
index 0000000..acced73
--- /dev/null
+++ b/Web/src/views/ClassificationEdit.vue
@@ -0,0 +1,338 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ currentTypeName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 新增分类
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web/src/views/ClassificationNLP.vue b/Web/src/views/ClassificationNLP.vue
new file mode 100644
index 0000000..eee3ef3
--- /dev/null
+++ b/Web/src/views/ClassificationNLP.vue
@@ -0,0 +1,371 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Web/src/views/SmartClassification.vue b/Web/src/views/ClassificationSmart.vue
similarity index 71%
rename from Web/src/views/SmartClassification.vue
rename to Web/src/views/ClassificationSmart.vue
index ba01000..687f849 100644
--- a/Web/src/views/SmartClassification.vue
+++ b/Web/src/views/ClassificationSmart.vue
@@ -7,7 +7,7 @@
@click-left="onClickLeft"
/>
-
+
未分类账单:
@@ -18,9 +18,11 @@
@@ -36,11 +38,11 @@
- {{ classifying ? '分类中...' : '开始智能分类' }}
+ {{ classifying ? '分类中...' : `开始分类 (${selectedIds.size}/${records.length})` }}
{
if (hasChanges.value) {
@@ -104,7 +108,7 @@ const loadUnclassifiedCount = async () => {
// 加载未分类账单列表
const loadUnclassified = async () => {
- const toast = showLoadingToast({
+ showLoadingToast({
message: '加载中...',
forbidClick: true,
duration: 0
@@ -114,6 +118,8 @@ const loadUnclassified = async () => {
const res = await getUnclassified(10)
if (res.success) {
records.value = res.data
+ // 默认全选所有账单
+ selectedIds.value = new Set(res.data.map(r => r.id))
} else {
showToast(res.message || '加载失败')
}
@@ -127,15 +133,24 @@ const loadUnclassified = async () => {
// 开始智能分类
const startClassify = async () => {
- if (records.value.length === 0) {
- showToast('没有需要分类的账单')
+ const idsToClassify = Array.from(selectedIds.value)
+
+ if (idsToClassify.length === 0) {
+ showToast('请先选择要分类的账单')
return
}
+ const toast = showLoadingToast({
+ message: '智能分类中...',
+ forbidClick: true,
+ duration: 0
+ })
+
classifying.value = true
+ classifyBuffer.value = '' // 重置缓冲区
try {
- const response = await smartClassify(10)
+ const response = await smartClassify(idsToClassify)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
@@ -173,49 +188,83 @@ const startClassify = async () => {
showToast(`分类失败: ${error.message}`)
} finally {
classifying.value = false
+ classifyBuffer.value = ''
+ closeToast()
}
}
// 处理SSE事件
const handleSSEEvent = (eventType, data) => {
if (eventType === 'data') {
- // 尝试解析JSON数据并更新对应账单的分类
try {
- // 累积JSON片段
- if (!window.classifyBuffer) {
- window.classifyBuffer = ''
- }
- window.classifyBuffer += data
+ // 累积AI输出的JSON片段
+ classifyBuffer.value += data
- // 尝试提取完整的JSON对象
- const jsonMatches = window.classifyBuffer.match(/\{[^}]+\}/g)
- if (jsonMatches) {
- for (const jsonStr of jsonMatches) {
+ // 尝试查找并提取完整的JSON对象
+ // 使用更精确的方式:查找 { 和匹配的 }
+ let startIndex = 0
+ while (startIndex < classifyBuffer.value.length) {
+ const openBrace = classifyBuffer.value.indexOf('{', startIndex)
+ if (openBrace === -1) {
+ // 没有找到开始的 {,清理前面的无用字符
+ classifyBuffer.value = ''
+ break
+ }
+
+ // 尝试找到匹配的闭合括号
+ let braceCount = 0
+ let closeBrace = -1
+ for (let i = openBrace; i < classifyBuffer.value.length; i++) {
+ if (classifyBuffer.value[i] === '{') braceCount++
+ else if (classifyBuffer.value[i] === '}') {
+ braceCount--
+ if (braceCount === 0) {
+ closeBrace = i
+ break
+ }
+ }
+ }
+
+ if (closeBrace !== -1) {
+ // 找到了完整的JSON
+ const jsonStr = classifyBuffer.value.substring(openBrace, closeBrace + 1)
+
try {
const result = JSON.parse(jsonStr)
+
if (result.id) {
const record = records.value.find(r => r.id === result.id)
if (record) {
record.classify = result.classify || ''
- record.subClassify = result.subClassify || ''
+ // 如果AI返回了type字段,也更新type
+ if (result.type !== undefined && result.type !== null) {
+ record.type = result.type
+ }
hasChanges.value = true
}
- // 移除已处理的JSON
- window.classifyBuffer = window.classifyBuffer.replace(jsonStr, '')
}
} catch (e) {
- // 不是完整的JSON,继续累积
+ console.error('JSON解析失败:', e)
}
+
+ // 移除已处理的部分
+ classifyBuffer.value = classifyBuffer.value.substring(closeBrace + 1)
+ startIndex = 0 // 从头开始查找下一个JSON
+ } else {
+ // 没有找到闭合括号,说明JSON还不完整,等待更多数据
+ break
}
}
} catch (error) {
console.error('解析分类结果失败', error)
}
+ } else if (eventType === 'start') {
+ showToast(data)
} else if (eventType === 'end') {
- window.classifyBuffer = ''
+ classifyBuffer.value = ''
showToast('分类完成')
} else if (eventType === 'error') {
- window.classifyBuffer = ''
+ classifyBuffer.value = ''
showToast(data)
}
}
@@ -227,7 +276,7 @@ const saveClassifications = async () => {
.map(r => ({
id: r.id,
classify: r.classify,
- subClassify: r.subClassify
+ type: r.type
}))
if (itemsToUpdate.length === 0) {
diff --git a/Web/src/views/EmailRecord.vue b/Web/src/views/EmailRecord.vue
index 2fa270b..36a6b53 100644
--- a/Web/src/views/EmailRecord.vue
+++ b/Web/src/views/EmailRecord.vue
@@ -415,8 +415,6 @@ onMounted(() => {