- AI 入口图标:灯泡→星花 sparkles+发光,与灵感入口区分 - 目标提取根本性改造:用户消息规则→工具调用推理(infer_goal_from_tool_calls) - Vec<String>→Vec<GoalEntry>(text+status),active→completed 状态流转 - 多条/轮提取,system_prompt 仅注入 active 目标 - 代码拆分到 helpers.rs(解决 brace 嵌套致 pub(crate) 不可见) - 审批修复:patch_file 全档位自动放行,批量审批防抖,乐观更新竞态保护 - 审批通知浮层:全局右上角 ApprovalOverlay,窗口遮挡时可见 - 设置导入导出移除:价值低占用空间,跨设备同步建议复制 SQLite - 分离窗口默认置顶+置顶状态持久化(刷新后保持) - header 菜单聚合:清空/压缩上下文收入 ... popout - LLM 文字重叠防御:delta 重复检测+丢弃 - 置顶图标:星→大头针 pushpin - 文档/注释更新:过期 Vec<String>/chat.rs 提取描述修正
258 lines
10 KiB
Vue
258 lines
10 KiB
Vue
<template>
|
|
<div class="settings">
|
|
<Transition name="toast">
|
|
<div v-if="toast.visible" class="toast" :class="'toast-' + toast.type">{{ toast.msg }}</div>
|
|
</Transition>
|
|
<ConfirmDialog :visible="confirmState.visible" :msg="confirmState.msg" :danger-label="$t('common.delete')" @result="answerConfirm" />
|
|
<!-- 页面头部 -->
|
|
<header class="page-header">
|
|
<h1>{{ $t('settings.title') }}</h1>
|
|
</header>
|
|
|
|
<!-- ═══ master-detail 布局(阶段3 UX 重构)═══
|
|
左 SettingsNav(~200px 固定):6 类导航,activeCategory 受控 + appSettings 持久化
|
|
右 content(scroll):按 activeCategory 渲染对应 Section/Panel -->
|
|
<div class="settings-body">
|
|
<SettingsNav
|
|
v-model="activeCategory"
|
|
:horizontal="isNarrowScreen"
|
|
@scroll-target="scrollToSettingItem"
|
|
/>
|
|
<!-- 导入/导出已移除:实测价值低且占用 tab 底部空间,如需跨设备同步建议直接复制 SQLite 文件 -->
|
|
|
|
<div ref="contentRef" class="settings-content">
|
|
<!-- 外观:theme/language/aiLanguage/showTokenUsage -->
|
|
<AppearanceSection v-if="activeCategory === 'appearance'" />
|
|
|
|
<!-- AI 模型:provider 列表 + 负载均衡池 + 模型拉取 -->
|
|
<ProviderPanel
|
|
v-else-if="activeCategory === 'ai-model'"
|
|
ref="providerPanelRef"
|
|
:confirm-dialog="confirmDialog"
|
|
@toast="showToast"
|
|
@providers-changed="onProvidersChanged"
|
|
/>
|
|
|
|
<!-- 知识库:auto_extract/trigger_mode/embedding_* -->
|
|
<KnowledgePanel
|
|
v-else-if="activeCategory === 'knowledge'"
|
|
:ai-providers="aiProviders"
|
|
/>
|
|
|
|
<!-- 性能:global/per-conv concurrency/max-iterations/max-retries -->
|
|
<PerformanceSection v-else-if="activeCategory === 'performance'" />
|
|
|
|
<!-- 安全:allowed_dirs -->
|
|
<AllowedDirsPanel
|
|
v-else-if="activeCategory === 'security'"
|
|
@toast="showToast"
|
|
/>
|
|
|
|
<!-- 高级:autoExecuteMode/logLevel + 连接(暂留待定) -->
|
|
<template v-else-if="activeCategory === 'advanced'">
|
|
<AdvancedSection />
|
|
<ConnectionPanel
|
|
ref="connectionPanelRef"
|
|
:confirm-dialog="confirmDialog"
|
|
@toast="showToast"
|
|
/>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, shallowRef, onMounted, onUnmounted, nextTick } from 'vue'
|
|
import { useConfirm } from '@/composables/useConfirm'
|
|
import { useToast } from '@/composables/useToast'
|
|
import ConfirmDialog from '@/components/ConfirmDialog.vue'
|
|
import SettingsNav, { type SettingsCategory } from '@/components/settings/SettingsNav.vue'
|
|
import ProviderPanel from '@/components/settings/ProviderPanel.vue'
|
|
import ConnectionPanel from '@/components/settings/ConnectionPanel.vue'
|
|
import AppearanceSection from '@/components/settings/AppearanceSection.vue'
|
|
import PerformanceSection from '@/components/settings/PerformanceSection.vue'
|
|
import AdvancedSection from '@/components/settings/AdvancedSection.vue'
|
|
import KnowledgePanel from '@/components/settings/KnowledgePanel.vue'
|
|
import AllowedDirsPanel from '@/components/settings/AllowedDirsPanel.vue'
|
|
// SettingsImportExport 已移除(价值低,占用 UI 空间;跨设备同步建议直接复制 SQLite)
|
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
|
import { aiApi } from '@/api'
|
|
import type { AiProviderConfig } from '@/api/types'
|
|
import { SETTINGS_INDEX } from '@/components/settings/searchIndex'
|
|
import { t as ti18n } from '@/i18n/i18n-helpers'
|
|
|
|
// ============================================================
|
|
// Settings 壳 — toast/confirm 共享 UI + master-detail 布局组合各 Section/Panel
|
|
// (阶段3 UX 重构:左导航 + 右按 activeCategory 渲染,各功能域 state/methods 已拆到子组件)
|
|
// ============================================================
|
|
|
|
// 顶部轻量提示(useToast composable,消除 4 处重复;默认 3000ms)
|
|
const { toast, showToast } = useToast()
|
|
|
|
// 确认弹层状态机抽至 composables/useConfirm(原 4 视图重复:Projects/ProjectDetail/Ideas/Settings)
|
|
// 模板 confirmState.visible/msg 在 <script setup> 中自动解包 ref,沿用既有内联确认弹层
|
|
const { confirmState, confirmDialog, answerConfirm } = useConfirm()
|
|
|
|
// 子面板 ref(挂载时触发各自 load,卸载时清子面板 timer)
|
|
const providerPanelRef = shallowRef<InstanceType<typeof ProviderPanel> | null>(null)
|
|
const connectionPanelRef = shallowRef<InstanceType<typeof ConnectionPanel> | null>(null)
|
|
|
|
// ============================================================
|
|
// master-detail:activeCategory 持久化(appSettings,默认 appearance)
|
|
// 用 computed 包 useSetting,双向绑 SettingsNav v-model
|
|
// ============================================================
|
|
const appSettings = useAppSettingsStore()
|
|
const activeCategory = computed<SettingsCategory>({
|
|
get: () => appSettings.get<SettingsCategory>('df-settings-active-category', 'appearance'),
|
|
set: (v) => { void appSettings.set('df-settings-active-category', v) },
|
|
})
|
|
|
|
// 窄屏响应式:监听窗口宽度,nav 折叠为横向 tab(避免依赖仅 CSS 的 :has,
|
|
// 并让 SettingsNav 按横向模式渲染 list)
|
|
const isNarrowScreen = ref(false)
|
|
function syncNarrow() {
|
|
isNarrowScreen.value = window.innerWidth <= 768
|
|
}
|
|
let _resizeTimer: ReturnType<typeof setTimeout> | null = null
|
|
function onResize() {
|
|
if (_resizeTimer) clearTimeout(_resizeTimer)
|
|
_resizeTimer = setTimeout(syncNarrow, 100)
|
|
}
|
|
|
|
// ============================================================
|
|
// 设置搜索滚动定位(阶段4)
|
|
// ------------------------------------------------------------
|
|
// SettingsNav 搜索命中首匹配项时上抛 scroll-target(key)。
|
|
// 此处据 key 查 SETTINGS_INDEX 取 labelKey → t() 得当前 locale 文本,
|
|
// 在右侧内容 DOM 中找匹配文本的 .setting-label(表单型 Section)或
|
|
// .panel-header h2(列表型面板如 ProviderPanel),scrollIntoView 平滑定位。
|
|
// 用文本匹配而非 data-* 属性:避免改动各 Section(严守"仅改 plan 列出文件")。
|
|
// ============================================================
|
|
const contentRef = ref<HTMLElement | null>(null)
|
|
|
|
function scrollToSettingItem(itemKey: string) {
|
|
const entry = SETTINGS_INDEX.find((e) => e.key === itemKey)
|
|
if (!entry) return
|
|
// 用 i18n-helpers 的 t(规避 vue-i18n 动态 key 的 TS2589,见 i18n-helpers.ts 注释)
|
|
const labelText = ti18n(entry.labelKey)
|
|
// 等右侧对应 Section v-if 切换后挂载(nextTick);再用二次 rAF 确保布局就绪
|
|
void nextTick(() => {
|
|
requestAnimationFrame(() => {
|
|
const root = contentRef.value
|
|
if (!root) return
|
|
// 优先 .setting-label 文本(表单型);退而求 .panel-header h2(面板标题)
|
|
const labels = root.querySelectorAll<HTMLElement>('.setting-label')
|
|
let target: HTMLElement | null = null
|
|
for (const el of labels) {
|
|
if (el.textContent?.trim() === labelText.trim()) {
|
|
target = el.closest<HTMLElement>('.setting-row') ?? el
|
|
break
|
|
}
|
|
}
|
|
if (!target) {
|
|
const headers = root.querySelectorAll<HTMLElement>('.panel-header h2')
|
|
for (const el of headers) {
|
|
if (el.textContent?.trim() === labelText.trim()) {
|
|
target = el
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if (target) {
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
// ============================================================
|
|
// KnowledgePanel embedding provider 下拉依赖 provider 列表。
|
|
// 阶段3 改 master-detail 后 ProviderPanel 仅在 ai-model 类挂载,
|
|
// knowledge 类切到时 ProviderPanel 可能未挂载 → 此处独立拉一次 providers
|
|
// 保证 KnowledgePanel 始终拿到 embedding provider 候选(与 ProviderPanel 自身
|
|
// onMounted loadProviders 互补,后者仅刷新 UI 列表 + aiStore)。
|
|
// ============================================================
|
|
const aiProviders = ref<AiProviderConfig[]>([])
|
|
|
|
async function loadAiProviders() {
|
|
try {
|
|
aiProviders.value = await aiApi.listProviders()
|
|
} catch (e) {
|
|
console.error('加载 AI 提供商失败:', e)
|
|
}
|
|
}
|
|
|
|
function onProvidersChanged() {
|
|
// ProviderPanel 自己 reload 后上抛,优先取其内存副本(最新),失败回退此处独立拉
|
|
aiProviders.value = providerPanelRef.value?.aiProviders ?? aiProviders.value
|
|
}
|
|
|
|
onMounted(() => {
|
|
syncNarrow()
|
|
window.addEventListener('resize', onResize)
|
|
// provider 列表预拉(knowledge 类依赖);connection 列表 advanced 类挂载时自载
|
|
void loadAiProviders()
|
|
})
|
|
|
|
// 组件卸载清 resize timer;toast 清理由 useToast 自动管理;各子面板自管自身 debounce timer
|
|
onUnmounted(() => {
|
|
if (_resizeTimer) clearTimeout(_resizeTimer)
|
|
window.removeEventListener('resize', onResize)
|
|
providerPanelRef.value?.clearPoolTimer()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.settings { padding: 16px 20px 20px; }
|
|
|
|
/* ===== 顶部 Toast 提示 ===== */
|
|
.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-enter-active, .toast-leave-active { transition: opacity 0.2s, transform 0.2s; }
|
|
.toast-enter-from, .toast-leave-to { opacity: 0; transform: translate(-50%, -8px); }
|
|
|
|
/* 确认弹层已替换为 ConfirmDialog 组件(不再需要自建 CSS) */
|
|
|
|
/* page-header / btn 等已提取到 global.css 全局 */
|
|
|
|
/* ===== master-detail 布局(阶段3 UX 重构) =====
|
|
左 SettingsNav(~200px 固定)+ 右 content(flex 占满 + 纵向滚动)。
|
|
nav 宽度/折叠由 SettingsNav scoped 控制;此处仅布局骨架。 */
|
|
.settings-body {
|
|
display: flex;
|
|
gap: 20px;
|
|
align-items: flex-start;
|
|
}
|
|
.settings-content {
|
|
flex: 1;
|
|
min-width: 0; /* 允许收缩,防子面板溢出撑破 flex */
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--df-gap-grid);
|
|
}
|
|
|
|
/* .btn / .btn-primary / .btn-ghost / .btn-sm 已迁移到 settings.css 全局,
|
|
本壳的 toast/confirm 内联按钮沿用全局基础样式;此处仅保留全局未定义的 .btn-danger。 */
|
|
|
|
/* ===== 窄屏响应式:nav 折叠为顶部横向 tab(由 SettingsNav is-horizontal 渲染) ===== */
|
|
@media (max-width: 768px) {
|
|
.settings-body {
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
}
|
|
</style>
|