feat: AI 图片记账、自定义交易渠道与淘宝/京东账单导入
- AI 图片记账:多图上传由视觉模型识别,SSE 实时推送进度,提取 rawData,按「类型+金额+时间」检测重复,支持缩略图与来源图片查看 - 自定义交易渠道:新增 t_channel 与渠道管理页,支持新建/改名/删除,并接入统计、导出与 AI 查询/记账/建议提示词 - AI 建议:单条建议新增交易渠道推荐(推荐已有渠道 / AI 新建渠道) - 账单导入:新增淘宝(xlsx)、京东(CSV)解析器 - 交易:新增批量创建接口,导入时按去重键兜底跳过重复 - 运行目录统一经 AppHome 解析,图片与数据文件落盘规范化 - 前端:AI 记账方式偏好记忆、AI 按钮与输入框对齐、来源图片弹窗支持加载原图
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user