优化: UI/UX遗留批(审计/AI命令层/前端组件,会话前基线收尾)
- AuditLog +298(筛选/详情/i18n)+ audit 后端 record/mod - AI 命令层:generate_image +81 / fetch_url / fetch_search / skills / tool_registry / tools/file / provider / conversation - 前端组件:AiChat/TopBar/ConversationSidebar/GitChanges/ApprovalPopup/Dashboard/ProjectDetail 等 30+ + composables + i18n - 诊断文档: aichat历史会话实证诊断-2026-08-04 + project_soft_delete 测试
This commit is contained in:
+295
-3
@@ -3,14 +3,28 @@
|
||||
<header class="page-header">
|
||||
<h1>{{ t('auditLog.title') }}</h1>
|
||||
<div class="header-actions">
|
||||
<button class="btn btn-ghost" @click="reload" :disabled="loading">
|
||||
<button class="btn btn-ghost" @click="reloadAudit" :disabled="loading" v-if="activeTab === 'audit'">
|
||||
<svg v-if="loading" class="audit-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21 12a9 9 0 11-6.219-8.56"/></svg>
|
||||
{{ t('auditLog.refresh') }}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="page-desc">{{ t('auditLog.desc') }}</p>
|
||||
<!-- Tab 导航 -->
|
||||
<nav class="detail-tabs audit-tabs">
|
||||
<button class="tab-btn" :class="{ 'tab-active': activeTab === 'audit' }" type="button" @click="switchTab('audit')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
|
||||
{{ t('auditLog.tabAudit') }}
|
||||
</button>
|
||||
<button class="tab-btn" :class="{ 'tab-active': activeTab === 'conversations' }" type="button" @click="switchTab('conversations')">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>
|
||||
{{ t('auditLog.tabConversations') }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- ═══ Tab: 审批历史 ═══ -->
|
||||
<template v-if="activeTab === 'audit'">
|
||||
<p class="page-desc">{{ t('auditLog.desc') }}</p>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<div class="filter-bar audit-filters">
|
||||
@@ -95,6 +109,62 @@
|
||||
<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>
|
||||
</template>
|
||||
<!-- ═══ Tab: 对话历史 ═══ -->
|
||||
<template v-else>
|
||||
<div class="conv-split">
|
||||
<!-- 左侧:对话标题列表 -->
|
||||
<div class="conv-list-panel">
|
||||
<div class="conv-list" v-if="conversations.length > 0">
|
||||
<div v-for="conv in conversations" :key="conv.id" class="conv-list-item" :class="{ active: expandedConv === conv.id }" @click="toggleConv(conv.id)">
|
||||
<span class="conv-list-item-title">{{ conv.title || '—' }}</span>
|
||||
<span class="conv-list-item-date">{{ formatDate(conv.updated_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!convLoading" class="empty-state">{{ t('auditLog.convEmpty') }}</div>
|
||||
<div v-if="convLoading && conversations.length === 0" class="empty-state"><span class="spinner"></span>{{ t('auditLog.convLoading') }}</div>
|
||||
</div>
|
||||
<!-- 右侧:完整交互日志 -->
|
||||
<div class="conv-detail-panel">
|
||||
<div v-if="!expandedConv" class="conv-detail-empty">{{ t('auditLog.convSelectHint') }}</div>
|
||||
<div v-else-if="convLoading" class="conv-detail-loading"><span class="spinner"></span>{{ t('auditLog.loading') }}</div>
|
||||
<div v-else-if="convError" class="conv-detail-error">{{ convError }}</div>
|
||||
<div v-else class="conv-messages">
|
||||
<div v-for="(msg, mi) in convMessages" :key="mi" class="conv-msg" :class="'conv-msg--' + msg.role">
|
||||
<!-- 头部:角色 + model + 时间 + token -->
|
||||
<div class="conv-msg-head">
|
||||
<span class="conv-msg-role">{{ msg.role === 'user' ? '🧑 用户' : msg.role === 'assistant' ? '🤖 助手' : msg.role === 'tool' ? '🔧 工具' : '⚙️ 系统' }}</span>
|
||||
<span v-if="msg.model" class="conv-msg-model">{{ msg.model }}</span>
|
||||
<span v-if="msg.tool_call_id" class="conv-msg-toolname">tool_call: {{ msg.tool_call_id }}</span>
|
||||
<span v-if="msg.timestamp" class="conv-msg-time">{{ formatDate(msg.timestamp) }}</span>
|
||||
<span v-if="msg.prompt_tokens || msg.completion_tokens" class="conv-msg-token">
|
||||
in={{ msg.prompt_tokens ?? 0 }} · out={{ msg.completion_tokens ?? 0 }}<template v-if="msg.reasoning_tokens"> · think={{ msg.reasoning_tokens }}</template>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 推理内容(DeepSeek thinking) -->
|
||||
<div v-if="msg.reasoning_content" class="conv-msg-reasoning">
|
||||
<div class="conv-msg-reasoning-label">🧠 {{ t('auditLog.convReasoning') }}</div>
|
||||
<pre class="conv-msg-text conv-msg-text--reasoning">{{ msg.reasoning_content }}</pre>
|
||||
</div>
|
||||
<!-- 正文:完整内容(发往/来自 LLM 的完整载荷) -->
|
||||
<div class="conv-msg-body">
|
||||
<pre class="conv-msg-text">{{ msg.content || (msg.role === 'tool' ? '—' : '') }}</pre>
|
||||
</div>
|
||||
<!-- 工具调用(assistant 发起,完整 name + arguments JSON) -->
|
||||
<div v-if="msg.tool_calls && msg.tool_calls.length > 0" class="conv-msg-tools">
|
||||
<div v-for="(tc, tci) in msg.tool_calls" :key="tci" class="conv-msg-toolcall">
|
||||
<div class="conv-msg-toolcall-head">
|
||||
<span class="conv-msg-toolcall-name">🔧 {{ tc.function?.name || '工具调用' }}</span>
|
||||
<span v-if="tc.id" class="conv-msg-toolcall-id">{{ tc.id }}</span>
|
||||
</div>
|
||||
<pre v-if="tc.function?.arguments" class="conv-msg-toolcall-args">{{ tc.function.arguments }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -104,6 +174,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { formatRelative, formatDate } from '@/utils/time'
|
||||
import { usePersistedRef } from '@/composables/usePersistedRef'
|
||||
import { aiApi } from '@/api/ai'
|
||||
import type { AiConversationSummary } from '@/api/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -228,7 +300,78 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
function reload() {
|
||||
// ── Tab �──
|
||||
const activeTab = usePersistedRef<'audit' | 'conversations'>('auditLog.activeTab', 'audit')
|
||||
function switchTab(tab: 'audit' | 'conversations') {
|
||||
activeTab.value = tab
|
||||
if (tab === 'conversations') void loadConversations()
|
||||
}
|
||||
|
||||
// ── 对话历史 ──
|
||||
const conversations = ref<AiConversationSummary[]>([])
|
||||
const expandedConv = ref<string | null>(null)
|
||||
const convLoading = ref(false)
|
||||
const convError = ref('')
|
||||
interface ConvToolCall {
|
||||
id?: string
|
||||
type?: string
|
||||
function?: { name: string; arguments: string }
|
||||
}
|
||||
interface ConvMessage {
|
||||
role: string
|
||||
content: string
|
||||
model?: string | null
|
||||
/** Unix 毫秒(ChatMessage.timestamp) */
|
||||
timestamp?: number | null
|
||||
reasoning_content?: string | null
|
||||
/** 工具调用(Vec<ToolCall>,role=assistant 时可能有) */
|
||||
tool_calls?: ConvToolCall[] | null
|
||||
/** 工具调用 ID(role=tool 时) */
|
||||
tool_call_id?: string | null
|
||||
parts?: { type?: string; text?: string; base64?: string; url?: string }[] | null
|
||||
prompt_tokens?: number | null
|
||||
completion_tokens?: number | null
|
||||
reasoning_tokens?: number | null
|
||||
status?: string | null
|
||||
}
|
||||
const convMessages = ref<ConvMessage[]>([])
|
||||
|
||||
async function loadConversations() {
|
||||
convLoading.value = true
|
||||
try {
|
||||
const list = await aiApi.listConversations(200, false)
|
||||
// 按 updated_at 降序排列
|
||||
conversations.value = (list ?? []).sort((a, b) => (b.updated_at > a.updated_at ? 1 : b.updated_at < a.updated_at ? -1 : 0))
|
||||
} catch (e) {
|
||||
conversations.value = []
|
||||
console.error('[AuditLog] 加载对话列表失败:', e)
|
||||
} finally {
|
||||
convLoading.value = false
|
||||
}
|
||||
}
|
||||
async function toggleConv(id: string) {
|
||||
if (expandedConv.value === id) {
|
||||
expandedConv.value = null
|
||||
convMessages.value = []
|
||||
return
|
||||
}
|
||||
expandedConv.value = id
|
||||
convLoading.value = true
|
||||
convError.value = ''
|
||||
try {
|
||||
const json = await aiApi.exportConversation(id, 'json')
|
||||
const parsed = JSON.parse(json) as ConvMessage[] ?? []
|
||||
convMessages.value = parsed || []
|
||||
} catch (e) {
|
||||
convError.value = String(e)
|
||||
convMessages.value = []
|
||||
} finally {
|
||||
convLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function reloadAudit() {
|
||||
if (loading.value) return
|
||||
offset.value = 0
|
||||
expandedId.value = null
|
||||
@@ -278,6 +421,7 @@ function decidedClass(d: string): string {
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
if (activeTab.value === 'conversations') void loadConversations()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -476,4 +620,152 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
/* empty-state 已提取到 global.css 全局 */
|
||||
|
||||
/* ===== Tab 导航(对齐 ProjectDetail 同款交互) ===== */
|
||||
.audit-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.audit-tabs .tab-btn {
|
||||
padding: 8px 16px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--df-text-dim);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
margin-bottom: -0.5px;
|
||||
}
|
||||
.audit-tabs .tab-btn:hover { color: var(--df-text); }
|
||||
.audit-tabs .tab-btn.tab-active {
|
||||
color: var(--df-text);
|
||||
border-bottom-color: var(--df-accent);
|
||||
}
|
||||
|
||||
/* ===== 对话历史 ===== */
|
||||
.conv-split {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
gap: var(--df-gap-grid);
|
||||
min-height: 400px;
|
||||
}
|
||||
.conv-list-panel {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 220px);
|
||||
}
|
||||
.conv-list-item {
|
||||
padding: 12px 14px;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.conv-list-item:last-child { border-bottom: none; }
|
||||
.conv-list-item:hover { background: var(--df-bg-card-hover); }
|
||||
.conv-list-item.active { background: var(--df-accent-soft); border-left: 3px solid var(--df-accent); }
|
||||
.conv-list-item-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--df-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.conv-list-item-date { font-size: 11px; color: var(--df-text-dim); }
|
||||
.conv-detail-panel {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
overflow-y: auto;
|
||||
max-height: calc(100vh - 220px);
|
||||
}
|
||||
.conv-detail-empty, .conv-detail-loading, .conv-detail-error {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
height: 100%; min-height: 200px;
|
||||
font-size: 13px; color: var(--df-text-dim);
|
||||
}
|
||||
.conv-detail-error { color: var(--df-danger); }
|
||||
.conv-messages { display: flex; flex-direction: column; gap: 10px; }
|
||||
.conv-msg {
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-size: 12px;
|
||||
}
|
||||
.conv-msg--user { background: var(--df-accent-bg); }
|
||||
.conv-msg--assistant { background: var(--df-bg-raised); }
|
||||
.conv-msg--tool { background: rgba(90,99,128,0.1); }
|
||||
.conv-msg-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.conv-msg-role { font-size: 12px; font-weight: 500; color: var(--df-text); }
|
||||
.conv-msg-model, .conv-msg-toolname, .conv-msg-token {
|
||||
font-size: 10px; padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(90,99,128,0.2); color: var(--df-text-dim);
|
||||
}
|
||||
.conv-msg-token { font-family: var(--df-font-mono); color: var(--df-info); background: var(--df-info-bg); }
|
||||
.conv-msg-time { font-size: 10px; color: var(--df-text-dim); margin-left: auto; }
|
||||
.conv-msg-body { min-width: 0; }
|
||||
.conv-msg-text {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: var(--df-font-sans);
|
||||
}
|
||||
/* 推理过程(thinking)块 */
|
||||
.conv-msg-reasoning {
|
||||
margin: 4px 0;
|
||||
padding: 6px 10px;
|
||||
border-left: 2px solid var(--df-info);
|
||||
background: var(--df-info-bg);
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
.conv-msg-reasoning-label {
|
||||
font-size: 10px; color: var(--df-info); font-weight: 500; margin-bottom: 4px;
|
||||
}
|
||||
.conv-msg-text--reasoning {
|
||||
font-size: 11px; color: var(--df-text-dim); font-style: italic;
|
||||
}
|
||||
/* 工具调用块(完整 name + arguments JSON) */
|
||||
.conv-msg-tools { margin-top: 4px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.conv-msg-toolcall {
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
background: var(--df-warning-bg);
|
||||
border: 0.5px solid rgba(240,199,94,0.2);
|
||||
}
|
||||
.conv-msg-toolcall-head {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
|
||||
}
|
||||
.conv-msg-toolcall-name { font-size: 11px; font-weight: 500; color: var(--df-warning); }
|
||||
.conv-msg-toolcall-id { font-size: 10px; color: var(--df-text-dim); font-family: var(--df-font-mono); }
|
||||
.conv-msg-toolcall-args {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
color: var(--df-text-secondary);
|
||||
background: var(--df-bg);
|
||||
border-radius: var(--df-radius-xs);
|
||||
padding: 6px 8px;
|
||||
font-family: var(--df-font-mono);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user