refactor: 前端 SSE 鉴权改造与公共逻辑抽取,修复统计分组空指针
- SSE 由 EventSource 改为 fetch 流式读取(新增 utils/sse.ts),token 走 Authorization 请求头,避免令牌经 URL 泄露到访问日志 - 抽取 useKeywordHistory,统一备注关键词与 AI 查询历史的读写、下拉与清空 - 抽取 renderCategoryLabel 至 utils/render.ts,消除多处重复实现 - 导出改用统一 request 实例(自动携带鉴权头),并将 blob 错误体解析为业务异常 - 新增 untagged 筛选参数;AI 智能查询改为弹窗入口并补充三语文案 - 分类/标签/邮箱/交易表单补充提交 loading,避免重复提交 - 交易时间改用本地格式化,修复 toISOString 造成的时区偏移 - 移除 Naive UI 全局注册;修正 NLog 的 rows 属性为 lines - 修复未分类记录分组时 categoryMap.get(null) 抛出的 NPE
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
// 关键词历史 composable:统一管理输入框历史记录的读写、下拉过滤与「清空历史」项
|
||||
import { computed, h, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import type { AutoCompleteOption } from 'naive-ui'
|
||||
import { NIcon } from 'naive-ui'
|
||||
import { CloseCircleOutline } from '@vicons/ionicons5'
|
||||
|
||||
/** 关键词历史配置 */
|
||||
export interface KeywordHistoryConfig {
|
||||
/** localStorage 存储键 */
|
||||
storageKey: string
|
||||
/** 与输入框绑定的文本 ref,由调用方持有 */
|
||||
text: Ref<string>
|
||||
/** 「清空历史」下拉项文案(函数形式,保证语言切换后取到最新值) */
|
||||
clearLabel: () => string
|
||||
/** 历史条目上限,默认 10 */
|
||||
max?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理输入框的关键词历史:持久化、去重、下拉过滤与清空。
|
||||
*
|
||||
* @param config 存储键、文本 ref、清空文案与上限
|
||||
*/
|
||||
export function useKeywordHistory(config: KeywordHistoryConfig) {
|
||||
const { storageKey, text, clearLabel, max = 10 } = config
|
||||
// 清空项哨兵值:以存储键派生,确保与真实关键词不冲突
|
||||
const clearValue = `__clear_history__${storageKey}`
|
||||
const history = ref<string[]>([])
|
||||
|
||||
/** 从 localStorage 读取历史,忽略损坏数据 */
|
||||
function load() {
|
||||
try {
|
||||
const raw = localStorage.getItem(storageKey)
|
||||
const parsed = raw ? JSON.parse(raw) : []
|
||||
history.value = Array.isArray(parsed)
|
||||
? parsed.filter((item) => typeof item === 'string' && item.trim()).slice(0, max)
|
||||
: []
|
||||
} catch {
|
||||
history.value = []
|
||||
}
|
||||
}
|
||||
|
||||
/** 记录当前关键词:去重(忽略大小写)、最新在前、上限截断 */
|
||||
function record() {
|
||||
const value = text.value.trim()
|
||||
if (!value) return
|
||||
const rest = history.value.filter((item) => item.toLowerCase() !== value.toLowerCase())
|
||||
history.value = [value, ...rest].slice(0, max)
|
||||
try {
|
||||
localStorage.setItem(storageKey, JSON.stringify(history.value))
|
||||
} catch {
|
||||
// 忽略写入失败
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空历史 */
|
||||
function clear() {
|
||||
history.value = []
|
||||
try {
|
||||
localStorage.removeItem(storageKey)
|
||||
} catch {
|
||||
// 忽略删除失败
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉选项:按当前输入过滤,历史非空时末尾追加「清空历史」
|
||||
const options = computed<AutoCompleteOption[]>(() => {
|
||||
const keyword = text.value.trim().toLowerCase()
|
||||
const list: AutoCompleteOption[] = history.value
|
||||
.filter((item) => item.toLowerCase().includes(keyword))
|
||||
.map((item) => ({ label: item, value: item }))
|
||||
if (history.value.length > 0) {
|
||||
list.push({ label: clearValue, value: clearValue })
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
/** 输入同步:命中清空项则清空历史,否则写回输入 */
|
||||
function handleInput(value: string | null) {
|
||||
if (value === clearValue) {
|
||||
clear()
|
||||
return
|
||||
}
|
||||
text.value = value || ''
|
||||
}
|
||||
|
||||
/** 聚焦即展示历史下拉 */
|
||||
function showMenu() {
|
||||
return true
|
||||
}
|
||||
|
||||
/** 渲染下拉项,清空项使用弱化样式与图标 */
|
||||
function renderLabel(option: AutoCompleteOption) {
|
||||
if (String(option.value) === clearValue) {
|
||||
return h('span', { style: { display: 'flex', alignItems: 'center', gap: '4px', color: '#999' } }, [
|
||||
h(NIcon, { size: 16 }, { default: () => h(CloseCircleOutline) }),
|
||||
clearLabel(),
|
||||
])
|
||||
}
|
||||
return option.label as string
|
||||
}
|
||||
|
||||
return { history, options, clearValue, load, record, clear, handleInput, showMenu, renderLabel }
|
||||
}
|
||||
Reference in New Issue
Block a user