新增: 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:
93
src/App.vue
93
src/App.vue
@@ -51,6 +51,7 @@
|
||||
>
|
||||
<span class="nav-link-icon" v-html="item.svg"></span>
|
||||
<span class="nav-link-text">{{ $t(item.label) }}</span>
|
||||
<span v-if="item.path === '/knowledge' && candidateCount > 0" class="nav-link-badge">{{ candidateCount }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -79,7 +80,8 @@
|
||||
</aside>
|
||||
|
||||
<!-- ═══ Main (页面内容) ═══ -->
|
||||
<main class="df-main" v-show="!aiStore.state.maximized">
|
||||
<!-- 仅在 AI 面板可见且最大化时隐藏主内容;面板关闭时无论 maximized 状态如何都显示 -->
|
||||
<main class="df-main" v-show="!(aiStore.state.maximized && aiStore.state.panelOpen)">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="page" mode="out-in">
|
||||
<component :is="Component" />
|
||||
@@ -95,17 +97,39 @@
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<!-- 全局错误 toast(根级,主窗口与分离窗口共用)-->
|
||||
<transition name="toast">
|
||||
<div v-if="errorMsg" class="df-toast">⚠ {{ errorMsg }}</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useAiStore } from './stores/ai'
|
||||
import { useKnowledgeStore } from './stores/knowledge'
|
||||
import { useProjectStore } from './stores/project'
|
||||
import { useAppSettingsStore } from './stores/appSettings'
|
||||
import i18n from './i18n'
|
||||
import AiChat from './components/AiChat.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const isDetached = computed(() => route.path === '/ai-detached')
|
||||
const aiStore = useAiStore()
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
// ── 全局错误 toast(消费 projectStore.error,所有 action 失败统一反馈)──
|
||||
const projectStore = useProjectStore()
|
||||
const errorMsg = ref('')
|
||||
let errorTimer: ReturnType<typeof setTimeout> | null = null
|
||||
watch(() => projectStore.error, (err) => {
|
||||
if (!err) return
|
||||
errorMsg.value = err
|
||||
projectStore.clearError() // 重置,允许连续同值错误再次触发
|
||||
if (errorTimer) clearTimeout(errorTimer)
|
||||
errorTimer = setTimeout(() => { errorMsg.value = '' }, 3500)
|
||||
})
|
||||
|
||||
// 点击菜单自动退出最大化 → 切为分栏模式
|
||||
watch(() => route.path, (path) => {
|
||||
@@ -115,7 +139,8 @@ watch(() => route.path, (path) => {
|
||||
})
|
||||
|
||||
// ── AI 面板拖拽调宽 ──
|
||||
const panelWidth = ref(parseInt(localStorage.getItem('df-ai-width') || '500', 10))
|
||||
// useSetting 监听 appSettings 缓存:loadAll 异步填充后会自动刷新 ref
|
||||
const panelWidth = appSettings.useSetting<number>('df-ai-width', 500)
|
||||
const resizing = ref(false)
|
||||
|
||||
// maximized=全宽(flex:1填充) | sidebar=固定宽
|
||||
@@ -147,7 +172,7 @@ function startResize(e: MouseEvent) {
|
||||
resizing.value = false
|
||||
document.body.style.cursor = ''
|
||||
document.body.style.userSelect = ''
|
||||
localStorage.setItem('df-ai-width', String(panelWidth.value))
|
||||
void appSettings.set('df-ai-width', panelWidth.value)
|
||||
document.removeEventListener('mousemove', onMouseMove)
|
||||
document.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
@@ -158,7 +183,7 @@ function startResize(e: MouseEvent) {
|
||||
|
||||
// 主题初始化
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem('df-theme') || 'dark'
|
||||
const saved = appSettings.get<string>('df-theme', 'dark')
|
||||
if (saved === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
} else if (saved === 'system') {
|
||||
@@ -175,11 +200,58 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 1. 先把 SQLite 全表 KV 填进 appSettings 缓存(后续 get/useSetting 才能读到真实值)
|
||||
await appSettings.loadAll()
|
||||
// 2. 一次性迁移:把旧 localStorage 的 11 个 key 导入 SQLite(仅首次跑,之后 localStorage 清净)
|
||||
await migrateLegacyLocalStorage()
|
||||
// 3. 缓存已就绪,初始化主题/locale
|
||||
initTheme()
|
||||
const savedLang = appSettings.get<string | null>('df-language', null)
|
||||
if (savedLang) {
|
||||
i18n.global.locale.value = savedLang as 'zh-CN' | 'en'
|
||||
}
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
// 加载知识库候选数(供侧栏 knowledge badge 显示)
|
||||
knowledgeStore.loadCandidates()
|
||||
})
|
||||
|
||||
/// 一次性迁移:遍历遗留的 localStorage key,若有值则导入 appSettings(SQLite),
|
||||
/// 然后删除 localStorage 中的旧 key。导入时按各 key 原存格式解析成 JS 值再交给 set
|
||||
/// (set 内部会 JSON.stringify)。之后 localStorage 只剩 df-ai-gen / df-ai-text 流式快照。
|
||||
const LEGACY_KEYS_RAW = new Set(['df-theme', 'df-language', 'df-ai-language']) // 裸字符串
|
||||
const LEGACY_KEYS_JSON = new Set([ // 已 JSON.stringify(裸值或对象)
|
||||
'df-ai-width', 'df-ai-ui', 'df-show-token-usage',
|
||||
'df-llm-global-concurrency', 'df-llm-per-conv-concurrency',
|
||||
'df-ai-active-conv', 'df-connections',
|
||||
])
|
||||
function parseLegacy(key: string, raw: string): unknown {
|
||||
if (LEGACY_KEYS_RAW.has(key)) return raw // 裸字符串,如 'dark' / 'zh-CN'
|
||||
// JSON 类:有的存的是 JSON.stringify('zh-CN') → '"zh-CN"'(虽不在本集合,保险起见 parse)
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return raw
|
||||
}
|
||||
}
|
||||
async function migrateLegacyLocalStorage(): Promise<void> {
|
||||
const allKeys = [...LEGACY_KEYS_RAW, ...LEGACY_KEYS_JSON]
|
||||
for (const key of allKeys) {
|
||||
const raw = localStorage.getItem(key)
|
||||
if (raw === null || raw === '') continue
|
||||
try {
|
||||
await appSettings.set(key, parseLegacy(key, raw))
|
||||
} catch (e) {
|
||||
console.error(`[migrate] 导入 ${key} 失败:`, e)
|
||||
}
|
||||
localStorage.removeItem(key)
|
||||
}
|
||||
}
|
||||
|
||||
// 知识库候选数量(侧栏 badge)— 解包 computed 取值保持响应式
|
||||
const knowledgeStore = useKnowledgeStore()
|
||||
const candidateCount = computed(() => knowledgeStore.candidateCount)
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
@@ -437,4 +509,15 @@ const secondaryNav = [
|
||||
.page-leave-active { transition: opacity 0.12s var(--df-ease), transform 0.12s var(--df-ease); }
|
||||
.page-enter-from { opacity: 0; transform: translateY(6px); }
|
||||
.page-leave-to { opacity: 0; transform: translateY(-4px); }
|
||||
|
||||
/* ═══ 全局错误 toast ═══ */
|
||||
.df-toast {
|
||||
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||||
background: var(--df-danger); color: #fff;
|
||||
padding: 10px 18px; border-radius: var(--df-radius-sm);
|
||||
font-size: 13px; z-index: 9999; max-width: 90vw;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
|
||||
}
|
||||
.toast-enter-active, .toast-leave-active { transition: all 0.25s var(--df-ease); }
|
||||
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, 10px); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user