优化: 所有剩余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:
+113
-14
@@ -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) ===== */
|
||||
|
||||
Reference in New Issue
Block a user