新增: Phase2 阶段收尾(Sprint 1-20)
重构:删 5 零引用 crate(df-evolve/plugin/stages/task/traceability)+ 清死模块、ai.rs 拆 11 子 module、ai.ts 拆 6 composable、i18n 拆目录 功能:知识库全栈(df-project/scan + CRUD + 时间线 + 前端)、Settings 拆分、appSettings KV 迁移、模型池、LLM 并发 Semaphore 修复:审批持久化根治、ConditionEngine 默认拒绝、NodeRegistry unimplemented 清除、promote 补偿删除、工具结果截断 50KB、路径校验防 symlink 逃逸 文档:B-03 人工审批设计、决策记录三分档、规格契约自检、经验记录、todo 看板、PROGRESS 更新 详见 PROGRESS.md。src-tauri/儿童每日打卡应用/ 与本项目无关,已排除。
This commit is contained in:
138
src/stores/appSettings.ts
Normal file
138
src/stores/appSettings.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
//! 应用设置 KV Store — localStorage → SQLite 迁移的统一持久化层
|
||||
//!
|
||||
//! 后端 `app_settings` 表的 value 永远是 JSON 字符串。本层职责:
|
||||
//! - 缓存已解析(反序列化)值,按 key 索引,组件同步读
|
||||
//! - 写操作立即更新缓存(乐观),并按 key debounce ~300ms 合并连续快速写
|
||||
//! - 组合式 `useSetting` 让组件像用 ref 一样用 v-model 绑定某个 key
|
||||
//!
|
||||
//! 注意:此为「KV 偏好持久化」层,与已有的 `stores/settings.ts`(mock 的 AI 提供商/
|
||||
//! 连接/通用配置列表)是两件事 —— 那个文件承载静态展示数据,本文件承载运行时偏好,
|
||||
//! 故单独成文,互不干扰。
|
||||
|
||||
import { reactive, ref, watch, type Ref } from 'vue'
|
||||
import { settingsApi } from '../api/settings'
|
||||
|
||||
/// 按 key 合并连续写的 debounce 时长(毫秒)
|
||||
const SET_DEBOUNCE_MS = 300
|
||||
|
||||
/// 已解析值的响应式缓存 —— 用 reactive 对象而非 Map,使 `useSetting` 的 watch 能
|
||||
/// 在 `loadAll` / `set` / `remove` 写入时被触发,从而同步刷新组件视图
|
||||
const cache = reactive<Record<string, unknown>>({})
|
||||
|
||||
/// 按 key 的 pending 写定时器:连续 set 在窗口内只发最后一次到 SQLite
|
||||
const pendingTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
/// 载荷队列:debounce 触发时取最新值(而非定时器创建时的快照)
|
||||
const pendingValues = new Map<string, unknown>()
|
||||
|
||||
/** 启动时一次性拉回全部 key/value 并解析填充缓存(App.vue 调用) */
|
||||
async function loadAll(): Promise<void> {
|
||||
const all = await settingsApi.getAll()
|
||||
for (const [k, raw] of Object.entries(all)) {
|
||||
cache[k] = parse(raw)
|
||||
}
|
||||
}
|
||||
|
||||
/** 同步取已解析值,未命中或缓存未加载时返回 defaultValue */
|
||||
function get<T>(key: string, defaultValue: T): T {
|
||||
const v = cache[key]
|
||||
return v === undefined ? defaultValue : (v as T)
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入某 key:立即更新缓存(乐观,视图无延迟),再 debounce 后落库。
|
||||
* 同 key 连续快速写(如拖拽/输入)在窗口内合并,只把最后一次值 JSON.stringify 发后端。
|
||||
*/
|
||||
async function set(key: string, value: unknown): Promise<void> {
|
||||
cache[key] = value
|
||||
pendingValues.set(key, value)
|
||||
|
||||
const existing = pendingTimers.get(key)
|
||||
if (existing) clearTimeout(existing)
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
pendingTimers.delete(key)
|
||||
const latest = pendingValues.get(key)
|
||||
pendingValues.delete(key)
|
||||
// 落库失败仅记日志:缓存已是最新值,降级为内存态,避免回滚造成 UI 与库不一致
|
||||
settingsApi
|
||||
.set(key, JSON.stringify(latest))
|
||||
.catch((e) => console.error(`[appSettings] 写入 ${key} 失败:`, e))
|
||||
}, SET_DEBOUNCE_MS)
|
||||
pendingTimers.set(key, timer)
|
||||
}
|
||||
|
||||
/** 删除某 key:清缓存 + 取消 pending 写 + 调后端删除 */
|
||||
async function remove(key: string): Promise<void> {
|
||||
delete cache[key]
|
||||
pendingValues.delete(key)
|
||||
const t = pendingTimers.get(key)
|
||||
if (t) {
|
||||
clearTimeout(t)
|
||||
pendingTimers.delete(key)
|
||||
}
|
||||
await settingsApi.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* 组合式:把某 key 绑定到一个 ref,组件可直接 v-model。
|
||||
* 读走缓存(同步、响应式),写触发 debounced set。
|
||||
*
|
||||
* 实现说明:用普通 `ref` 持有当前值,`watch(cache[key])` 单向同步缓存 → ref,
|
||||
* 这样 `loadAll` 异步填充缓存后能正确刷新组件视图(customRef 的 trigger 不会被
|
||||
* reactive cache 的异步 mutation 自动唤起,故改用 watch 显式同步)。
|
||||
*
|
||||
* @example
|
||||
* const theme = useSetting('df-theme', 'dark')
|
||||
* // 模板里 <a-switch v-model="theme" /> —— 改动自动落库
|
||||
*/
|
||||
function useSetting<T>(key: string, defaultValue: T): Ref<T> {
|
||||
const r = ref(get<T>(key, defaultValue)) as Ref<T>
|
||||
|
||||
// 缓存 → ref:loadAll/set/remove 改 cache[key] 时同步到 ref
|
||||
watch(
|
||||
() => cache[key],
|
||||
(v) => {
|
||||
// 写回值与当前 ref 不同时才更新,避免组件 set 后被 watch 回写造成抖动
|
||||
const next = v === undefined ? defaultValue : (v as T)
|
||||
if (!Object.is(next, r.value)) r.value = next
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// ref → 缓存(及落库):组件改 .value 时触发 debounced set
|
||||
watch(
|
||||
r,
|
||||
(v) => {
|
||||
// 仅当与缓存当前值不同时才写,避免 watch 回写触发循环
|
||||
if (!Object.is(v, cache[key])) void set(key, v)
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析后端原始值:JSON.parse 失败时回退为原字符串(兼容历史非 JSON 数据)。
|
||||
* null/空串直接返回,避免 `JSON.parse('')` 抛错。
|
||||
*/
|
||||
function parse(raw: string): unknown {
|
||||
if (raw === null || raw === '') return raw
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
|
||||
export function useAppSettingsStore() {
|
||||
return {
|
||||
cache,
|
||||
loadAll,
|
||||
get,
|
||||
set,
|
||||
remove,
|
||||
useSetting,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user