1061 lines
26 KiB
Vue
1061 lines
26 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<Transition name="modal">
|
||
<div v-if="visible" class="modal-overlay" @click.self="close">
|
||
<div class="settings-modal">
|
||
<!-- 头部 -->
|
||
<div class="modal-header">
|
||
<div class="header-title">
|
||
<Settings :size="22" />
|
||
<h3>设置</h3>
|
||
</div>
|
||
<button class="close-btn" @click="close">
|
||
<X :size="20" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 标签页 -->
|
||
<div class="settings-tabs">
|
||
<button
|
||
v-for="tab in tabs"
|
||
:key="tab.id"
|
||
class="tab-btn"
|
||
:class="{ active: activeTab === tab.id }"
|
||
@click="activeTab = tab.id"
|
||
>
|
||
<component :is="tab.icon" :size="18" />
|
||
<span>{{ tab.label }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 内容区域 -->
|
||
<div class="settings-content">
|
||
<!-- 外观设置 -->
|
||
<div v-show="activeTab === 'appearance'" class="settings-section">
|
||
<div class="section-title">外观</div>
|
||
|
||
<!-- 主题 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">主题</span>
|
||
<span class="setting-desc">选择界面主题</span>
|
||
</div>
|
||
<div class="theme-options">
|
||
<button
|
||
v-for="theme in themeOptions"
|
||
:key="theme.value"
|
||
class="theme-btn"
|
||
:class="{ active: settings.theme === theme.value }"
|
||
@click="setTheme(theme.value)"
|
||
>
|
||
<component :is="theme.icon" :size="18" />
|
||
<span>{{ theme.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 字体大小 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">字体大小</span>
|
||
<span class="setting-desc">调整界面文字大小</span>
|
||
</div>
|
||
<div class="font-size-options">
|
||
<button
|
||
v-for="size in fontSizeOptions"
|
||
:key="size.value"
|
||
class="size-btn"
|
||
:class="{ active: settings.fontSize === size.value }"
|
||
@click="setFontSize(size.value)"
|
||
>
|
||
{{ size.label }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 语言 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">语言</span>
|
||
<span class="setting-desc">选择界面语言</span>
|
||
</div>
|
||
<FormSelect
|
||
:model-value="settings.language"
|
||
:options="languageOptions"
|
||
@update:model-value="
|
||
updateSettings({ language: $event as string })
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 紧凑模式 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">紧凑模式</span>
|
||
<span class="setting-desc">减少界面元素间距</span>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.compactMode"
|
||
@update:model-value="updateSettings({ compactMode: $event })"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 显示时间戳 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">显示时间戳</span>
|
||
<span class="setting-desc">在消息旁显示发送时间</span>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.showTimestamp"
|
||
@update:model-value="
|
||
updateSettings({ showTimestamp: $event })
|
||
"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 对话设置 -->
|
||
<div v-show="activeTab === 'chat'" class="settings-section">
|
||
<div class="section-title">对话</div>
|
||
|
||
<!-- 发送方式 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">Enter 发送</span>
|
||
<span class="setting-desc"
|
||
>按 Enter 直接发送消息(关闭后需要 Ctrl+Enter)</span
|
||
>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.sendOnEnter"
|
||
@update:model-value="updateSettings({ sendOnEnter: $event })"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 默认模型 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">默认模型</span>
|
||
<span class="setting-desc">新对话使用的默认 AI 模型</span>
|
||
</div>
|
||
<FormSelect
|
||
:model-value="defaultModel"
|
||
:options="modelOptions"
|
||
@update:model-value="
|
||
defaultModel = $event as string;
|
||
updateLocalStorage('defaultModel', $event as string);
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 温度 -->
|
||
<div class="setting-item vertical">
|
||
<div class="setting-info">
|
||
<span class="setting-label">默认温度</span>
|
||
<span class="setting-desc"
|
||
>控制回复的随机性(0 = 精确,1 = 创造性)</span
|
||
>
|
||
</div>
|
||
<div class="slider-wrapper">
|
||
<FormSlider
|
||
:model-value="settings.defaultTemperature"
|
||
:min="0"
|
||
:max="1"
|
||
:step="0.1"
|
||
@update:model-value="
|
||
updateSettings({ defaultTemperature: $event })
|
||
"
|
||
/>
|
||
<span class="slider-value">{{
|
||
settings.defaultTemperature.toFixed(1)
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 最大 Token -->
|
||
<div class="setting-item vertical">
|
||
<div class="setting-info">
|
||
<span class="setting-label">最大回复长度</span>
|
||
<span class="setting-desc">单次回复的最大 Token 数量</span>
|
||
</div>
|
||
<div class="slider-wrapper">
|
||
<FormSlider
|
||
:model-value="settings.defaultMaxTokens"
|
||
:min="256"
|
||
:max="8192"
|
||
:step="256"
|
||
@update:model-value="
|
||
updateSettings({ defaultMaxTokens: $event })
|
||
"
|
||
/>
|
||
<span class="slider-value">{{
|
||
settings.defaultMaxTokens
|
||
}}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 系统提示词 -->
|
||
<div class="setting-item vertical">
|
||
<div class="setting-info">
|
||
<span class="setting-label">默认系统提示词</span>
|
||
<span class="setting-desc">设定 AI 助手的角色和行为</span>
|
||
</div>
|
||
<textarea
|
||
class="prompt-textarea"
|
||
:value="settings.defaultSystemPrompt"
|
||
rows="4"
|
||
placeholder="输入系统提示词..."
|
||
@input="
|
||
updateSettings({
|
||
defaultSystemPrompt: (
|
||
$event.target as HTMLTextAreaElement
|
||
).value,
|
||
})
|
||
"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 功能设置 -->
|
||
<div v-show="activeTab === 'features'" class="settings-section">
|
||
<div class="section-title">功能</div>
|
||
|
||
<!-- 声音 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">声音提示</span>
|
||
<span class="setting-desc">收到消息时播放提示音</span>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.enableSound"
|
||
@update:model-value="updateSettings({ enableSound: $event })"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 通知 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">桌面通知</span>
|
||
<span class="setting-desc">在后台时显示桌面通知</span>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.enableNotification"
|
||
@update:model-value="
|
||
updateSettings({ enableNotification: $event })
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 自动保存 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">自动保存间隔</span>
|
||
<span class="setting-desc">对话自动保存的时间间隔(秒)</span>
|
||
</div>
|
||
<FormSelect
|
||
:model-value="settings.autoSaveInterval"
|
||
:options="autoSaveOptions"
|
||
@update:model-value="
|
||
updateSettings({ autoSaveInterval: $event as number })
|
||
"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 隐私设置 -->
|
||
<div v-show="activeTab === 'privacy'" class="settings-section">
|
||
<div class="section-title">隐私</div>
|
||
|
||
<!-- 保存历史 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">保存对话历史</span>
|
||
<span class="setting-desc">在本地存储对话记录</span>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.saveHistory"
|
||
@update:model-value="updateSettings({ saveHistory: $event })"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 分析数据 -->
|
||
<div class="setting-item">
|
||
<div class="setting-info">
|
||
<span class="setting-label">分享使用数据</span>
|
||
<span class="setting-desc">帮助我们改进产品(匿名)</span>
|
||
</div>
|
||
<FormSwitch
|
||
:model-value="settings.shareAnalytics"
|
||
@update:model-value="
|
||
updateSettings({ shareAnalytics: $event })
|
||
"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 数据管理 -->
|
||
<div class="section-title" style="margin-top: 24px">数据管理</div>
|
||
|
||
<div class="data-actions">
|
||
<button class="data-btn" @click="handleExportSettings">
|
||
<Download :size="18" />
|
||
<span>导出设置</span>
|
||
</button>
|
||
<button class="data-btn" @click="handleImportSettings">
|
||
<Upload :size="18" />
|
||
<span>导入设置</span>
|
||
</button>
|
||
<button class="data-btn danger" @click="handleClearData">
|
||
<Trash2 :size="18" />
|
||
<span>清除所有数据</span>
|
||
</button>
|
||
</div>
|
||
|
||
<input
|
||
ref="importInputRef"
|
||
type="file"
|
||
accept=".json"
|
||
hidden
|
||
@change="handleImportFile"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 关于 -->
|
||
<div v-show="activeTab === 'about'" class="settings-section">
|
||
<div class="section-title">关于</div>
|
||
|
||
<div class="about-content">
|
||
<div class="app-info">
|
||
<div class="app-logo">
|
||
<Bot :size="40" />
|
||
</div>
|
||
<h4>Kexue AI Chat</h4>
|
||
<p class="version">版本 1.0.0</p>
|
||
<p class="desc">企业级 AI 对话聊天界面</p>
|
||
</div>
|
||
|
||
<div class="about-links">
|
||
<a href="#" class="about-link">
|
||
<FileText :size="18" />
|
||
<span>使用文档</span>
|
||
<ExternalLink :size="14" />
|
||
</a>
|
||
<a href="#" class="about-link">
|
||
<MessageSquare :size="18" />
|
||
<span>反馈建议</span>
|
||
<ExternalLink :size="14" />
|
||
</a>
|
||
<a href="#" class="about-link">
|
||
<Shield :size="18" />
|
||
<span>隐私政策</span>
|
||
<ExternalLink :size="14" />
|
||
</a>
|
||
</div>
|
||
|
||
<div class="copyright">
|
||
© 2024 Your Company. All rights reserved.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部 -->
|
||
<div class="modal-footer">
|
||
<button class="reset-btn" @click="handleResetSettings">
|
||
<RotateCcw :size="16" />
|
||
<span>重置为默认</span>
|
||
</button>
|
||
<button class="done-btn" @click="close">完成</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Transition>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, onMounted } from "vue";
|
||
import { storeToRefs } from "pinia";
|
||
import { useSettingsStore } from "@/stores/settings";
|
||
import FormSwitch from "@/components/ui/FormSwitch.vue";
|
||
import FormSlider from "@/components/ui/FormSlider.vue";
|
||
import FormSelect from "@/components/ui/FormSelect.vue";
|
||
import {
|
||
Settings,
|
||
X,
|
||
Sun,
|
||
Moon,
|
||
Monitor,
|
||
Palette,
|
||
MessageSquare,
|
||
Zap,
|
||
Shield,
|
||
Info,
|
||
Download,
|
||
Upload,
|
||
Trash2,
|
||
Bot,
|
||
FileText,
|
||
ExternalLink,
|
||
RotateCcw,
|
||
} from "@/components/icons";
|
||
import type { AppSettings } from "@/types/chat";
|
||
import { chatApi } from "@/services/api.ts";
|
||
|
||
const settingsStore = useSettingsStore();
|
||
|
||
const { showSettingsModal: visible, settings } = storeToRefs(settingsStore);
|
||
const availableModels: any = ref([]);
|
||
const defaultModel: any = ref(localStorage.getItem("defaultModel"));
|
||
|
||
onMounted(() => {
|
||
// chatApi.getModels().then((res: any) => {
|
||
// availableModels.value = res;
|
||
// if (!defaultModel.value) defaultModel.value = res[0].name;
|
||
// });
|
||
});
|
||
|
||
const activeTab = ref("appearance");
|
||
const importInputRef = ref<HTMLInputElement | null>(null);
|
||
|
||
// 标签页配置
|
||
const tabs = [
|
||
{ id: "appearance", label: "外观", icon: Palette },
|
||
{ id: "chat", label: "对话", icon: MessageSquare },
|
||
{ id: "features", label: "功能", icon: Zap },
|
||
{ id: "privacy", label: "隐私", icon: Shield },
|
||
{ id: "about", label: "关于", icon: Info },
|
||
];
|
||
|
||
// 主题选项
|
||
const themeOptions = [
|
||
{ value: "light" as const, label: "浅色", icon: Sun },
|
||
{ value: "dark" as const, label: "深色", icon: Moon },
|
||
{ value: "system" as const, label: "系统", icon: Monitor },
|
||
];
|
||
|
||
// 字体大小选项
|
||
const fontSizeOptions = [
|
||
{ value: "small" as const, label: "小" },
|
||
{ value: "medium" as const, label: "中" },
|
||
{ value: "large" as const, label: "大" },
|
||
];
|
||
|
||
// 语言选项
|
||
const languageOptions = [
|
||
{ value: "zh-CN", label: "简体中文" },
|
||
{ value: "zh-TW", label: "繁體中文" },
|
||
{ value: "en-US", label: "English" },
|
||
{ value: "ja-JP", label: "日本語" },
|
||
];
|
||
|
||
// 模型选项 - 添加安全检查
|
||
const modelOptions = computed(() => {
|
||
if (!availableModels.value || !Array.isArray(availableModels.value)) {
|
||
return [{ value: "gpt-4", label: "GPT-4", description: "默认模型" }];
|
||
}
|
||
return availableModels.value?.map((model: any) => ({
|
||
value: model.id,
|
||
label: model.name,
|
||
description: model.description,
|
||
}));
|
||
});
|
||
|
||
// 自动保存选项
|
||
const autoSaveOptions = [
|
||
{ value: 10, label: "10 秒" },
|
||
{ value: 30, label: "30 秒" },
|
||
{ value: 60, label: "1 分钟" },
|
||
{ value: 300, label: "5 分钟" },
|
||
];
|
||
|
||
function updateLocalStorage(name: string, data: string) {
|
||
localStorage.setItem(name, data);
|
||
}
|
||
|
||
function close() {
|
||
settingsStore.closeSettingsModal();
|
||
}
|
||
|
||
function setTheme(theme: AppSettings["theme"]) {
|
||
settingsStore.setTheme(theme);
|
||
}
|
||
|
||
function setFontSize(size: AppSettings["fontSize"]) {
|
||
settingsStore.setFontSize(size);
|
||
}
|
||
|
||
function updateSettings(updates: Partial<AppSettings>) {
|
||
settingsStore.updateSettings(updates);
|
||
}
|
||
|
||
function handleResetSettings() {
|
||
if (confirm("确定要将所有设置重置为默认值吗?")) {
|
||
settingsStore.resetSettings();
|
||
}
|
||
}
|
||
|
||
function handleExportSettings() {
|
||
const json = settingsStore.exportSettings();
|
||
const blob = new Blob([json], { type: "application/json" });
|
||
const url = URL.createObjectURL(blob);
|
||
const a = document.createElement("a");
|
||
a.href = url;
|
||
a.download = "chat-settings.json";
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function handleImportSettings() {
|
||
importInputRef.value?.click();
|
||
}
|
||
|
||
function handleImportFile(event: Event) {
|
||
const file = (event.target as HTMLInputElement).files?.[0];
|
||
if (!file) return;
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const json = e.target?.result as string;
|
||
if (settingsStore.importSettings(json)) {
|
||
alert("设置导入成功!");
|
||
} else {
|
||
alert("设置导入失败,请检查文件格式。");
|
||
}
|
||
};
|
||
reader.readAsText(file);
|
||
}
|
||
|
||
function handleClearData() {
|
||
if (
|
||
confirm(
|
||
"确定要清除所有数据吗?这将删除所有对话历史和设置。此操作不可恢复!",
|
||
)
|
||
) {
|
||
localStorage.clear();
|
||
location.reload();
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1000;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.settings-modal {
|
||
width: 640px;
|
||
max-width: 90vw;
|
||
max-height: 85vh;
|
||
background: white;
|
||
border-radius: 20px;
|
||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
|
||
.dark & {
|
||
background: #1e1e2e;
|
||
}
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 20px 24px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
|
||
.dark & {
|
||
border-bottom-color: #2d2d3d;
|
||
}
|
||
}
|
||
|
||
.header-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
|
||
svg {
|
||
color: #3b82f6;
|
||
}
|
||
|
||
h3 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
|
||
.dark & {
|
||
color: #f3f4f6;
|
||
}
|
||
}
|
||
}
|
||
|
||
.close-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 36px;
|
||
height: 36px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: #f3f4f6;
|
||
color: #1f2937;
|
||
|
||
.dark & {
|
||
background: #374151;
|
||
color: #f3f4f6;
|
||
}
|
||
}
|
||
}
|
||
|
||
.settings-tabs {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 12px 24px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
overflow-x: auto;
|
||
|
||
.dark & {
|
||
border-bottom-color: #2d2d3d;
|
||
}
|
||
}
|
||
|
||
.tab-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 16px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
background: #f3f4f6;
|
||
color: #374151;
|
||
|
||
.dark & {
|
||
background: #2d2d3d;
|
||
color: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background: rgba(59, 130, 246, 0.1);
|
||
color: #3b82f6;
|
||
}
|
||
}
|
||
|
||
.settings-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px 24px;
|
||
}
|
||
|
||
.settings-section {
|
||
animation: fadeIn 0.2s ease;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: #9ca3af;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.setting-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 0;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
|
||
.dark & {
|
||
border-bottom-color: #2d2d3d;
|
||
}
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
&.vertical {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.setting-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
}
|
||
|
||
.setting-label {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #1f2937;
|
||
|
||
.dark & {
|
||
color: #f3f4f6;
|
||
}
|
||
}
|
||
|
||
.setting-desc {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.theme-options {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.theme-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 14px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: white;
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
.dark & {
|
||
background: #2d2d3d;
|
||
border-color: #374151;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
&:hover {
|
||
border-color: #3b82f6;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
&.active {
|
||
background: rgba(59, 130, 246, 0.1);
|
||
border-color: #3b82f6;
|
||
color: #3b82f6;
|
||
}
|
||
}
|
||
|
||
.font-size-options {
|
||
display: flex;
|
||
gap: 4px;
|
||
background: #f3f4f6;
|
||
padding: 4px;
|
||
border-radius: 10px;
|
||
|
||
.dark & {
|
||
background: #2d2d3d;
|
||
}
|
||
}
|
||
|
||
.size-btn {
|
||
padding: 6px 16px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
color: #374151;
|
||
|
||
.dark & {
|
||
color: #e5e7eb;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background: white;
|
||
color: #3b82f6;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
|
||
.dark & {
|
||
background: #374151;
|
||
}
|
||
}
|
||
}
|
||
|
||
.slider-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
|
||
.form-slider {
|
||
flex: 1;
|
||
}
|
||
}
|
||
|
||
.slider-value {
|
||
min-width: 48px;
|
||
text-align: right;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.prompt-textarea {
|
||
width: 100%;
|
||
padding: 12px 14px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: white;
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
color: #1f2937;
|
||
resize: vertical;
|
||
min-height: 100px;
|
||
|
||
.dark & {
|
||
background: #2d2d3d;
|
||
border-color: #374151;
|
||
color: #f3f4f6;
|
||
}
|
||
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #3b82f6;
|
||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: #9ca3af;
|
||
}
|
||
}
|
||
|
||
.data-actions {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 12px;
|
||
}
|
||
|
||
.data-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 12px 20px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: white;
|
||
color: #374151;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
.dark & {
|
||
background: #2d2d3d;
|
||
border-color: #374151;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
&:hover {
|
||
border-color: #3b82f6;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
&.danger {
|
||
border-color: #fecaca;
|
||
color: #ef4444;
|
||
|
||
&:hover {
|
||
background: rgba(239, 68, 68, 0.1);
|
||
border-color: #ef4444;
|
||
}
|
||
}
|
||
}
|
||
|
||
.about-content {
|
||
text-align: center;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
.app-info {
|
||
margin-bottom: 32px;
|
||
|
||
.app-logo {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 80px;
|
||
height: 80px;
|
||
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
|
||
border-radius: 20px;
|
||
color: white;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
h4 {
|
||
margin: 0 0 4px;
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
|
||
.dark & {
|
||
color: #f3f4f6;
|
||
}
|
||
}
|
||
|
||
.version {
|
||
margin: 0 0 8px;
|
||
font-size: 14px;
|
||
color: #3b82f6;
|
||
}
|
||
|
||
.desc {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
}
|
||
}
|
||
|
||
.about-links {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
max-width: 280px;
|
||
margin: 0 auto 32px;
|
||
}
|
||
|
||
.about-link {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 12px 16px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
color: #374151;
|
||
text-decoration: none;
|
||
transition: all 0.2s ease;
|
||
|
||
.dark & {
|
||
border-color: #374151;
|
||
color: #e5e7eb;
|
||
}
|
||
|
||
span {
|
||
flex: 1;
|
||
text-align: left;
|
||
}
|
||
|
||
&:hover {
|
||
border-color: #3b82f6;
|
||
color: #3b82f6;
|
||
}
|
||
}
|
||
|
||
.copyright {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 16px 24px;
|
||
border-top: 1px solid #e2e8f0;
|
||
|
||
.dark & {
|
||
border-top-color: #2d2d3d;
|
||
}
|
||
}
|
||
|
||
.reset-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 16px;
|
||
border: 1px solid #e2e8f0;
|
||
border-radius: 10px;
|
||
background: transparent;
|
||
color: #6b7280;
|
||
font-size: 14px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
.dark & {
|
||
border-color: #374151;
|
||
}
|
||
|
||
&:hover {
|
||
border-color: #f59e0b;
|
||
color: #f59e0b;
|
||
}
|
||
}
|
||
|
||
.done-btn {
|
||
padding: 10px 24px;
|
||
border: none;
|
||
border-radius: 10px;
|
||
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||
}
|
||
}
|
||
|
||
// 动画
|
||
.modal-enter-active,
|
||
.modal-leave-active {
|
||
transition: all 0.25s ease;
|
||
|
||
.settings-modal {
|
||
transition: all 0.25s ease;
|
||
}
|
||
}
|
||
|
||
.modal-enter-from,
|
||
.modal-leave-to {
|
||
opacity: 0;
|
||
|
||
.settings-modal {
|
||
transform: scale(0.9);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|