新增: 初始化 DevFlow 项目仓库
Tauri 2 + Vue 3 + Vite 6 桌面应用,Rust workspace 含 13 个 crate (df-ai / df-storage / df-workflow / df-core / df-execute 等)。 核心能力:AI 聊天 agentic 循环(工具调用+人工审批)、工作流引擎、 任务/想法/项目/阶段管理、可追溯性,及配套前端组件。
This commit is contained in:
696
src/views/Settings.vue
Normal file
696
src/views/Settings.vue
Normal file
@@ -0,0 +1,696 @@
|
||||
<template>
|
||||
<div class="settings">
|
||||
<!-- 页面头部 -->
|
||||
<header class="page-header">
|
||||
<h1>⚙️ 设置</h1>
|
||||
</header>
|
||||
|
||||
<div class="settings-grid">
|
||||
<!-- ═══ AI 模型配置 ═══ -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>🤖 AI 模型配置</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="openProviderForm()">+ 添加 Provider</button>
|
||||
</div>
|
||||
<div class="provider-list" v-if="aiProviders.length > 0">
|
||||
<div class="provider-card" v-for="p in aiProviders" :key="p.id">
|
||||
<div class="provider-top">
|
||||
<div class="provider-info">
|
||||
<span class="provider-name">{{ p.name }}</span>
|
||||
<span v-if="p.is_default" class="default-badge">默认</span>
|
||||
</div>
|
||||
<div class="provider-actions">
|
||||
<button class="btn-link" @click="setDefaultProvider(p.id)" v-if="!p.is_default">设为默认</button>
|
||||
<button class="btn-link" @click="openProviderForm(p)">编辑</button>
|
||||
<button class="btn-link btn-link-danger" @click="deleteProvider(p.id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="provider-detail">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Base URL</span>
|
||||
<span class="detail-value">{{ p.base_url }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">模型</span>
|
||||
<span class="detail-value">{{ p.default_model }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">API Key</span>
|
||||
<span class="detail-value mask">{{ maskKey(p.api_key) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-hint" v-else>暂无 AI 提供商,点击上方按钮添加</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ AI 提供商表单 ═══ -->
|
||||
<section class="panel" v-if="providerForm.visible">
|
||||
<div class="panel-header">
|
||||
<h2>{{ providerForm.editId ? '编辑 Provider' : '添加 Provider' }}</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="providerForm.visible = false">取消</button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label class="form-label">名称</label>
|
||||
<input v-model="providerForm.name" class="setting-input" placeholder="如 DeepSeek、GLM-4" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">协议类型</label>
|
||||
<select v-model="providerForm.providerType" class="setting-select">
|
||||
<option value="openai_compat">OpenAI 兼容(DeepSeek / GLM v4 / OpenAI)</option>
|
||||
<option value="anthropic">Anthropic 兼容(GLM 订阅 / Claude)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Base URL</label>
|
||||
<input v-model="providerForm.baseUrl" class="setting-input" placeholder="https://api.deepseek.com" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">API Key</label>
|
||||
<input v-model="providerForm.apiKey" class="setting-input" type="password" placeholder="sk-..." />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">默认模型</label>
|
||||
<input v-model="providerForm.defaultModel" class="setting-input" placeholder="deepseek-chat" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" @click="saveProvider" :disabled="providerForm.saving">
|
||||
{{ providerForm.saving ? '保存中...' : '保存' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ 连接管理 ═══ -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>🔗 连接管理</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="openConnForm()">+ 添加连接</button>
|
||||
</div>
|
||||
<div class="connection-list" v-if="connections.length > 0">
|
||||
<div class="connection-card" v-for="conn in connections" :key="conn.id">
|
||||
<div class="conn-left">
|
||||
<span class="conn-icon">{{ typeIcon(conn.type) }}</span>
|
||||
<div class="conn-info">
|
||||
<div class="conn-name-row">
|
||||
<span class="conn-name">{{ conn.name }}</span>
|
||||
<span class="conn-type" :class="'type-' + conn.type">{{ typeLabel(conn.type) }}</span>
|
||||
</div>
|
||||
<span class="conn-host">{{ conn.host }}{{ conn.port ? ':' + conn.port : '' }}</span>
|
||||
<span class="conn-user" v-if="conn.user">{{ conn.user }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conn-actions">
|
||||
<button class="btn-link" @click="openConnForm(conn)">编辑</button>
|
||||
<button class="btn-link btn-link-danger" @click="removeConn(conn.id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-hint" v-else>暂无连接,点击上方按钮添加</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ 连接表单 ═══ -->
|
||||
<section class="panel" v-if="connForm.visible">
|
||||
<div class="panel-header">
|
||||
<h2>{{ connForm.editId ? '编辑连接' : '添加连接' }}</h2>
|
||||
<button class="btn btn-ghost btn-sm" @click="connForm.visible = false">取消</button>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<div class="form-field">
|
||||
<label class="form-label">名称</label>
|
||||
<input v-model="connForm.name" class="setting-input" placeholder="flux_dev" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">类型</label>
|
||||
<select v-model="connForm.type" class="setting-select">
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="ssh">SSH</option>
|
||||
<option value="redis">Redis</option>
|
||||
<option value="mongo">MongoDB</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Host</label>
|
||||
<input v-model="connForm.host" class="setting-input" placeholder="127.0.0.1" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">Port</label>
|
||||
<input v-model.number="connForm.port" class="setting-input" type="number" placeholder="3306" />
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<label class="form-label">用户名</label>
|
||||
<input v-model="connForm.user" class="setting-input" placeholder="root" />
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-primary" @click="saveConn">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ 通用设置 ═══ -->
|
||||
<section class="panel">
|
||||
<div class="panel-header">
|
||||
<h2>🎨 通用设置</h2>
|
||||
</div>
|
||||
<div class="general-settings">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">主题</span>
|
||||
<span class="setting-desc">深色 / 浅色模式</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select v-model="settings.theme" class="setting-select">
|
||||
<option value="dark">🌙 深色模式</option>
|
||||
<option value="light">☀️ 浅色模式</option>
|
||||
<option value="system">💻 跟随系统</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">语言</span>
|
||||
<span class="setting-desc">界面显示语言</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select v-model="settings.language" class="setting-select">
|
||||
<option value="zh-CN">简体中文</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">AI 对话语言</span>
|
||||
<span class="setting-desc">AI 回复使用的默认语言</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select v-model="settings.aiLanguage" class="setting-select">
|
||||
<option value="auto">🔄 跟随界面语言</option>
|
||||
<option value="zh-CN">🇨🇳 简体中文</option>
|
||||
<option value="en">🇺🇸 English</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">AI 自动执行</span>
|
||||
<span class="setting-desc">AI 可自动执行低风险操作</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<label class="toggle">
|
||||
<input type="checkbox" v-model="settings.autoExecute" />
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<span class="setting-label">日志级别</span>
|
||||
<span class="setting-desc">工作流日志详细程度</span>
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<select v-model="settings.logLevel" class="setting-select">
|
||||
<option value="error">Error</option>
|
||||
<option value="warn">Warning</option>
|
||||
<option value="info">Info</option>
|
||||
<option value="debug">Debug</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch, onMounted } from 'vue'
|
||||
import { aiApi } from '../api'
|
||||
import type { AiProviderConfig } from '../api/types'
|
||||
import i18n from '../i18n'
|
||||
|
||||
// ============================================================
|
||||
// AI 提供商 — 真实后端 CRUD
|
||||
// ============================================================
|
||||
|
||||
const aiProviders = ref<AiProviderConfig[]>([])
|
||||
|
||||
const providerForm = reactive({
|
||||
visible: false,
|
||||
editId: '' as string,
|
||||
name: '',
|
||||
providerType: 'openai_compat',
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
defaultModel: '',
|
||||
saving: false,
|
||||
})
|
||||
|
||||
async function loadProviders() {
|
||||
try {
|
||||
aiProviders.value = await aiApi.listProviders()
|
||||
} catch (e) {
|
||||
console.error('加载提供商失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function openProviderForm(p?: AiProviderConfig) {
|
||||
if (p) {
|
||||
providerForm.editId = p.id
|
||||
providerForm.name = p.name
|
||||
providerForm.baseUrl = p.base_url
|
||||
providerForm.apiKey = p.api_key
|
||||
providerForm.defaultModel = p.default_model
|
||||
providerForm.providerType = p.provider_type || 'openai_compat'
|
||||
} else {
|
||||
providerForm.editId = ''
|
||||
providerForm.name = ''
|
||||
providerForm.providerType = 'openai_compat'
|
||||
providerForm.baseUrl = ''
|
||||
providerForm.apiKey = ''
|
||||
providerForm.defaultModel = ''
|
||||
}
|
||||
providerForm.visible = true
|
||||
}
|
||||
|
||||
async function saveProvider() {
|
||||
if (!providerForm.name || !providerForm.baseUrl || !providerForm.apiKey || !providerForm.defaultModel) return
|
||||
providerForm.saving = true
|
||||
try {
|
||||
await aiApi.saveProvider({
|
||||
id: providerForm.editId || undefined,
|
||||
name: providerForm.name,
|
||||
providerType: providerForm.providerType,
|
||||
baseUrl: providerForm.baseUrl,
|
||||
apiKey: providerForm.apiKey,
|
||||
defaultModel: providerForm.defaultModel,
|
||||
})
|
||||
providerForm.visible = false
|
||||
await loadProviders()
|
||||
} catch (e) {
|
||||
console.error('保存提供商失败:', e)
|
||||
} finally {
|
||||
providerForm.saving = false
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProvider(id: string) {
|
||||
// 暂无后端 delete 命令,通过 save 空 id 不行,需要后端支持
|
||||
// 先从前端移除,后续补 delete IPC
|
||||
aiProviders.value = aiProviders.value.filter(p => p.id !== id)
|
||||
}
|
||||
|
||||
async function setDefaultProvider(id: string) {
|
||||
try {
|
||||
await aiApi.setProvider(id)
|
||||
await loadProviders()
|
||||
} catch (e) {
|
||||
console.error('设置默认提供商失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
function maskKey(key: string): string {
|
||||
if (!key || key.length < 8) return '••••••••'
|
||||
return key.slice(0, 4) + '••••••••' + key.slice(-4)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 连接管理 — localStorage CRUD
|
||||
// ============================================================
|
||||
|
||||
interface ConnRecord {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
host: string
|
||||
port: number
|
||||
user: string
|
||||
}
|
||||
|
||||
const connections = ref<ConnRecord[]>([])
|
||||
|
||||
const connForm = reactive({
|
||||
visible: false,
|
||||
editId: '' as string,
|
||||
name: '',
|
||||
type: 'mysql',
|
||||
host: '',
|
||||
port: 3306,
|
||||
user: '',
|
||||
})
|
||||
|
||||
const CONN_STORAGE_KEY = 'df-connections'
|
||||
|
||||
function loadConnections() {
|
||||
try {
|
||||
const raw = localStorage.getItem(CONN_STORAGE_KEY)
|
||||
if (raw) connections.value = JSON.parse(raw)
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function persistConnections() {
|
||||
localStorage.setItem(CONN_STORAGE_KEY, JSON.stringify(connections.value))
|
||||
}
|
||||
|
||||
function typeIcon(type: string) {
|
||||
const map: Record<string, string> = { mysql: '🗄️', ssh: '🔐', redis: '⚡', mongo: '🍃' }
|
||||
return map[type] || '🔗'
|
||||
}
|
||||
|
||||
function typeLabel(type: string) {
|
||||
const map: Record<string, string> = { mysql: 'MySQL', ssh: 'SSH', redis: 'Redis', mongo: 'MongoDB' }
|
||||
return map[type] || type
|
||||
}
|
||||
|
||||
function openConnForm(conn?: ConnRecord) {
|
||||
if (conn) {
|
||||
connForm.editId = conn.id
|
||||
connForm.name = conn.name
|
||||
connForm.type = conn.type
|
||||
connForm.host = conn.host
|
||||
connForm.port = conn.port
|
||||
connForm.user = conn.user
|
||||
} else {
|
||||
connForm.editId = ''
|
||||
connForm.name = ''
|
||||
connForm.type = 'mysql'
|
||||
connForm.host = ''
|
||||
connForm.port = 3306
|
||||
connForm.user = ''
|
||||
}
|
||||
connForm.visible = true
|
||||
}
|
||||
|
||||
function saveConn() {
|
||||
if (!connForm.name || !connForm.host) return
|
||||
const record: ConnRecord = {
|
||||
id: connForm.editId || Date.now().toString(36),
|
||||
name: connForm.name,
|
||||
type: connForm.type,
|
||||
host: connForm.host,
|
||||
port: connForm.port,
|
||||
user: connForm.user,
|
||||
}
|
||||
if (connForm.editId) {
|
||||
const idx = connections.value.findIndex(c => c.id === connForm.editId)
|
||||
if (idx >= 0) connections.value[idx] = record
|
||||
} else {
|
||||
connections.value.push(record)
|
||||
}
|
||||
persistConnections()
|
||||
connForm.visible = false
|
||||
}
|
||||
|
||||
function removeConn(id: string) {
|
||||
connections.value = connections.value.filter(c => c.id !== id)
|
||||
persistConnections()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 通用设置
|
||||
// ============================================================
|
||||
|
||||
const settings = reactive({
|
||||
theme: localStorage.getItem('df-theme') || 'dark',
|
||||
language: localStorage.getItem('df-language') || 'zh-CN',
|
||||
aiLanguage: localStorage.getItem('df-ai-language') || 'auto',
|
||||
autoExecute: false,
|
||||
logLevel: 'info',
|
||||
})
|
||||
|
||||
function applyTheme(theme: string) {
|
||||
if (theme === 'system') {
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
if (!prefersDark) document.documentElement.setAttribute('data-theme', 'light')
|
||||
} else if (theme === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', 'light')
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => settings.theme, (val) => {
|
||||
applyTheme(val)
|
||||
localStorage.setItem('df-theme', val)
|
||||
})
|
||||
|
||||
watch(() => settings.aiLanguage, (val) => {
|
||||
localStorage.setItem('df-ai-language', val)
|
||||
})
|
||||
|
||||
watch(() => settings.language, (val) => {
|
||||
localStorage.setItem('df-language', val)
|
||||
// 同步 i18n 实例,让界面立即跟随语言切换
|
||||
i18n.global.locale.value = val as 'zh-CN' | 'en'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
applyTheme(settings.theme)
|
||||
loadProviders()
|
||||
loadConnections()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.settings { padding: 16px 20px 20px; }
|
||||
|
||||
.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; }
|
||||
|
||||
.btn-link {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--df-accent);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
.btn-link:hover { text-decoration: underline; }
|
||||
.btn-link-danger { color: var(--df-danger); }
|
||||
|
||||
/* ===== 设置网格 ===== */
|
||||
.settings-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--df-gap-grid);
|
||||
}
|
||||
|
||||
/* ===== 面板 ===== */
|
||||
.panel {
|
||||
background: var(--df-bg-card);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
padding: var(--df-pad-panel);
|
||||
}
|
||||
.panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--df-gap-head);
|
||||
}
|
||||
.panel-header h2 { font-size: 15px; font-weight: 500; }
|
||||
|
||||
.empty-hint {
|
||||
font-size: 13px;
|
||||
color: var(--df-text-dim);
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
/* ===== Provider 卡片 ===== */
|
||||
.provider-list { display: flex; flex-direction: column; gap: 12px; }
|
||||
.provider-card {
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
.provider-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.provider-info { display: flex; align-items: center; gap: 8px; }
|
||||
.provider-name { font-size: 14px; font-weight: 500; color: var(--df-text); }
|
||||
.default-badge {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-xs);
|
||||
background: rgba(100,255,218,0.15);
|
||||
color: var(--df-success);
|
||||
font-weight: 500;
|
||||
}
|
||||
.provider-actions { display: flex; gap: 4px; }
|
||||
|
||||
.provider-detail { display: flex; flex-direction: column; gap: 6px; }
|
||||
.detail-row { display: flex; align-items: flex-start; gap: 12px; }
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
min-width: 60px;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-secondary);
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.detail-value.mask { color: var(--df-text-dim); letter-spacing: 1px; }
|
||||
|
||||
/* ===== 表单 ===== */
|
||||
.form-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.form-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--df-text-secondary);
|
||||
}
|
||||
.form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
/* ===== 连接卡片 ===== */
|
||||
.connection-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.connection-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 14px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius);
|
||||
}
|
||||
.conn-left { display: flex; align-items: center; gap: 10px; }
|
||||
.conn-icon { font-size: 18px; }
|
||||
.conn-info { display: flex; flex-direction: column; gap: 2px; }
|
||||
.conn-name-row { display: flex; align-items: center; gap: 8px; }
|
||||
.conn-name { font-size: 14px; font-weight: 500; color: var(--df-text); }
|
||||
.conn-type {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
.type-mysql { background: rgba(100,181,246,0.15); color: var(--df-info); }
|
||||
.type-ssh { background: rgba(108,99,255,0.15); color: var(--df-accent); }
|
||||
.type-redis { background: rgba(255,107,107,0.15); color: var(--df-danger); }
|
||||
.type-mongo { background: rgba(100,255,218,0.15); color: var(--df-success); }
|
||||
|
||||
.conn-host, .conn-user {
|
||||
font-size: 12px;
|
||||
color: var(--df-text-dim);
|
||||
font-family: var(--df-font-mono);
|
||||
}
|
||||
.conn-user {
|
||||
background: rgba(90,99,128,0.2);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--df-radius-sm);
|
||||
}
|
||||
.conn-actions { display: flex; gap: 4px; }
|
||||
|
||||
/* ===== 通用设置 ===== */
|
||||
.general-settings { display: flex; flex-direction: column; }
|
||||
.setting-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 0;
|
||||
border-bottom: 0.5px solid var(--df-border);
|
||||
}
|
||||
.setting-row:last-child { border-bottom: none; }
|
||||
.setting-info { display: flex; flex-direction: column; gap: 2px; }
|
||||
.setting-label { font-size: 14px; font-weight: 500; color: var(--df-text); }
|
||||
.setting-desc { font-size: 12px; color: var(--df-text-dim); }
|
||||
|
||||
.setting-control { display: flex; align-items: center; }
|
||||
|
||||
.setting-select {
|
||||
padding: 6px 12px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
color: var(--df-text);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
min-width: 160px;
|
||||
}
|
||||
.setting-select:focus { border-color: var(--df-accent); }
|
||||
|
||||
.setting-input {
|
||||
padding: 6px 12px;
|
||||
background: var(--df-bg);
|
||||
border: 0.5px solid var(--df-border);
|
||||
border-radius: var(--df-radius-sm);
|
||||
color: var(--df-text);
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
min-width: 260px;
|
||||
}
|
||||
.setting-input:focus { border-color: var(--df-accent); }
|
||||
|
||||
/* ===== Toggle 开关 ===== */
|
||||
.toggle {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 40px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle input { opacity: 0; width: 0; height: 0; }
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: var(--df-border);
|
||||
border-radius: var(--df-radius-lg);
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.toggle-slider::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background: var(--df-text-dim);
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s, background 0.2s;
|
||||
}
|
||||
.toggle input:checked + .toggle-slider { background: var(--df-accent); }
|
||||
.toggle input:checked + .toggle-slider::before {
|
||||
transform: translateX(18px);
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user