优化: 所有剩余UI/UX待办一批完成(持久化+AuditLog+解耦+total+原12大改+P2)

持久化(P1-c):新建 usePersistedRef composable,Tasks/AuditLog/ProjectDetail 等接入 localStorage

AuditLog(P1-d):后端 list_tool_executions 加 WHERE 筛选+返 {items,total,has_more},前端对接+长列折叠+筛选持久化

数据源解耦(P1-g):ProjectDetail projectTasks 按 project_id 独立加载 + ChatInput @项目联想独立加载(不读 store.tasks 当前页)

GitChanges(12a):后端 get_module_commits 加 git rev-list --count 返 total,前端显真实总数

原12大改:Dashboard 统计卡压底行(1)/Projects 列表卡片视图(2)/project_event 埋点排序(3)/TaskDetail 重设计(4)/IdeaDetail 重设计(5)/KnowledgeDetail 重设计(6)/界面持久化+侧栏Ctrl+B+审批数字键(7)/ProjectDetail 三栏改两栏(10)

P2打磨:文件浏览器(FileTree去重/FilePreview行号.md Diff/selectedFilePath归位)/settings反馈(假保存/端口校验)/AI会话(try-catch/scrollIntoView)/后端计数(move_queue事件/timeline total/workflow分页/import_batch分块)/杂项(TopBar/ConfirmDialog键盘/CIStatus i18n/ToolResultBody/ModuleNode/ApprovalDialog全选)
This commit is contained in:
lxy
2026-08-02 13:11:06 +08:00
parent caaabf0c15
commit f736f435bc
70 changed files with 2645 additions and 576 deletions
+113 -14
View File
@@ -8,6 +8,11 @@
</div>
</header>
<!-- ⑤ 操作反馈 toast(保存/发布/拒绝/归档/创建成功 + 保存失败)-->
<Transition name="toast">
<div v-if="toast.visible" class="toast" :class="'toast-' + toast.type">{{ toast.msg }}</div>
</Transition>
<!-- 错误条:消费 store.error(样式全局 .error-banner,仅间距用页面变量) -->
<div v-if="store.error" class="error-banner" style="margin-bottom: var(--df-gap-page)">
<span class="error-text">{{ store.error }}</span>
@@ -155,15 +160,48 @@ import {
knowledgeConfidenceLabel as confidenceLabel,
} from '@/stores/knowledge'
import { useRendered } from '@/composables/useMarkdown'
import { useToast } from '@/composables/useToast'
import { stripMd } from '@/utils/markdown'
import KnowledgeDetail from '@/components/knowledge/KnowledgeDetail.vue'
import type { KnowledgeDetailPayload } from '@/api/types'
import type { KnowledgeDetailPayload, KnowledgeRecord } from '@/api/types'
const { t } = useI18n()
const store = useKnowledgeStore()
// ⑤ 编辑/审核反馈:成功/失败明确提示(对齐 Settings.vue/Projects.vue useToast 模式)
const { toast, showToast } = useToast()
// ④ topTab/activeKind/searchQuery 持久化 localStorage(跨刷新保留用户视图态)
// 仅持久化视图态:不持久化详情选中(id 跨刷新可能已失效)。容错:解析失败/无效值回退默认。
const LS_KEY = 'kn.view'
function loadView(): { topTab: 'library' | 'inbox'; activeKind: string; searchQuery: string } {
try {
const raw = localStorage.getItem(LS_KEY)
if (!raw) return { topTab: 'library', activeKind: 'all', searchQuery: '' }
const v = JSON.parse(raw)
return {
topTab: v.topTab === 'inbox' ? 'inbox' : 'library',
activeKind: typeof v.activeKind === 'string' ? v.activeKind : 'all',
searchQuery: typeof v.searchQuery === 'string' ? v.searchQuery : '',
}
} catch {
return { topTab: 'library', activeKind: 'all', searchQuery: '' }
}
}
function saveView() {
try {
localStorage.setItem(LS_KEY, JSON.stringify({
topTab: topTab.value,
activeKind: activeKind.value,
searchQuery: searchQuery.value,
}))
} catch {
/* localStorage 不可用(隐私模式/配额)静默降级,视图态非关键 */
}
}
const persisted = loadView()
// 顶层 Tab: library(知识库) | inbox(审核收件箱)
const topTab = ref<'library' | 'inbox'>('library')
const topTab = ref<'library' | 'inbox'>(persisted.topTab)
const categories = [
{ key: 'all', icon: '📦' },
@@ -173,8 +211,8 @@ const categories = [
function catLabel(key: string): string {
return key === 'all' ? t('knowledge.categoryAll') : kindText(key)
}
const activeKind = ref('all')
const searchQuery = ref('')
const activeKind = ref(persisted.activeKind)
const searchQuery = ref(persisted.searchQuery)
let searchTimer: ReturnType<typeof setTimeout> | null = null
function onSearchInput() {
@@ -208,6 +246,9 @@ function switchTopTab(tab: 'library' | 'inbox') {
}
}
// ④ 视图态(topTab/activeKind/searchQuery)持久化:任一变化即写 localStorage
watch([topTab, activeKind, searchQuery], saveView)
// 关闭错误条
function clearError() {
store.clearError()
@@ -263,19 +304,26 @@ async function onDetailSave(payload: { id: string; data: { title: string; conten
await store.update(payload.id, payload.data)
// 重新拉详情
await selectKnowledge(payload.id)
// ⑤ 编辑保存成功反馈(此前静默无反馈,用户不知是否生效)
showToast(t('knowledge.savedToast'), 'success')
} catch (e: any) {
// 保存失败:不重新抛出(emit 调用方未 await,抛出无人处理),仅 toast 反馈
showToast(e?.toString() ?? t('knowledge.err.saveFailed'), 'error')
} finally {
submitting.value = false
}
}
// ===== 审核操作(收件箱) =====
// P0-②:发布/拒绝/归档后自动选中下一条(而非清空详情),减少审核者来回点击
async function publishCurrent() {
if (!detail.value) return
const currentId = detail.value.knowledge.id
submitting.value = true
try {
await store.updateStatus(detail.value.knowledge.id, 'published')
selectedId.value = null
detail.value = null
await store.updateStatus(currentId, 'published')
await selectNextCandidate(currentId)
showToast(t('knowledge.publishedToast'), 'success')
} finally {
submitting.value = false
}
@@ -283,11 +331,12 @@ async function publishCurrent() {
async function rejectCurrent() {
if (!detail.value) return
const currentId = detail.value.knowledge.id
submitting.value = true
try {
await store.archive(detail.value.knowledge.id)
selectedId.value = null
detail.value = null
await store.archive(currentId)
await selectNextCandidate(currentId)
showToast(t('knowledge.rejectedToast'), 'info')
} finally {
submitting.value = false
}
@@ -295,16 +344,43 @@ async function rejectCurrent() {
async function archiveCurrent() {
if (!detail.value) return
const currentId = detail.value.knowledge.id
submitting.value = true
try {
await store.archive(detail.value.knowledge.id)
selectedId.value = null
detail.value = null
await store.archive(currentId)
// 知识库 tab 无「下一条」语义(library 不限 candidate),归档后清空详情
if (topTab.value === 'inbox') {
await selectNextCandidate(currentId)
} else {
selectedId.value = null
detail.value = null
}
showToast(t('knowledge.archivedToast'), 'info')
} finally {
submitting.value = false
}
}
// 当前条目从 candidates 移除后,选中同位置后续条目;末尾则清空详情
// 实现策略:操作前记下 currentId 在原列表中的 index,移除后从剩余列表的同
// index 取(若无则取前一条),保证「下一条」语义而非跳回开头
function pickNextCandidate(remaining: KnowledgeRecord[], currentIndex: number): KnowledgeRecord | null {
return remaining[currentIndex] ?? remaining[currentIndex - 1] ?? null
}
async function selectNextCandidate(currentId: string) {
const before = store.candidates
const idx = before.findIndex(c => c.id === currentId)
const remaining = store.candidates // store 操作后已 filter 移除 currentId
const next = idx >= 0 ? pickNextCandidate(remaining, idx) : null
if (next) {
await selectKnowledge(next.id)
} else {
selectedId.value = null
detail.value = null
}
}
// ===== 新增对话框 =====
const showCreateModal = ref(false)
const form = ref({ kind: 'pitfall', title: '', content: '', tagsInput: '', confidence: '' })
@@ -329,6 +405,7 @@ async function submitCreate() {
})
showCreateModal.value = false
switchTopTab('inbox')
showToast(t('knowledge.createdToast'), 'success')
} finally {
submitting.value = false
}
@@ -339,7 +416,16 @@ async function submitCreate() {
onMounted(() => {
ensureLoaded() // 后台预热 Markdown 渲染器(模块单例,与 AiChat/TaskDetail 共享),不阻塞
store.loadList()
// ④ 按持久化视图态加载初始数据源(topTab 决定加载 items 还是 candidates)
if (topTab.value === 'inbox') {
store.loadCandidates()
} else if (searchQuery.value.trim()) {
// 持久化的搜索词:恢复搜索结果而非空列表
store.search(searchQuery.value)
} else {
store.loadList()
}
// candidates badge 始终预加载(顶部 Tab 计数用)
store.loadCandidates()
})
</script>
@@ -347,6 +433,19 @@ onMounted(() => {
<style scoped>
.knowledge { padding: 16px 20px 20px; height: 100%; display: flex; flex-direction: column; }
/* ===== 顶部 Toast 提示(⑤ 操作反馈,对齐 Settings.vue)===== */
.toast {
position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
z-index: 1000; padding: 8px 16px; border-radius: var(--df-radius-sm);
font-size: 13px; box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.toast-error { background: var(--df-danger-bg); color: var(--df-danger); border: 0.5px solid var(--df-danger); }
.toast-warning { background: var(--df-warning-bg); color: var(--df-warning); border: 0.5px solid var(--df-warning); }
.toast-info { background: var(--df-accent-bg); color: var(--df-accent); border: 0.5px solid var(--df-accent); }
.toast-success { background: var(--df-success-bg); color: var(--df-success); border: 0.5px solid var(--df-success); }
.toast-enter-active, .toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
/* page-header / btn / modal 等已提取到 global.css 全局 */
/* ===== 错误条样式已提取到 styles/components.css 全局(.error-banner) ===== */