feat: AI 图片记账、自定义交易渠道与淘宝/京东账单导入

- AI 图片记账:多图上传由视觉模型识别,SSE 实时推送进度,提取 rawData,按「类型+金额+时间」检测重复,支持缩略图与来源图片查看
- 自定义交易渠道:新增 t_channel 与渠道管理页,支持新建/改名/删除,并接入统计、导出与 AI 查询/记账/建议提示词
- AI 建议:单条建议新增交易渠道推荐(推荐已有渠道 / AI 新建渠道)
- 账单导入:新增淘宝(xlsx)、京东(CSV)解析器
- 交易:新增批量创建接口,导入时按去重键兜底跳过重复
- 运行目录统一经 AppHome 解析,图片与数据文件落盘规范化
- 前端:AI 记账方式偏好记忆、AI 按钮与输入框对齐、来源图片弹窗支持加载原图
This commit is contained in:
2026-09-21 16:21:58 +08:00
parent 96130d269e
commit 2ade60da42
73 changed files with 5024 additions and 354 deletions
+34 -2
View File
@@ -1,13 +1,45 @@
// 交易渠道相关工具:将后端返回的交易渠道编码映射为当前语言标签
// 交易渠道相关工具:将后端返回的交易渠道编码映射为当前语言标签,并支持用户自定义渠道
import i18n from '../locales'
/** i18n 全局实例的翻译函数签名(避免动态 key 触发过深的类型推断) */
type Translate = (key: string) => string
type HasKey = (key: string) => boolean
/** 将交易渠道编码转换为当前语言的标签,无对应文案时回退为原始编码 */
/** 渠道选项结构(与后端 /transaction/channels 对应) */
export interface ChannelOption {
code: string
name?: string | null
builtin?: boolean
}
/** 自定义渠道「编码 → 名称」映射,由渠道列表接口填充 */
const customChannelMap = new Map<string, string>()
/**
* 注册渠道选项:仅记录用户自定义渠道(builtin=false),内置渠道仍走多语言文案。
*
* @param list 渠道选项列表
*/
export function setCustomChannels(list: ChannelOption[]): void {
customChannelMap.clear()
for (const item of list || []) {
if (item && item.builtin === false && item.name) {
customChannelMap.set(item.code, item.name)
}
}
}
/**
* 将交易渠道编码转换为当前语言的标签:
* 自定义渠道取后端返回的名称,内置渠道取 i18n 文案,均无匹配时回退原始编码。
*
* @param val 渠道编码
* @returns 渠道标签
*/
export function getChannelLabel(val: string): string {
if (!val) return '-'
const custom = customChannelMap.get(val)
if (custom) return custom
const { t, te } = i18n.global as unknown as { t: Translate; te: HasKey }
const key = `common.channel.${val.toLowerCase()}`
return te(key) ? t(key) : val
+8 -1
View File
@@ -5,6 +5,10 @@ import { useUserStore } from '../store/user'
/** SSE 事件回调集合 */
export interface SseOptions {
/** 请求方法,默认 GET;POST 用于需要携带请求体(如上传文件)的流式接口 */
method?: 'GET' | 'POST'
/** 请求体,POST 时使用(FormData 由浏览器自动设置 Content-Type) */
body?: BodyInit | null
/** 收到事件时回调,event 为事件名,data 为已合并多行的数据 */
onEvent: (event: string, data: string) => void
/** 连接异常(含认证失败、网络错误)时回调 */
@@ -17,18 +21,21 @@ export interface SseOptions {
* 建立 SSE 连接。
*
* @param url 相对或绝对地址(不含 token 查询参数)
* @param options 事件回调
* @param options 事件回调与可选的请求方法与请求体
* @returns 用于中断连接的 AbortController
*/
export function openSse(url: string, options: SseOptions): AbortController {
const controller = new AbortController()
const userStore = useUserStore()
const { method = 'GET', body } = options
fetch(url, {
method,
headers: {
Authorization: `Bearer ${userStore.token}`,
Accept: 'text/event-stream',
},
body,
signal: controller.signal,
})
.then(async (response) => {