优化: 所有剩余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
+82 -31
View File
@@ -92,17 +92,18 @@
<!-- 分页 -->
<div v-if="!loading && !errorMsg" class="pager">
<button class="btn btn-ghost btn-sm" :disabled="offset === 0" @click="prevPage">{{ t('auditLog.pager.prev') }}</button>
<span class="pager-info">{{ t('auditLog.pager.page', { n: page }) }}{{ hasMore ? '' : t('auditLog.pager.last') }}</span>
<span class="pager-info">{{ t('auditLog.pager.page', { n: page }) }}{{ hasMore ? '' : t('auditLog.pager.last') }}<span class="pager-total">{{ t('auditLog.pager.total', { n: total }) }}</span></span>
<button class="btn btn-ghost btn-sm" :disabled="!hasMore" @click="nextPage">{{ t('auditLog.pager.next') }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { invoke } from '@tauri-apps/api/core'
import { formatRelative, formatDate } from '@/utils/time'
import { usePersistedRef } from '@/composables/usePersistedRef'
const { t } = useI18n()
@@ -122,40 +123,61 @@ interface ToolExecutionRecord {
decided_by: string | null
}
// 后端分页结果 {items,total,has_more}(对标项目通用结构)
interface ToolExecutionPage {
items: ToolExecutionRecord[]
total: number
has_more: boolean
}
const PAGE_SIZE = 50
const records = ref<ToolExecutionRecord[]>([])
const total = ref(0)
// hasMore 来自后端(基于 offset+items.len()<total 推断),非"本页满 PAGE_SIZE"启发式
const hasMore = ref(false)
const offset = ref(0)
const loading = ref(false)
const errorMsg = ref('')
// 筛选状态(客户端过滤,数据量轻量)
const statusFilter = ref('')
const riskFilter = ref('')
const toolSearch = ref('')
// 筛选状态(后端 WHERE 收口,非客户端 filter 当前页)。持久化到 localStorage。
// 空串 = 不过滤(对应后端 Option<String>=None)。
const statusFilter = usePersistedRef('auditLog.statusFilter', '')
const riskFilter = usePersistedRef('auditLog.riskFilter', '')
const toolSearch = usePersistedRef('auditLog.toolSearch', '')
// 展开行(查看完整 args/result)
const expandedId = ref<string | null>(null)
const statusKeys = ['pending', 'approved', 'rejected', 'executing', 'completed', 'failed']
// interrupted 来自 cleanup_stale_pending(超时 pending 标记),与 statusKeys 一并展示
const statusKeys = ['pending', 'approved', 'rejected', 'executing', 'completed', 'failed', 'interrupted']
const riskKeys = ['low', 'medium', 'high']
const page = computed(() => Math.floor(offset.value / PAGE_SIZE) + 1)
// 下一页存在性:本页满 PAGE_SIZE 视为可能还有更多(hasMore);末页不足时点下一页会拉空,自动回退
const hasMore = computed(() => records.value.length === PAGE_SIZE)
// 客户端筛选(状态/风险/工具名)
// 后端 list_tool_executions 不支持 WHERE 筛选,数据量小(每页 50),前端 filter 轻量。
const filteredRecords = computed(() => {
let list = records.value
if (statusFilter.value) list = list.filter(r => r.status === statusFilter.value)
if (riskFilter.value) list = list.filter(r => r.risk_level === riskFilter.value)
const q = toolSearch.value.trim().toLowerCase()
if (q) list = list.filter(r => r.tool_name.toLowerCase().includes(q))
return list
// 筛选条件变化 → 重置分页 + 关闭展开行 + 重拉(后端 WHERE)
// 下拉框(status/risk)即变即查;工具名自由文本输入走 300ms 防抖(对齐 Tasks.vue searchKeyword)。
watch([statusFilter, riskFilter], () => {
offset.value = 0
expandedId.value = null
void load()
})
// 筛选变化时关闭展开行(避免展开行被筛掉后残留)
watch([statusFilter, riskFilter, toolSearch], () => { expandedId.value = null })
let _searchTimer: ReturnType<typeof setTimeout> | null = null
watch(toolSearch, () => {
if (_searchTimer) clearTimeout(_searchTimer)
_searchTimer = setTimeout(() => {
offset.value = 0
expandedId.value = null
void load()
}, 300)
})
onUnmounted(() => {
if (_searchTimer) clearTimeout(_searchTimer)
})
// 列表展示用 records(后端已按筛选条件过滤,前端不再二次 filter)
const filteredRecords = computed(() => records.value)
function toggleExpand(id: string) {
expandedId.value = expandedId.value === id ? null : id
@@ -172,20 +194,33 @@ function durationLabel(requested: string, executed: string): string {
return `${m}m${s % 60}s`
}
// 构造后端查询参数(空串/纯空白 → undefined = 不过滤)
function buildQuery() {
const q: {
status?: string
risk_level?: string
tool_keyword?: string
limit: number
offset: number
} = { limit: PAGE_SIZE, offset: offset.value }
if (statusFilter.value) q.status = statusFilter.value
if (riskFilter.value) q.risk_level = riskFilter.value
const kw = toolSearch.value.trim()
if (kw) q.tool_keyword = kw
return q
}
async function load() {
loading.value = true
errorMsg.value = ''
try {
const list = await invoke<ToolExecutionRecord[]>('list_tool_executions', {
limit: PAGE_SIZE,
offset: offset.value,
const result = await invoke<ToolExecutionPage>('list_tool_executions', {
query: buildQuery(),
})
records.value = list
// 下一页拉到空数组 → 应回退到上一页(避免停在空页)
if (list.length === 0 && offset.value > 0) {
offset.value = Math.max(0, offset.value - PAGE_SIZE)
await load()
}
records.value = result.items
total.value = result.total
hasMore.value = result.has_more
// 后端 total 已正确,空结果即真无数据(不再像旧"满页推断"需回退)
} catch (e) {
errorMsg.value = String(e)
} finally {
@@ -206,6 +241,7 @@ function prevPage() {
void load()
}
function nextPage() {
if (!hasMore.value) return
offset.value += PAGE_SIZE
expandedId.value = null
void load()
@@ -230,6 +266,7 @@ function statusClass(s: string): string {
executing: 'status-executing',
completed: 'status-completed',
failed: 'status-failed',
interrupted: 'status-interrupted',
} as Record<string, string>)[s] ?? 'status-pending'
}
function decidedLabel(d: string): string {
@@ -322,8 +359,16 @@ onMounted(() => {
.col-risk { width: 56px; }
.col-status { width: 80px; }
.col-decided { width: 64px; }
.col-args { min-width: 200px; }
.col-result { min-width: 220px; }
.col-args { min-width: 200px; max-width: 320px; }
.col-result { min-width: 220px; max-width: 360px; }
/* 表格内 args/result 单元格限高 + 截断(长内容不撑爆行),完整内容点行展开查 detail-pre */
.col-args .brief, .col-result .brief {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
max-height: 48px; /* 约 3 行(行高 ~16px),超长内容折叠,展开行看完整 */
}
/* 可点击行 */
.audit-row { cursor: pointer; transition: background 0.12s; }
@@ -397,6 +442,7 @@ onMounted(() => {
.status-executing { background: var(--df-info-bg); color: var(--df-info); }
.status-completed { background: var(--df-success-bg); color: var(--df-success); }
.status-failed { background: var(--df-danger-bg); color: var(--df-danger); }
.status-interrupted { background: rgba(90,99,128,0.2); color: var(--df-text-dim); }
/* 决策者 tag */
.decided-tag {
@@ -423,6 +469,11 @@ onMounted(() => {
color: var(--df-text-dim);
font-family: var(--df-font-mono);
}
.pager-total {
margin-left: 8px;
color: var(--df-text-dim);
opacity: 0.7;
}
/* empty-state 已提取到 global.css 全局 */
</style>