优化: 所有剩余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:
+82
-31
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user