Files
DevFlow/src/views/Settings.vue
绝尘 968c3df5ab 新增: F-260619-03 PhaseA AI工具文件访问动态权限白名单
- state.rs: AllowedDirs(persistent HashSet + is_authorized workspace_root+starts_with + canonicalize) + AppState allowed_dirs Arc + reload/set/get Settings KV
- tool_registry.rs: resolve_workspace_path 多目录白名单双校验(词法+canonicalize) + handler闭包引入AllowedDirs Arc(9文件工具) + run_command不走白名单
- commands/config.rs: ai_get/set_allowed_dirs IPC + lib.rs注册
- 前端: api/ai.ts + AllowedDirsPanel.vue(Settings授权目录UI) + i18n
- 测试: 5 AllowedDirs单测 + 基线适配(29工具不变)
workspace_root始终授权(零回归); Phase A仅持久化(不含B弹窗/C写约束)
主代兜底: cargo check devflow 0 + test 129 + vue-tsc 0
2026-06-19 16:41:45 +08:00

187 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="settings">
<Transition name="toast">
<div v-if="toast.visible" class="toast" :class="'toast-' + toast.type">{{ toast.msg }}</div>
</Transition>
<Transition name="confirm">
<div v-if="confirmState.visible" class="confirm-mask" @click.self="answerConfirm(false)">
<div class="confirm-box">
<div class="confirm-msg">{{ confirmState.msg }}</div>
<div class="confirm-actions">
<button class="btn btn-ghost btn-sm" @click="answerConfirm(false)">{{ $t('common.cancel') }}</button>
<button class="btn btn-danger btn-sm" @click="answerConfirm(true)">{{ $t('common.delete') }}</button>
</div>
</div>
</div>
</Transition>
<!-- 页面头部 -->
<header class="page-header">
<h1>{{ $t('settings.title') }}</h1>
</header>
<div class="settings-grid">
<!-- AI 提供商(列表 + 表单)+ F-04c 负载均衡池 -->
<ProviderPanel
ref="providerPanelRef"
:confirm-dialog="confirmDialog"
@toast="showToast"
@providers-changed="onProvidersChanged"
/>
<!-- 连接管理(localStorage CRUD) -->
<ConnectionPanel
ref="connectionPanelRef"
:confirm-dialog="confirmDialog"
@toast="showToast"
/>
<!-- 通用设置(主题/语言/并发/Agent 轮次 即时调 IPC) -->
<GeneralPanel />
<!-- 知识库配置(后端 IPC,embedding provider 依赖 ProviderPanel 列表) -->
<KnowledgePanel :ai-providers="aiProviders" />
<!-- F-260619-03 Phase A: AI 工具授权目录(白名单持久化) -->
<AllowedDirsPanel />
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, shallowRef, onMounted, onUnmounted } from 'vue'
import { useConfirm } from '@/composables/useConfirm'
import ProviderPanel from '@/components/settings/ProviderPanel.vue'
import ConnectionPanel from '@/components/settings/ConnectionPanel.vue'
import GeneralPanel from '@/components/settings/GeneralPanel.vue'
import KnowledgePanel from '@/components/settings/KnowledgePanel.vue'
import AllowedDirsPanel from '@/components/settings/AllowedDirsPanel.vue'
import type { AiProviderConfig } from '@/api/types'
// ============================================================
// Settings 壳 — toast/confirm 共享 UI + 组合 4 子面板
// (纯重构零功能:各功能域 state/methods/CSS 已拆到子组件)
// ============================================================
// 顶部轻量提示(错误/警告/完成3s 自动消失;零依赖,替代未接入的 Arco Message
const toast = reactive({ visible: false, msg: '', type: 'info' as 'error' | 'warning' | 'info' })
let _toastTimer: ReturnType<typeof setTimeout> | null = null
function showToast(msg: string, type: 'error' | 'warning' | 'info' = 'info') {
toast.msg = msg
toast.type = type
toast.visible = true
if (_toastTimer) clearTimeout(_toastTimer)
_toastTimer = setTimeout(() => { toast.visible = false }, 3000)
}
// 确认弹层状态机抽至 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)
// KnowledgePanel embedding provider 下拉依赖 ProviderPanel 的 aiProviders
// ProviderPanel loadProviders 后上抛 providers-changed,此处同步本地副本
const aiProviders = ref<AiProviderConfig[]>([])
function onProvidersChanged() {
aiProviders.value = providerPanelRef.value?.aiProviders ?? []
}
onMounted(() => {
providerPanelRef.value?.loadProviders()
connectionPanelRef.value?.loadConnections()
})
// 组件卸载清 toast timer;各子面板自管自身 debounce timer(各自 onUnmounted)
onUnmounted(() => {
if (_toastTimer) clearTimeout(_toastTimer)
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); }
/* ===== 确认弹层 ===== */
.confirm-mask {
position: fixed;
inset: 0;
z-index: 1100;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.confirm-box {
background: var(--df-bg-card);
border: 0.5px solid var(--df-border);
border-radius: var(--df-radius-lg);
padding: 20px;
min-width: 280px;
max-width: 360px;
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
}
.confirm-msg { font-size: 13px; color: var(--df-text); line-height: 1.5; margin-bottom: 16px; }
.confirm-actions { display: flex; justify-content: flex-end; gap: 8px; }
.btn-danger { background: var(--df-danger); color: #fff; }
.btn-danger:hover { filter: brightness(1.1); }
.confirm-enter-active, .confirm-leave-active { transition: opacity 0.15s; }
.confirm-enter-from, .confirm-leave-to { opacity: 0; }
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--df-gap-page);
}
.page-header h1 { font-size: 24px; font-weight: 500; color: var(--df-text); }
/* ===== 按钮 ===== */
.btn {
padding: 8px 16px;
border: none;
border-radius: var(--df-radius-sm);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.btn-primary { background: var(--df-accent); color: #fff; }
.btn-primary:hover { background: var(--df-accent-hover); }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-ghost { background: transparent; color: var(--df-text-secondary); border: 0.5px solid var(--df-border); }
.btn-ghost:hover { background: var(--df-bg-card); color: var(--df-text); }
.btn-sm { padding: 4px 12px; font-size: 12px; }
/* ===== 设置网格 ===== */
.settings-grid {
display: flex;
flex-direction: column;
gap: var(--df-gap-grid);
}
/* 注:.panel / .panel-header / .empty-hint 等子组件内元素的样式
已随功能域拆到各子组件 scoped(父 scoped 无法穿透命中子组件根 section),
各子组件自带面板容器 + 按钮基础样式副本,确保零视觉变化。 */
</style>