优化: 所有剩余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:
@@ -20,7 +20,7 @@
|
||||
</select>
|
||||
</SettingRow>
|
||||
<SettingRow ref="rowLogLevel" :label="$t('settings.labelLogLevel')" :desc="$t('settings.descLogLevel')">
|
||||
<select v-model="settings.logLevel" class="setting-select" @change="markSaved">
|
||||
<select v-model="settings.logLevel" class="setting-select" @change="onLogLevelChange">
|
||||
<option value="error">Error</option>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
@@ -110,11 +110,21 @@ function clampMode(raw: string): AutoMode {
|
||||
return (VALID_MODES as readonly string[]).includes(raw) ? (raw as AutoMode) : 'low'
|
||||
}
|
||||
|
||||
// M15: logLevel 合法值白名单(对齐 <select> option)+ 脏值兜底(默认 info)。
|
||||
// 防 KV 里历史脏值(手改/旧版本/导入备份)落到 select 显示空。
|
||||
const VALID_LOG_LEVELS = ['error', 'warn', 'info', 'debug'] as const
|
||||
type LogLevel = (typeof VALID_LOG_LEVELS)[number]
|
||||
function clampLogLevel(raw: string): LogLevel {
|
||||
return (VALID_LOG_LEVELS as readonly string[]).includes(raw) ? (raw as LogLevel) : 'info'
|
||||
}
|
||||
|
||||
const settings = reactive({
|
||||
// 三档 low/medium/all,默认 low(=旧 autoExecute=false 现状),KV 持久化 df-ai-auto-execute-mode。
|
||||
// clampMode 兜底脏值。
|
||||
autoExecuteMode: clampMode(appSettings.get<string>('df-ai-auto-execute-mode', 'low')),
|
||||
logLevel: 'info',
|
||||
// M15: logLevel 真持久化——从 KV 读回(默认 info),onLogLevelChange 写回 KV。
|
||||
// 原 bug:仅 reactive 内存态,刷新即失,@change 显「已保存」是假成功。
|
||||
logLevel: clampLogLevel(appSettings.get<string>('df-log-level', 'info')),
|
||||
// 审批超时(ms):0=不限时,缺省 900000(15min);仅固定合法选项,脏值回退默认
|
||||
approvalTimeout: clampApprovalTimeout(appSettings.get<number>('df-approval-timeout', 900000)),
|
||||
})
|
||||
@@ -158,8 +168,12 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// logLevel 行即时反馈(无参:logLevel 仅 session 内 reactive 无持久化,markSaved 只显「已保存」反馈)
|
||||
function markSaved() {
|
||||
// M15: logLevel 真持久化——@change 写 KV(df-log-level)+ 显「已保存」反馈。
|
||||
// 原 bug:仅 reactive session 态,markSaved 显「已保存」是假成功(刷新即失)。
|
||||
// 注:仅持久化偏好值;运行时日志过滤器是否实时读此值取决于后端日志初始化策略
|
||||
// (本任务范围只修「假保存」,运行时生效链路另行评估,不擅自扩展)。
|
||||
function onLogLevelChange() {
|
||||
void appSettings.set('df-log-level', settings.logLevel)
|
||||
rowLogLevel.value?.markSaved()
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelPort') }}</label>
|
||||
<input v-model.number="connForm.port" class="setting-input" type="number" :placeholder="$t('settings.phPort')" />
|
||||
<input v-model.number="connForm.port" class="setting-input" type="number" min="1" max="65535" :placeholder="$t('settings.phPort')" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">{{ $t('settings.labelUser') }}</label>
|
||||
@@ -158,6 +158,12 @@ function saveConn() {
|
||||
emit('toast', t('settings.toastConnIncomplete'), 'warning')
|
||||
return
|
||||
}
|
||||
// L20: 端口范围校验(1-65535)。v-model.number 清空输入会得 NaN,一并拦截。
|
||||
const port = connForm.port
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
emit('toast', t('settings.toastConnPortInvalid'), 'warning')
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
try {
|
||||
const record: ConnRecord = {
|
||||
|
||||
@@ -86,6 +86,9 @@ async function loadKnowledgeConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// M33: 跟踪待反馈的行 key——@change 触发 markSaved 不再立即显「已保存」,
|
||||
// 改为记下 key,等真正 IPC saveConfig resolve 后才显,防假成功(乐观 UI 误导)。
|
||||
let _pendingSavedKey: SavedKey | null = null
|
||||
let _knowledgeSaveTimer: ReturnType<typeof setTimeout> | null = null
|
||||
function saveKnowledgeConfigDebounced() {
|
||||
if (_knowledgeSaveTimer) clearTimeout(_knowledgeSaveTimer)
|
||||
@@ -96,8 +99,14 @@ function saveKnowledgeConfigDebounced() {
|
||||
// cfg 形态与后端 KnowledgeConfig 对齐(auto_extract/trigger_mode/min_messages/
|
||||
// auto_inject/vector_enabled/embedding_provider_id/embedding_model),无需 as any
|
||||
await knowledgeApi.saveConfig(cfg)
|
||||
// M33: IPC 成功后才显「已保存」(防假成功)。失败走 catch 不显,避免误导。
|
||||
if (_pendingSavedKey) flashRow(_pendingSavedKey)
|
||||
} catch (e) {
|
||||
console.error('保存知识库配置失败:', e)
|
||||
// M33: 失败时不显「已保存」;后端错误已 console,前端无行级错误态(对齐原实现,
|
||||
// 原实现也只是吞错)。如需更明显反馈可在此 emit toast,但本任务范围只防假成功。
|
||||
} finally {
|
||||
_pendingSavedKey = null
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
@@ -118,8 +127,20 @@ const rowAutoInject = useTemplateRef<InstanceType<typeof SettingRow>>('rowAutoIn
|
||||
const rowVectorEnabled = useTemplateRef<InstanceType<typeof SettingRow>>('rowVectorEnabled')
|
||||
const rowEmbeddingProvider = useTemplateRef<InstanceType<typeof SettingRow>>('rowEmbeddingProvider')
|
||||
const rowEmbeddingModel = useTemplateRef<InstanceType<typeof SettingRow>>('rowEmbeddingModel')
|
||||
function markSaved(key: 'autoExtract' | 'triggerMode' | 'minMessages' | 'autoInject' | 'vectorEnabled' | 'embeddingProvider' | 'embeddingModel') {
|
||||
const map = {
|
||||
type SavedKey = 'autoExtract' | 'triggerMode' | 'minMessages' | 'autoInject' | 'vectorEnabled' | 'embeddingProvider' | 'embeddingModel'
|
||||
|
||||
/**
|
||||
* M33: @change 触发入口——不立即显「已保存」,先记 pending key,
|
||||
* 等 saveKnowledgeConfigDebounced 内 IPC saveConfig 成功后调 flashRow 显反馈(防假成功)。
|
||||
* 连续多字段改动时取最后一次 key(用户视线所在),旧 key 反馈被新 key 覆盖可接受。
|
||||
*/
|
||||
function markSaved(key: SavedKey) {
|
||||
_pendingSavedKey = key
|
||||
}
|
||||
|
||||
/** 真正点亮行内「✓已保存」反馈(仅 IPC 成功路径调用)。 */
|
||||
function flashRow(key: SavedKey) {
|
||||
const map: Record<SavedKey, InstanceType<typeof SettingRow> | null> = {
|
||||
autoExtract: rowAutoExtract.value,
|
||||
triggerMode: rowTriggerMode.value,
|
||||
minMessages: rowMinMessages.value,
|
||||
|
||||
@@ -61,7 +61,8 @@
|
||||
<section class="panel" v-if="providerForm.visible">
|
||||
<div class="panel-header">
|
||||
<h2>{{ providerForm.editId ? $t('settings.editProviderTitle') : $t('settings.addProviderTitle') }}</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="providerForm.visible = false">{{ $t('common.cancel') }}</button>
|
||||
<!-- L19: 取消走 closeProviderForm,脏表单二次确认后再关(防误弃改动) -->
|
||||
<button class="btn btn-ghost btn-sm" @click="closeProviderForm">{{ $t('common.cancel') }}</button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
@@ -139,6 +140,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="model-list-hint">{{ $t('settings.modelListHint') }}</div>
|
||||
<!-- M32: 模型列表 enabled/weight 改了未保存时显提示(关闭/切走将丢弃) -->
|
||||
<div v-if="isFormDirty" class="model-list-unsaved-hint">{{ $t('settings.modelListUnsavedHint') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -200,8 +203,51 @@ const providerForm = reactive({
|
||||
// toast 3s 自隐易被忽略,保存失败时表单内常驻错误条 + 可操作建议,
|
||||
// 直到下次保存尝试/重开表单才清除。null=无错误。
|
||||
saveError: null as string | null,
|
||||
// L19/M32: 表单打开/保存成功时的「已落库快照」JSON 串,用于 isFormDirty 比对。
|
||||
// openProviderForm 写入初始快照,saveProvider 成功后刷新;关闭表单清空。
|
||||
savedSnapshot: '' as string,
|
||||
})
|
||||
|
||||
/**
|
||||
* L19/M32: 表单脏判定——当前表单状态与已落库快照不一致即脏。
|
||||
* 触发场景:① 取消/关闭有改动的表单 → 二次确认放弃(L19);
|
||||
* ② 模型列表 enabled/weight 改了未保存 → 表单内提示(M32)。
|
||||
* 比对用 JSON.stringify(整体快照语义,简单可靠;表单字段少,性能无虞)。
|
||||
* 不含 saving/fetching/saveError/visible 等 UI 态字段(只比业务字段)。
|
||||
*/
|
||||
const isFormDirty = computed(() => {
|
||||
if (!providerForm.visible) return false
|
||||
return formBusinessSnapshot() !== providerForm.savedSnapshot
|
||||
})
|
||||
|
||||
function formBusinessSnapshot(): string {
|
||||
return JSON.stringify({
|
||||
name: providerForm.name,
|
||||
providerType: providerForm.providerType,
|
||||
baseUrl: providerForm.baseUrl,
|
||||
apiKey: providerForm.apiKey,
|
||||
defaultModel: providerForm.defaultModel,
|
||||
models: providerForm.models.map(m => ({
|
||||
model_id: m.model_id,
|
||||
enabled: m.enabled,
|
||||
weight: m.weight,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* L19: 关闭表单入口——脏表单二次确认,清表单后关闭。
|
||||
* 用于模板「取消」按钮 + saveProvider 成功后(无脏直接关)。
|
||||
*/
|
||||
async function closeProviderForm() {
|
||||
if (isFormDirty.value) {
|
||||
const ok = await props.confirmDialog(t('settings.confirmDiscardUnsaved'))
|
||||
if (!ok) return // 用户取消关闭,继续编辑
|
||||
}
|
||||
providerForm.visible = false
|
||||
providerForm.savedSnapshot = ''
|
||||
}
|
||||
|
||||
async function loadProviders() {
|
||||
try {
|
||||
aiProviders.value = await aiApi.listProviders()
|
||||
@@ -241,6 +287,8 @@ function openProviderForm(p?: AiProviderConfig) {
|
||||
providerForm.fetching = false
|
||||
providerForm.saveError = null // P0-2: 重开表单清旧错误横幅
|
||||
providerForm.visible = true
|
||||
// L19/M32: 记初始快照(在 visible=true、字段回填完成后),isFormDirty 据此比对。
|
||||
providerForm.savedSnapshot = formBusinessSnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,6 +402,7 @@ async function saveProvider() {
|
||||
// P0-2: 成功明确反馈——清错误横幅 + 关表单 + 绿色「已保存」toast
|
||||
providerForm.saveError = null
|
||||
providerForm.visible = false
|
||||
providerForm.savedSnapshot = '' // L19: 关表单清快照
|
||||
await loadProviders()
|
||||
emit('toast', t('settings.toastSaved'), 'success')
|
||||
} catch (e) {
|
||||
@@ -445,10 +494,14 @@ function maskKey(key: string): string {
|
||||
return key.slice(0, 4) + '••••••••' + key.slice(-4)
|
||||
}
|
||||
|
||||
/** 暴露给 shell:loadProviders 供外部主动刷新 / Knowledge 读 aiProviders / 卸载清 timer */
|
||||
/** 暴露给 shell:loadProviders 供外部主动刷新 / Knowledge 读 aiProviders / 卸载清 timer。
|
||||
* L19: isFormDirty + closeProviderForm 暴露,供外部(如导航切走前)查询/触发拦截。
|
||||
* 当前 Settings.vue 未接入 nav 拦截(不在本次改文件集),接口先行供后续 shell 接入。 */
|
||||
defineExpose({
|
||||
aiProviders,
|
||||
loadProviders,
|
||||
isFormDirty,
|
||||
closeProviderForm,
|
||||
clearPoolTimer() { if (_poolTimer) clearTimeout(_poolTimer) },
|
||||
})
|
||||
|
||||
@@ -563,4 +616,12 @@ onMounted(loadProviders)
|
||||
.toggle--sm { width: 32px; height: 18px; }
|
||||
.toggle--sm .toggle-slider::before { width: 12px; height: 12px; left: 3px; bottom: 3px; }
|
||||
.toggle--sm input:checked + .toggle-slider::before { transform: translateX(14px); }
|
||||
|
||||
/* M32: 模型列表有未保存调整时的提示(弱 warning 色,贴合 model-list-hint 之下) */
|
||||
.model-list-unsaved-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--df-warning);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user