719 lines
17 KiB
Vue
719 lines
17 KiB
Vue
<template>
|
||
<div class="settings-page">
|
||
<!-- 左侧窄边栏 -->
|
||
<aside class="settings-sidebar">
|
||
<nav class="sidebar-nav">
|
||
<a
|
||
v-for="item in navItems"
|
||
:key="item.key"
|
||
:class="['nav-item', { active: activeTab === item.key }]"
|
||
@click="activeTab = item.key"
|
||
>
|
||
<component :is="item.icon" :size="18" :stroke-width="1.5" />
|
||
<span>{{ item.label }}</span>
|
||
</a>
|
||
</nav>
|
||
|
||
<!-- 底部状态 -->
|
||
<div class="sidebar-footer">
|
||
<div class="connection-status">
|
||
<span :class="['status-dot', { connected: isConnected }]"></span>
|
||
<span class="status-text">{{ statusText }}</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<main class="settings-main">
|
||
<div class="settings-content">
|
||
<!-- 页面标题 -->
|
||
<header class="page-header">
|
||
<h1>设置</h1>
|
||
<p class="page-description">配置 AI 模型提供商和连接参数</p>
|
||
</header>
|
||
|
||
<!-- 分割线 -->
|
||
<hr class="divider">
|
||
|
||
<!-- 供应商选择 -->
|
||
<section class="settings-section">
|
||
<h2 class="section-title">模型提供商</h2>
|
||
|
||
<!-- Segmented Control 风格 -->
|
||
<div class="provider-selector">
|
||
<button
|
||
v-for="provider in providers"
|
||
:key="provider.value"
|
||
:class="['provider-btn', { active: config.provider === provider.value }]"
|
||
@click="selectProvider(provider.value)"
|
||
>
|
||
{{ provider.label }}
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 选中提供商说明 -->
|
||
<div class="provider-info" v-if="selectedProvider">
|
||
<span class="provider-desc">{{ selectedProvider.desc }}</span>
|
||
<span class="provider-pricing">{{ selectedProvider.pricing }}</span>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 连接配置 -->
|
||
<section class="settings-section">
|
||
<h2 class="section-title">连接配置</h2>
|
||
|
||
<!-- API 密钥配置 -->
|
||
<div v-if="showApiKey && selectedProvider" class="form-group">
|
||
<label class="form-label">API 密钥</label>
|
||
<input
|
||
v-model="apiKey"
|
||
type="password"
|
||
class="form-input"
|
||
:placeholder="apiKeyPlaceholder"
|
||
>
|
||
<div class="form-hint">
|
||
获取地址:<a :href="selectedProvider.apiUrl" target="_blank" rel="noopener">{{ selectedProvider.apiLabel }}</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Ollama 地址配置 -->
|
||
<div v-if="config.provider === 'ollama'" class="form-group">
|
||
<label class="form-label">服务地址</label>
|
||
<input
|
||
v-model="config.ollama_host"
|
||
type="text"
|
||
class="form-input"
|
||
placeholder="http://127.0.0.1:11434"
|
||
>
|
||
<div class="form-actions">
|
||
<button class="btn-secondary" @click="testConnection" :disabled="testing">
|
||
{{ testing ? '测试中...' : '测试连接' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 高级选项(折叠) -->
|
||
<details class="advanced-options">
|
||
<summary>高级选项</summary>
|
||
<div class="advanced-content">
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">Temperature</label>
|
||
<input v-model.number="config.temperature" type="number" step="0.1" min="0" max="2" class="form-input">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">Max Tokens</label>
|
||
<input v-model.number="config.max_tokens" type="number" step="128" min="128" max="8192" class="form-input">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
</section>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<footer class="settings-footer">
|
||
<div class="footer-status">{{ saveStatus }}</div>
|
||
<div class="footer-actions">
|
||
<button class="btn-ghost" @click="resetConfig">重置</button>
|
||
<button class="btn-primary" @click="saveConfig" :disabled="saving">
|
||
{{ saving ? '保存中...' : '保存配置' }}
|
||
</button>
|
||
</div>
|
||
</footer>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- 重启确认弹窗 -->
|
||
<div v-if="showRestartModal" class="modal-overlay" @click.self="showRestartModal = false">
|
||
<div class="modal">
|
||
<div class="modal-header">
|
||
<h3>需要重启后端</h3>
|
||
<button class="modal-close" @click="showRestartModal = false">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<p>LLM 配置已更改,需要重启后端服务才能生效。</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-ghost" @click="showRestartModal = false">取消</button>
|
||
<button class="btn-warning" @click="restartBackend">确认重启</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, h } from 'vue'
|
||
import axios from 'axios'
|
||
|
||
// Lucide-style icons (inline SVG)
|
||
const SettingsIcon = () => h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.5 }, [
|
||
h('path', { d: 'M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z' }),
|
||
h('circle', { cx: '12', cy: '12', r: '3' })
|
||
])
|
||
|
||
const CloudIcon = () => h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.5 }, [
|
||
h('path', { d: 'M17.5 19c0-1.7-1.3-3-3-3h-11c-1.7 0-3 1.3-3 3s1.3 3 3 3h11c1.7 0 3-1.3 3-3z' }),
|
||
h('path', { d: 'M17.5 16c2.5 0 4.5-2 4.5-4.5S20 7 17.5 7c-.4 0-.8.1-1.2.2C15.6 5.7 13.9 5 12 5c-2.8 0-5.1 2-5.7 4.7-.5-.2-1-.2-1.5-.2-2.5 0-4.5 2-4.5 4.5s2 4.5 4.5 4.5h12.2z' })
|
||
])
|
||
|
||
const KeyIcon = () => h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.5 }, [
|
||
h('path', { d: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4' })
|
||
])
|
||
|
||
// 侧边栏导航
|
||
const navItems = [
|
||
{ key: 'provider', label: '模型服务', icon: CloudIcon },
|
||
{ key: 'general', label: '通用设置', icon: SettingsIcon },
|
||
{ key: 'advanced', label: '高级选项', icon: KeyIcon }
|
||
]
|
||
|
||
const activeTab = ref('provider')
|
||
|
||
// 配置数据
|
||
const config = reactive({
|
||
provider: 'deepseek',
|
||
deepseek_api_key: '',
|
||
openai_api_key: '',
|
||
qwen_api_key: '',
|
||
ollama_host: 'http://127.0.0.1:11434',
|
||
temperature: 0.7,
|
||
max_tokens: 2048
|
||
})
|
||
|
||
// 提供商数据
|
||
const providers = [
|
||
{ value: 'deepseek', label: 'DeepSeek', desc: '高质量、低成本的大语言模型', pricing: '¥1/百万 tokens', apiUrl: 'https://platform.deepseek.com/api_keys', apiLabel: 'platform.deepseek.com' },
|
||
{ value: 'openai', label: 'OpenAI', desc: '业界领先的 GPT 系列模型', pricing: '~$15/百万 tokens', apiUrl: 'https://platform.openai.com/api-keys', apiLabel: 'platform.openai.com' },
|
||
{ value: 'qwen', label: 'Qwen', desc: '阿里云通义千问系列', pricing: '¥0.5/百万 tokens', apiUrl: 'https://dashscope.console.aliyun.com/apiKey', apiLabel: 'dashscope.console.aliyun.com' },
|
||
{ value: 'ollama', label: 'Ollama', desc: '本地运行的开源模型', pricing: '免费', apiUrl: '', apiLabel: '' }
|
||
]
|
||
|
||
// 状态
|
||
const saving = ref(false)
|
||
const testing = ref(false)
|
||
const showRestartModal = ref(false)
|
||
const saveStatus = ref('')
|
||
const isConnected = ref(false)
|
||
const statusText = ref('未连接')
|
||
|
||
// 计算属性
|
||
const selectedProvider = computed(() => providers.find(p => p.value === config.provider) || providers[0])
|
||
|
||
const showApiKey = computed(() => ['deepseek', 'openai', 'qwen'].includes(config.provider))
|
||
|
||
const apiKey = computed({
|
||
get: () => {
|
||
if (config.provider === 'deepseek') return config.deepseek_api_key
|
||
if (config.provider === 'openai') return config.openai_api_key
|
||
if (config.provider === 'qwen') return config.qwen_api_key
|
||
return ''
|
||
},
|
||
set: (value: string) => {
|
||
if (config.provider === 'deepseek') config.deepseek_api_key = value
|
||
if (config.provider === 'openai') config.openai_api_key = value
|
||
if (config.provider === 'qwen') config.qwen_api_key = value
|
||
}
|
||
})
|
||
|
||
const apiKeyPlaceholder = computed(() => {
|
||
if (config.provider === 'deepseek') return 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||
if (config.provider === 'openai') return 'sk-proj-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||
if (config.provider === 'qwen') return 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
|
||
return ''
|
||
})
|
||
|
||
// 方法
|
||
const selectProvider = (value: string) => {
|
||
config.provider = value
|
||
saveStatus.value = `已切换到 ${providers.find(p => p.value === value)?.label}`
|
||
}
|
||
|
||
const testConnection = async () => {
|
||
testing.value = true
|
||
try {
|
||
const response = await axios.get(`${config.ollama_host.replace(':11434', ':8000')}/ai/status`, { timeout: 5000 })
|
||
if (response.data.status === 'ready') {
|
||
isConnected.value = true
|
||
statusText.value = `${response.data.model_count} 个模型`
|
||
saveStatus.value = '连接成功'
|
||
} else {
|
||
isConnected.value = false
|
||
statusText.value = '未就绪'
|
||
}
|
||
} catch {
|
||
isConnected.value = false
|
||
statusText.value = '连接失败'
|
||
} finally {
|
||
testing.value = false
|
||
}
|
||
}
|
||
|
||
const loadConfig = async () => {
|
||
try {
|
||
const response = await axios.get('http://127.0.0.1:8000/ai/config')
|
||
config.provider = response.data.provider
|
||
config.ollama_host = response.data.ollama_host
|
||
} catch {}
|
||
}
|
||
|
||
const saveConfig = async () => {
|
||
saving.value = true
|
||
try {
|
||
await axios.post('http://127.0.0.1:8000/ai/config', {
|
||
provider: config.provider,
|
||
deepseek_api_key: config.deepseek_api_key || undefined,
|
||
openai_api_key: config.openai_api_key || undefined,
|
||
qwen_api_key: config.qwen_api_key || undefined
|
||
})
|
||
saveStatus.value = '配置已保存'
|
||
showRestartModal.value = true
|
||
} catch (error: any) {
|
||
saveStatus.value = '保存失败: ' + (error.response?.data?.detail || error.message)
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const resetConfig = () => {
|
||
config.provider = 'deepseek'
|
||
config.temperature = 0.7
|
||
config.max_tokens = 2048
|
||
saveStatus.value = '配置已重置'
|
||
}
|
||
|
||
const restartBackend = async () => {
|
||
try {
|
||
await axios.post('http://127.0.0.1:8000/ai/stop')
|
||
saveStatus.value = '后端已停止,请手动启动'
|
||
showRestartModal.value = false
|
||
} catch {
|
||
saveStatus.value = '停止失败'
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadConfig()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 全局样式 */
|
||
.settings-page {
|
||
display: flex;
|
||
height: 100%;
|
||
background: #09090b;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'PingFang SC', sans-serif;
|
||
}
|
||
|
||
/* 左侧边栏 */
|
||
.settings-sidebar {
|
||
width: 200px;
|
||
border-right: 1px solid #27272a;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
flex: 1;
|
||
padding: 16px 12px;
|
||
}
|
||
|
||
.nav-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 12px;
|
||
font-size: 14px;
|
||
color: #a1a1aa;
|
||
text-decoration: none;
|
||
border-radius: 6px;
|
||
margin-bottom: 2px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background: #18181b;
|
||
color: #fafafa;
|
||
}
|
||
|
||
.nav-item.active {
|
||
background: #27272a;
|
||
color: #fafafa;
|
||
}
|
||
|
||
.sidebar-footer {
|
||
padding: 16px;
|
||
border-top: 1px solid #27272a;
|
||
}
|
||
|
||
.connection-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: #71717a;
|
||
}
|
||
|
||
.status-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: #ef4444;
|
||
}
|
||
|
||
.status-dot.connected {
|
||
background: #22c55e;
|
||
}
|
||
|
||
/* 主内容区 */
|
||
.settings-main {
|
||
flex: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.settings-content {
|
||
width: 100%;
|
||
max-width: 720px;
|
||
padding: 40px 48px 120px;
|
||
}
|
||
|
||
/* 页面标题 */
|
||
.page-header h1 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #fafafa;
|
||
margin: 0 0 8px 0;
|
||
}
|
||
|
||
.page-description {
|
||
font-size: 14px;
|
||
color: #71717a;
|
||
margin: 0;
|
||
}
|
||
|
||
.divider {
|
||
border: none;
|
||
border-top: 1px solid #27272a;
|
||
margin: 24px 0 32px;
|
||
}
|
||
|
||
/* 设置区块 */
|
||
.settings-section {
|
||
margin-bottom: 40px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #a1a1aa;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin: 0 0 16px 0;
|
||
}
|
||
|
||
/* 供应商选择器 - Segmented Control 风格 */
|
||
.provider-selector {
|
||
display: flex;
|
||
background: #18181b;
|
||
border: 1px solid #27272a;
|
||
border-radius: 8px;
|
||
padding: 4px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.provider-btn {
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #a1a1aa;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.provider-btn:hover {
|
||
color: #fafafa;
|
||
}
|
||
|
||
.provider-btn.active {
|
||
background: #27272a;
|
||
color: #fafafa;
|
||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.provider-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.provider-desc {
|
||
font-size: 13px;
|
||
color: #71717a;
|
||
}
|
||
|
||
.provider-pricing {
|
||
font-size: 12px;
|
||
color: #52525b;
|
||
padding: 4px 8px;
|
||
background: #18181b;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
/* 表单 */
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-label {
|
||
display: block;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #a1a1aa;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-input {
|
||
width: 100%;
|
||
height: 36px;
|
||
padding: 0 12px;
|
||
font-size: 14px;
|
||
color: #fafafa;
|
||
background: #18181b;
|
||
border: 1px solid #27272a;
|
||
border-radius: 6px;
|
||
box-sizing: border-box;
|
||
transition: border-color 0.15s ease;
|
||
}
|
||
|
||
.form-input:focus {
|
||
outline: none;
|
||
border-color: #4f46e5;
|
||
}
|
||
|
||
.form-input::placeholder {
|
||
color: #52525b;
|
||
}
|
||
|
||
.form-hint {
|
||
font-size: 12px;
|
||
color: #52525b;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.form-hint a {
|
||
color: #52525b;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.form-hint a:hover {
|
||
color: #71717a;
|
||
}
|
||
|
||
.form-actions {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.form-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
/* 高级选项 */
|
||
.advanced-options {
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.advanced-options summary {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #71717a;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
list-style: none;
|
||
}
|
||
|
||
.advanced-options summary::-webkit-details-marker {
|
||
display: none;
|
||
}
|
||
|
||
.advanced-options summary::before {
|
||
content: '▶';
|
||
font-size: 10px;
|
||
margin-right: 6px;
|
||
transition: transform 0.15s ease;
|
||
}
|
||
|
||
.advanced-options[open] summary::before {
|
||
transform: rotate(90deg);
|
||
}
|
||
|
||
.advanced-content {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #27272a;
|
||
}
|
||
|
||
/* 底部操作栏 */
|
||
.settings-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding-top: 24px;
|
||
border-top: 1px solid #27272a;
|
||
}
|
||
|
||
.footer-status {
|
||
font-size: 13px;
|
||
color: #71717a;
|
||
}
|
||
|
||
.footer-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 按钮样式 */
|
||
.btn-ghost {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #a1a1aa;
|
||
background: transparent;
|
||
border: 1px solid #27272a;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.btn-ghost:hover:not(:disabled) {
|
||
color: #fafafa;
|
||
border-color: #3f3f46;
|
||
}
|
||
|
||
.btn-secondary {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #fafafa;
|
||
background: #27272a;
|
||
border: 1px solid #3f3f46;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.btn-secondary:hover:not(:disabled) {
|
||
background: #3f3f46;
|
||
}
|
||
|
||
.btn-primary {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #fafafa;
|
||
background: #4f46e5;
|
||
border: 1px solid #4f46e5;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: all 0.15s ease;
|
||
}
|
||
|
||
.btn-primary:hover:not(:disabled) {
|
||
background: #4338ca;
|
||
}
|
||
|
||
.btn-primary:disabled,
|
||
.btn-secondary:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.btn-warning {
|
||
padding: 8px 16px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #fafafa;
|
||
background: #b91c1c;
|
||
border: 1px solid #b91c1c;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #991b1b;
|
||
}
|
||
|
||
/* Modal */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal {
|
||
background: #18181b;
|
||
border: 1px solid #27272a;
|
||
border-radius: 12px;
|
||
width: 100%;
|
||
max-width: 420px;
|
||
padding: 24px;
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #fafafa;
|
||
margin: 0;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 28px;
|
||
height: 28px;
|
||
font-size: 20px;
|
||
color: #71717a;
|
||
background: transparent;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.modal-close:hover {
|
||
background: #27272a;
|
||
}
|
||
|
||
.modal-body {
|
||
font-size: 14px;
|
||
color: #a1a1aa;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 8px;
|
||
}
|
||
</style>
|