perf(code): 格式化代码以提升可读性与一致性,统一代码风格 [优化:使用 Prettier 进行格式化]

This commit is contained in:
SuperManTouX 2026-03-06 09:23:39 +08:00
parent 972d92ba1a
commit 6984a09737
34 changed files with 10043 additions and 9964 deletions

17
package-lock.json generated
View File

@ -32,6 +32,7 @@
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.24",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.8.1",
"sass": "^1.97.3", "sass": "^1.97.3",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "~5.9.3", "typescript": "~5.9.3",
@ -4363,6 +4364,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/prettier": {
"version": "3.8.1",
"resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.1.tgz",
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/property-information": { "node_modules/property-information": {
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz", "resolved": "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz",

View File

@ -33,6 +33,7 @@
"@vue/tsconfig": "^0.8.1", "@vue/tsconfig": "^0.8.1",
"autoprefixer": "^10.4.24", "autoprefixer": "^10.4.24",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.8.1",
"sass": "^1.97.3", "sass": "^1.97.3",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"typescript": "~5.9.3", "typescript": "~5.9.3",

View File

@ -1,13 +1,10 @@
<template> <template>
<div class="app" :class="{ 'dark': isDark }"> <div class="app" :class="{ dark: isDark }">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<ChatSidebar /> <ChatSidebar />
<!-- 主内容区 --> <!-- 主内容区 -->
<ChatMain <ChatMain ref="chatMainRef" @toggle-sidebar="toggleSidebar" />
ref="chatMainRef"
@toggle-sidebar="toggleSidebar"
/>
<!-- 模态框 --> <!-- 模态框 -->
<SearchModal /> <SearchModal />
@ -35,70 +32,70 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from "vue";
import { storeToRefs } from 'pinia' import { storeToRefs } from "pinia";
import { useChatStore } from '@/stores/chat' import { useChatStore } from "@/stores/chat";
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from "@/stores/settings";
import { useKeyboard, getDefaultShortcuts } from '@/composables/useKeyboard' import { useKeyboard, getDefaultShortcuts } from "@/composables/useKeyboard";
import ChatSidebar from '@/components/sidebar/ChatSidebar.vue' import ChatSidebar from "@/components/sidebar/ChatSidebar.vue";
import ChatMain from '@/components/chat/ChatMain.vue' import ChatMain from "@/components/chat/ChatMain.vue";
import SearchModal from '@/components/modals/SearchModal.vue' import SearchModal from "@/components/modals/SearchModal.vue";
import ShortcutsModal from '@/components/modals/ShortcutsModal.vue' import ShortcutsModal from "@/components/modals/ShortcutsModal.vue";
import SettingsModal from '@/components/modals/SettingsModal.vue' import SettingsModal from "@/components/modals/SettingsModal.vue";
import ConversationSettingsModal from '@/components/modals/ConversationSettingsModal.vue' import ConversationSettingsModal from "@/components/modals/ConversationSettingsModal.vue";
import { Check, AlertCircle, Info } from '@/components/icons' import { Check, AlertCircle, Info } from "@/components/icons";
// Stores // Stores
const chatStore = useChatStore() const chatStore = useChatStore();
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore();
const { settings } = storeToRefs(settingsStore) const { settings } = storeToRefs(settingsStore);
// Refs // Refs
const chatMainRef = ref<InstanceType<typeof ChatMain> | null>(null) const chatMainRef = ref<InstanceType<typeof ChatMain> | null>(null);
// //
const isDark = computed(() => { const isDark = computed(() => {
if (settings.value.theme === 'system') { if (settings.value.theme === "system") {
return window.matchMedia('(prefers-color-scheme: dark)').matches return window.matchMedia("(prefers-color-scheme: dark)").matches;
} }
return settings.value.theme === 'dark' return settings.value.theme === "dark";
}) });
// Toast // Toast
interface Toast { interface Toast {
id: number id: number;
message: string message: string;
type: 'success' | 'error' | 'info' type: "success" | "error" | "info";
} }
const toasts = ref<Toast[]>([]) const toasts = ref<Toast[]>([]);
let toastId = 0 let toastId = 0;
function showToast(message: string, type: Toast['type'] = 'info') { function showToast(message: string, type: Toast["type"] = "info") {
const id = ++toastId const id = ++toastId;
toasts.value.push({ id, message, type }) toasts.value.push({ id, message, type });
setTimeout(() => { setTimeout(() => {
const index = toasts.value.findIndex(t => t.id === id) const index = toasts.value.findIndex((t) => t.id === id);
if (index !== -1) { if (index !== -1) {
toasts.value.splice(index, 1) toasts.value.splice(index, 1);
} }
}, 3000) }, 3000);
} }
// //
function toggleSidebar() { function toggleSidebar() {
settingsStore.toggleSidebar() settingsStore.toggleSidebar();
} }
function newChat() { function newChat() {
chatStore.createConversation() chatStore.createConversation();
showToast('已创建新对话', 'success') showToast("已创建新对话", "success");
} }
function focusInput() { function focusInput() {
chatMainRef.value?.focusInput() chatMainRef.value?.focusInput();
} }
// //
@ -110,33 +107,33 @@ useKeyboard(
sendMessage: () => {}, // ChatInput sendMessage: () => {}, // ChatInput
cancelStream: () => { cancelStream: () => {
if (chatStore.isStreaming) { if (chatStore.isStreaming) {
chatStore.stopStreaming() chatStore.stopStreaming();
showToast('已停止生成', 'info') showToast("已停止生成", "info");
} }
}, },
toggleTheme: () => { toggleTheme: () => {
settingsStore.toggleTheme() settingsStore.toggleTheme();
showToast(`主题已切换为 ${settings.value.theme}`, 'success') showToast(`主题已切换为 ${settings.value.theme}`, "success");
}, },
showShortcuts: () => { showShortcuts: () => {
settingsStore.openShortcutsModal() settingsStore.openShortcutsModal();
}, },
searchConversations: () => { searchConversations: () => {
settingsStore.openSearchModal() settingsStore.openSearchModal();
}, },
}) }),
) );
// //
onMounted(() => { onMounted(() => {
// //
if (chatStore.conversations.length === 0) { if (chatStore.conversations.length === 0) {
chatStore.createConversation() chatStore.createConversation();
} }
}) });
// 使 // 使
window.$toast = showToast window.$toast = showToast;
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -59,7 +59,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from "vue";
import { import {
Bot, Bot,
MessageSquare, MessageSquare,
@ -72,57 +72,57 @@ import {
Globe, Globe,
Lightbulb, Lightbulb,
PenTool, PenTool,
} from '@/components/icons' } from "@/components/icons";
defineEmits<{ defineEmits<{
select: [text: string] select: [text: string];
}>() }>();
const features = computed(() => [ const features = computed(() => [
{ {
icon: MessageSquare, icon: MessageSquare,
title: '智能对话', title: "智能对话",
description: '自然流畅的对话体验,理解上下文', description: "自然流畅的对话体验,理解上下文",
gradient: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', gradient: "linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)",
}, },
{ {
icon: Code, icon: Code,
title: '代码助手', title: "代码助手",
description: '编写、解释、优化各种编程语言代码', description: "编写、解释、优化各种编程语言代码",
gradient: 'linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)', gradient: "linear-gradient(135deg, #8b5cf6 0%, #6366f1 100%)",
}, },
{ {
icon: Image, icon: Image,
title: '图像理解', title: "图像理解",
description: '分析图片内容,提取关键信息', description: "分析图片内容,提取关键信息",
gradient: 'linear-gradient(135deg, #ec4899 0%, #d946ef 100%)', gradient: "linear-gradient(135deg, #ec4899 0%, #d946ef 100%)",
}, },
{ {
icon: FileText, icon: FileText,
title: '文档处理', title: "文档处理",
description: '阅读、总结、翻译各类文档', description: "阅读、总结、翻译各类文档",
gradient: 'linear-gradient(135deg, #f59e0b 0%, #f97316 100%)', gradient: "linear-gradient(135deg, #f59e0b 0%, #f97316 100%)",
}, },
]) ]);
const suggestions = computed(() => [ const suggestions = computed(() => [
{ {
icon: Lightbulb, icon: Lightbulb,
text: '帮我写一个 Vue 3 组件示例', text: "帮我写一个 Vue 3 组件示例",
}, },
{ {
icon: Globe, icon: Globe,
text: '解释一下什么是机器学习', text: "解释一下什么是机器学习",
}, },
{ {
icon: PenTool, icon: PenTool,
text: '帮我写一封商务邮件', text: "帮我写一封商务邮件",
}, },
{ {
icon: Code, icon: Code,
text: '如何优化 React 应用性能', text: "如何优化 React 应用性能",
}, },
]) ]);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -162,7 +162,11 @@ const suggestions = computed(() => [
.logo-glow { .logo-glow {
position: absolute; position: absolute;
inset: -20px; inset: -20px;
background: radial-gradient(circle, rgba(59, 130, 246, 0.2) 0%, transparent 70%); background: radial-gradient(
circle,
rgba(59, 130, 246, 0.2) 0%,
transparent 70%
);
pointer-events: none; pointer-events: none;
} }

View File

@ -9,7 +9,11 @@
> >
<!-- 图片预览 --> <!-- 图片预览 -->
<template v-if="attachment.type === 'image'"> <template v-if="attachment.type === 'image'">
<img :src="attachment.url" :alt="attachment.name" class="preview-image" /> <img
:src="attachment.url"
:alt="attachment.name"
class="preview-image"
/>
</template> </template>
<!-- 视频预览 --> <!-- 视频预览 -->
@ -32,7 +36,9 @@
<!-- 文件预览 --> <!-- 文件预览 -->
<template v-else> <template v-else>
<div class="preview-file"> <div class="preview-file">
<span class="file-emoji">{{ getFileEmoji(attachment.mimeType) }}</span> <span class="file-emoji">{{
getFileEmoji(attachment.mimeType)
}}</span>
<div class="file-details"> <div class="file-details">
<span class="file-name">{{ truncateName(attachment.name) }}</span> <span class="file-name">{{ truncateName(attachment.name) }}</span>
<span class="file-size">{{ formatSize(attachment.size) }}</span> <span class="file-size">{{ formatSize(attachment.size) }}</span>
@ -41,10 +47,7 @@
</template> </template>
<!-- 删除按钮 --> <!-- 删除按钮 -->
<button <button class="remove-btn" @click="$emit('remove', attachment.id)">
class="remove-btn"
@click="$emit('remove', attachment.id)"
>
<X :size="14" /> <X :size="14" />
</button> </button>
@ -61,39 +64,39 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { X, Video, Play } from '@/components/icons' import { X, Video, Play } from "@/components/icons";
import { formatFileSize, getFileIcon, truncateText } from '@/utils/helpers' import { formatFileSize, getFileIcon, truncateText } from "@/utils/helpers";
interface AttachmentWithProgress { interface AttachmentWithProgress {
id: string id: string;
name: string name: string;
type: 'image' | 'file' | 'video' type: "image" | "file" | "video";
url: string url: string;
size?: number size?: number;
mimeType?: string mimeType?: string;
thumbnail?: string thumbnail?: string;
uploading?: boolean uploading?: boolean;
progress?: number progress?: number;
} }
defineProps<{ defineProps<{
attachments: AttachmentWithProgress[] attachments: AttachmentWithProgress[];
}>() }>();
defineEmits<{ defineEmits<{
remove: [id: string] remove: [id: string];
}>() }>();
function getFileEmoji(mimeType?: string) { function getFileEmoji(mimeType?: string) {
return getFileIcon(mimeType || '') return getFileIcon(mimeType || "");
} }
function formatSize(size?: number) { function formatSize(size?: number) {
return size ? formatFileSize(size) : '' return size ? formatFileSize(size) : "";
} }
function truncateName(name: string) { function truncateName(name: string) {
return truncateText(name, 20) return truncateText(name, 20);
} }
</script> </script>

View File

@ -211,9 +211,7 @@ const imageInputRef = ref<HTMLInputElement | null>(null);
// //
const charCount = computed(() => inputText.value.length); const charCount = computed(() => inputText.value.length);
const isUploading = computed(() => const isUploading = computed(() => attachments.value.some((a) => a.uploading));
attachments.value.some((a) => a.uploading),
);
const canSend = computed(() => { const canSend = computed(() => {
return ( return (
(inputText.value.trim().length > 0 || attachments.value.length > 0) && (inputText.value.trim().length > 0 || attachments.value.length > 0) &&
@ -363,9 +361,9 @@ async function uploadFileToServer(id: string, file: File) {
} }
// //
window.$toast && window.$toast('文件上传成功', 'success'); window.$toast && window.$toast("文件上传成功", "success");
} catch (error) { } catch (error) {
console.error('文件上传失败:', error); console.error("文件上传失败:", error);
const attachment = attachments.value.find((a) => a.id === id); const attachment = attachments.value.find((a) => a.id === id);
if (attachment) { if (attachment) {
@ -375,7 +373,7 @@ async function uploadFileToServer(id: string, file: File) {
} }
// //
window.$toast && window.$toast('文件上传失败,请重试', 'error'); window.$toast && window.$toast("文件上传失败,请重试", "error");
} }
} }

View File

@ -4,7 +4,7 @@
<div class="code-header"> <div class="code-header">
<div class="code-language"> <div class="code-language">
<Code :size="14" /> <Code :size="14" />
<span>{{ language || 'code' }}</span> <span>{{ language || "code" }}</span>
</div> </div>
<div class="code-actions"> <div class="code-actions">
<button <button
@ -42,49 +42,52 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from "vue";
import { Code, Copy, Check, Maximize2, Minimize2 } from '@/components/icons' import { Code, Copy, Check, Maximize2, Minimize2 } from "@/components/icons";
import { copyToClipboard } from '@/utils/helpers' import { copyToClipboard } from "@/utils/helpers";
const props = withDefaults(defineProps<{ const props = withDefaults(
code: string defineProps<{
language?: string code: string;
showLineNumbers?: boolean language?: string;
maxHeight?: number showLineNumbers?: boolean;
}>(), { maxHeight?: number;
language: 'plaintext', }>(),
{
language: "plaintext",
showLineNumbers: true, showLineNumbers: true,
maxHeight: 400, maxHeight: 400,
}) },
);
const emit = defineEmits<{ const emit = defineEmits<{
copy: [] copy: [];
}>() }>();
const isCopied = ref(false) const isCopied = ref(false);
const isExpanded = ref(false) const isExpanded = ref(false);
const lineCount = computed(() => { const lineCount = computed(() => {
return props.code.split('\n').length return props.code.split("\n").length;
}) });
const canExpand = computed(() => { const canExpand = computed(() => {
return lineCount.value > 15 return lineCount.value > 15;
}) });
async function handleCopy() { async function handleCopy() {
const success = await copyToClipboard(props.code) const success = await copyToClipboard(props.code);
if (success) { if (success) {
isCopied.value = true isCopied.value = true;
emit('copy') emit("copy");
setTimeout(() => { setTimeout(() => {
isCopied.value = false isCopied.value = false;
}, 2000) }, 2000);
} }
} }
function toggleExpand() { function toggleExpand() {
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value;
} }
</script> </script>
@ -166,7 +169,7 @@ function toggleExpand() {
overflow-x: auto; overflow-x: auto;
code { code {
font-family: 'JetBrains Mono', 'Fira Code', 'Monaco', monospace; font-family: "JetBrains Mono", "Fira Code", "Monaco", monospace;
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
color: #cdd6f4; color: #cdd6f4;
@ -192,7 +195,7 @@ function toggleExpand() {
pointer-events: none; pointer-events: none;
span { span {
font-family: 'JetBrains Mono', monospace; font-family: "JetBrains Mono", monospace;
font-size: 12px; font-size: 12px;
line-height: 1.6; line-height: 1.6;
color: #585b70; color: #585b70;
@ -200,13 +203,30 @@ function toggleExpand() {
} }
:deep(.code-content) { :deep(.code-content) {
.keyword { color: #cba6f7; } .keyword {
.string { color: #a6e3a1; } color: #cba6f7;
.number { color: #fab387; } }
.comment { color: #6c7086; font-style: italic; } .string {
.function { color: #89b4fa; } color: #a6e3a1;
.operator { color: #89dceb; } }
.punctuation { color: #9399b2; } .number {
.class-name { color: #f9e2af; } color: #fab387;
}
.comment {
color: #6c7086;
font-style: italic;
}
.function {
color: #89b4fa;
}
.operator {
color: #89dceb;
}
.punctuation {
color: #9399b2;
}
.class-name {
color: #f9e2af;
}
} }
</style> </style>

View File

@ -67,12 +67,19 @@ async function textCopy(data: any) {
<div class="thinking-title"> <div class="thinking-title">
<strong class="text-sm">💭 深度思考</strong> <strong class="text-sm">💭 深度思考</strong>
<!-- 加载动画 --> <!-- 加载动画 -->
<span v-if="node.loading" class="thinking-dots visible" aria-hidden="true"> <span
v-if="node.loading"
class="thinking-dots visible"
aria-hidden="true"
>
<span class="dot dot-1" /> <span class="dot dot-1" />
<span class="dot dot-2" /> <span class="dot dot-2" />
<span class="dot dot-3" /> <span class="dot dot-3" />
</span> </span>
<span v-else class="thinking-status text-xs text-slate-500 dark:text-slate-300"> <span
v-else
class="thinking-status text-xs text-slate-500 dark:text-slate-300"
>
已完成 已完成
</span> </span>
</div> </div>
@ -157,7 +164,9 @@ async function textCopy(data: any) {
.thinking-content { .thinking-content {
max-height: 2000px; max-height: 2000px;
overflow: hidden; overflow: hidden;
transition: max-height 0.35s ease, opacity 0.25s ease; transition:
max-height 0.35s ease,
opacity 0.25s ease;
opacity: 1; opacity: 1;
} }
.thinking-content.collapsed { .thinking-content.collapsed {

View File

@ -218,7 +218,9 @@ onMounted(() => {
chatApi.getModels().then((res: any) => { chatApi.getModels().then((res: any) => {
availableModels.value = res; availableModels.value = res;
// //
const model = availableModels.value?.find((m: any) => m.id === currentModelId.value); const model = availableModels.value?.find(
(m: any) => m.id === currentModelId.value,
);
if (model) { if (model) {
modelSelect.value = model.name; modelSelect.value = model.name;
} else if (availableModels.value.length > 0) { } else if (availableModels.value.length > 0) {

View File

@ -70,87 +70,89 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, nextTick } from 'vue' import { ref, computed, watch, nextTick } from "vue";
import { storeToRefs } from 'pinia' import { storeToRefs } from "pinia";
import { useChatStore } from '@/stores/chat' import { useChatStore } from "@/stores/chat";
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from "@/stores/settings";
import { Search, MessageSquare, FolderOpen, Pin } from '@/components/icons' import { Search, MessageSquare, FolderOpen, Pin } from "@/components/icons";
import { formatTimestamp } from '@/utils/helpers' import { formatTimestamp } from "@/utils/helpers";
const chatStore = useChatStore() const chatStore = useChatStore();
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore();
const { conversations } = storeToRefs(chatStore) const { conversations } = storeToRefs(chatStore);
const { showSearchModal: visible } = storeToRefs(settingsStore) const { showSearchModal: visible } = storeToRefs(settingsStore);
const searchQuery = ref('') const searchQuery = ref("");
const selectedIndex = ref(0) const selectedIndex = ref(0);
const inputRef = ref<HTMLInputElement | null>(null) const inputRef = ref<HTMLInputElement | null>(null);
const filteredConversations = computed(() => { const filteredConversations = computed(() => {
if (!searchQuery.value.trim()) { if (!searchQuery.value.trim()) {
return conversations.value.slice(0, 10) return conversations.value.slice(0, 10);
} }
const query = searchQuery.value.toLowerCase() const query = searchQuery.value.toLowerCase();
return conversations.value.filter(conv => { return conversations.value
.filter((conv) => {
// //
if (conv.title.toLowerCase().includes(query)) return true if (conv.title.toLowerCase().includes(query)) return true;
// //
return conv.messages.some(msg => return conv.messages.some((msg) =>
msg.content.text?.toLowerCase().includes(query) msg.content.text?.toLowerCase().includes(query),
) );
}).slice(0, 10)
}) })
.slice(0, 10);
});
function formatTime(timestamp: number) { function formatTime(timestamp: number) {
return formatTimestamp(timestamp) return formatTimestamp(timestamp);
} }
function close() { function close() {
settingsStore.closeSearchModal() settingsStore.closeSearchModal();
searchQuery.value = '' searchQuery.value = "";
selectedIndex.value = 0 selectedIndex.value = 0;
} }
function navigateDown() { function navigateDown() {
if (selectedIndex.value < filteredConversations.value.length - 1) { if (selectedIndex.value < filteredConversations.value.length - 1) {
selectedIndex.value++ selectedIndex.value++;
} }
} }
function navigateUp() { function navigateUp() {
if (selectedIndex.value > 0) { if (selectedIndex.value > 0) {
selectedIndex.value-- selectedIndex.value--;
} }
} }
function selectCurrent() { function selectCurrent() {
const conv = filteredConversations.value[selectedIndex.value] const conv = filteredConversations.value[selectedIndex.value];
if (conv) { if (conv) {
selectConversation(conv.id) selectConversation(conv.id);
} }
} }
function selectConversation(id: string) { function selectConversation(id: string) {
chatStore.selectConversation(id) chatStore.selectConversation(id);
close() close();
} }
// //
watch(visible, (val) => { watch(visible, (val) => {
if (val) { if (val) {
nextTick(() => { nextTick(() => {
inputRef.value?.focus() inputRef.value?.focus();
}) });
} }
}) });
// //
watch(searchQuery, () => { watch(searchQuery, () => {
selectedIndex.value = 0 selectedIndex.value = 0;
}) });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -39,7 +39,9 @@
<!-- 底部 --> <!-- 底部 -->
<div class="modal-footer"> <div class="modal-footer">
<span class="tip"> <kbd>ESC</kbd> <kbd>?</kbd> 关闭此窗口</span> <span class="tip"
> <kbd>ESC</kbd> <kbd>?</kbd> 关闭此窗口</span
>
</div> </div>
</div> </div>
</div> </div>
@ -48,45 +50,45 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from "vue";
import { storeToRefs } from 'pinia' import { storeToRefs } from "pinia";
import { useSettingsStore } from '@/stores/settings' import { useSettingsStore } from "@/stores/settings";
import { Keyboard, X } from '@/components/icons' import { Keyboard, X } from "@/components/icons";
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore();
const { showShortcutsModal: visible } = storeToRefs(settingsStore) const { showShortcutsModal: visible } = storeToRefs(settingsStore);
const shortcutGroups = computed(() => [ const shortcutGroups = computed(() => [
{ {
title: '通用', title: "通用",
shortcuts: [ shortcuts: [
{ description: '新建对话', keys: ['⌘', 'N'] }, { description: "新建对话", keys: ["⌘", "N"] },
{ description: '搜索对话', keys: ['⌘', 'K'] }, { description: "搜索对话", keys: ["⌘", "K"] },
{ description: '切换侧边栏', keys: ['⌘', 'B'] }, { description: "切换侧边栏", keys: ["⌘", "B"] },
{ description: '切换主题', keys: ['⌘', '⇧', 'D'] }, { description: "切换主题", keys: ["⌘", "⇧", "D"] },
{ description: '显示快捷键', keys: ['⌘', '?'] }, { description: "显示快捷键", keys: ["⌘", "?"] },
], ],
}, },
{ {
title: '对话', title: "对话",
shortcuts: [ shortcuts: [
{ description: '发送消息', keys: ['⌘', '↵'] }, { description: "发送消息", keys: ["⌘", "↵"] },
{ description: '换行', keys: ['⇧', '↵'] }, { description: "换行", keys: ["⇧", "↵"] },
{ description: '聚焦输入框', keys: ['⌘', '/'] }, { description: "聚焦输入框", keys: ["⌘", "/"] },
{ description: '停止生成', keys: ['ESC'] }, { description: "停止生成", keys: ["ESC"] },
], ],
}, },
{ {
title: '消息操作', title: "消息操作",
shortcuts: [ shortcuts: [
{ description: '复制消息', keys: ['⌘', 'C'] }, { description: "复制消息", keys: ["⌘", "C"] },
{ description: '重新生成', keys: ['⌘', 'R'] }, { description: "重新生成", keys: ["⌘", "R"] },
], ],
}, },
]) ]);
function close() { function close() {
settingsStore.closeShortcutsModal() settingsStore.closeShortcutsModal();
} }
</script> </script>

View File

@ -9,7 +9,7 @@
<div class="sidebar-header"> <div class="sidebar-header">
<div class="logo"> <div class="logo">
<Bot :size="24" class="logo-icon" /> <Bot :size="24" class="logo-icon" />
<span v-show="!isCollapsed" class="logo-text">Kexue AI Chat</span> <span v-show="!isCollapsed" class="logo-text">AI Chat</span>
</div> </div>
<button <button
class="collapse-btn" class="collapse-btn"

View File

@ -2,8 +2,8 @@
<div <div
class="conversation-item group" class="conversation-item group"
:class="{ :class="{
'active': isActive, active: isActive,
'pinned': conversation.pinned pinned: conversation.pinned,
}" }"
@click="handleSelect" @click="handleSelect"
@dblclick="handleRename" @dblclick="handleRename"
@ -49,18 +49,10 @@
<PinOff v-if="conversation.pinned" :size="14" /> <PinOff v-if="conversation.pinned" :size="14" />
<Pin v-else :size="14" /> <Pin v-else :size="14" />
</button> </button>
<button <button class="action-btn" title="重命名" @click="handleRename">
class="action-btn"
title="重命名"
@click="handleRename"
>
<Edit3 :size="14" /> <Edit3 :size="14" />
</button> </button>
<button <button class="action-btn delete" title="删除" @click="handleDelete">
class="action-btn delete"
title="删除"
@click="handleDelete"
>
<Trash2 :size="14" /> <Trash2 :size="14" />
</button> </button>
</div> </div>
@ -68,65 +60,72 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, nextTick } from 'vue' import { ref, computed, nextTick } from "vue";
import { MessageSquare, Pin, PinOff, Edit3, Trash2, Clock } from '@/components/icons' import {
import { formatTimestamp } from '@/utils/helpers' MessageSquare,
import type { Conversation } from '@/types/chat' Pin,
PinOff,
Edit3,
Trash2,
Clock,
} from "@/components/icons";
import { formatTimestamp } from "@/utils/helpers";
import type { Conversation } from "@/types/chat";
const props = defineProps<{ const props = defineProps<{
conversation: Conversation conversation: Conversation;
isActive: boolean isActive: boolean;
}>() }>();
const emit = defineEmits<{ const emit = defineEmits<{
select: [id: string] select: [id: string];
delete: [id: string] delete: [id: string];
rename: [id: string, title: string] rename: [id: string, title: string];
togglePin: [id: string] togglePin: [id: string];
}>() }>();
const isEditing = ref(false) const isEditing = ref(false);
const editTitle = ref('') const editTitle = ref("");
const inputRef = ref<HTMLInputElement | null>(null) const inputRef = ref<HTMLInputElement | null>(null);
const formattedTime = computed(() => { const formattedTime = computed(() => {
return formatTimestamp(props.conversation.updatedAt) return formatTimestamp(props.conversation.updatedAt);
}) });
function handleSelect() { function handleSelect() {
if (!isEditing.value) { if (!isEditing.value) {
emit('select', props.conversation.id) emit("select", props.conversation.id);
} }
} }
function handleTogglePin() { function handleTogglePin() {
emit('togglePin', props.conversation.id) emit("togglePin", props.conversation.id);
} }
function handleRename() { function handleRename() {
isEditing.value = true isEditing.value = true;
editTitle.value = props.conversation.title editTitle.value = props.conversation.title;
nextTick(() => { nextTick(() => {
inputRef.value?.focus() inputRef.value?.focus();
inputRef.value?.select() inputRef.value?.select();
}) });
} }
function handleSaveRename() { function handleSaveRename() {
if (editTitle.value.trim()) { if (editTitle.value.trim()) {
emit('rename', props.conversation.id, editTitle.value.trim()) emit("rename", props.conversation.id, editTitle.value.trim());
} }
isEditing.value = false isEditing.value = false;
} }
function handleCancelRename() { function handleCancelRename() {
isEditing.value = false isEditing.value = false;
editTitle.value = '' editTitle.value = "";
} }
function handleDelete() { function handleDelete() {
if (confirm('确定要删除这个对话吗?')) { if (confirm("确定要删除这个对话吗?")) {
emit('delete', props.conversation.id) emit("delete", props.conversation.id);
} }
} }
</script> </script>

View File

@ -16,32 +16,35 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from "vue";
const props = withDefaults(defineProps<{ const props = withDefaults(
modelValue: number defineProps<{
min?: number modelValue: number;
max?: number min?: number;
step?: number max?: number;
disabled?: boolean step?: number;
}>(), { disabled?: boolean;
}>(),
{
min: 0, min: 0,
max: 100, max: 100,
step: 1, step: 1,
disabled: false, disabled: false,
}) },
);
const emit = defineEmits<{ const emit = defineEmits<{
'update:modelValue': [value: number] "update:modelValue": [value: number];
}>() }>();
const fillPercent = computed(() => { const fillPercent = computed(() => {
return ((props.modelValue - props.min) / (props.max - props.min)) * 100 return ((props.modelValue - props.min) / (props.max - props.min)) * 100;
}) });
function handleInput(event: Event) { function handleInput(event: Event) {
const value = parseFloat((event.target as HTMLInputElement).value) const value = parseFloat((event.target as HTMLInputElement).value);
emit('update:modelValue', value) emit("update:modelValue", value);
} }
</script> </script>

View File

@ -4,7 +4,9 @@
type="checkbox" type="checkbox"
:checked="modelValue" :checked="modelValue"
:disabled="disabled" :disabled="disabled"
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)" @change="
$emit('update:modelValue', ($event.target as HTMLInputElement).checked)
"
/> />
<span class="switch-slider"></span> <span class="switch-slider"></span>
</label> </label>
@ -12,13 +14,13 @@
<script setup lang="ts"> <script setup lang="ts">
defineProps<{ defineProps<{
modelValue: boolean modelValue: boolean;
disabled?: boolean disabled?: boolean;
}>() }>();
defineEmits<{ defineEmits<{
'update:modelValue': [value: boolean] "update:modelValue": [value: boolean];
}>() }>();
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -65,7 +67,7 @@ defineEmits<{
} }
&::before { &::before {
content: ''; content: "";
position: absolute; position: absolute;
width: 20px; width: 20px;
height: 20px; height: 20px;

View File

@ -1,128 +1,133 @@
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from "vue";
export interface KeyboardShortcut { export interface KeyboardShortcut {
key: string key: string;
ctrl?: boolean ctrl?: boolean;
shift?: boolean shift?: boolean;
alt?: boolean alt?: boolean;
meta?: boolean meta?: boolean;
description: string description: string;
action: () => void action: () => void;
} }
// 快捷键管理组合式函数 // 快捷键管理组合式函数
export function useKeyboard(shortcuts: KeyboardShortcut[]) { export function useKeyboard(shortcuts: KeyboardShortcut[]) {
const activeKeys = ref<Set<string>>(new Set()) const activeKeys = ref<Set<string>>(new Set());
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
activeKeys.value.add(event.key.toLowerCase()) activeKeys.value.add(event.key.toLowerCase());
for (const shortcut of shortcuts) { for (const shortcut of shortcuts) {
const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase() const keyMatch = event.key.toLowerCase() === shortcut.key.toLowerCase();
const ctrlMatch = !!shortcut.ctrl === (event.ctrlKey || event.metaKey) const ctrlMatch = !!shortcut.ctrl === (event.ctrlKey || event.metaKey);
const shiftMatch = !!shortcut.shift === event.shiftKey const shiftMatch = !!shortcut.shift === event.shiftKey;
const altMatch = !!shortcut.alt === event.altKey const altMatch = !!shortcut.alt === event.altKey;
if (keyMatch && ctrlMatch && shiftMatch && altMatch) { if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
// 排除在输入框中的部分快捷键 // 排除在输入框中的部分快捷键
const target = event.target as HTMLElement const target = event.target as HTMLElement;
const isInput = target.tagName === 'INPUT' || const isInput =
target.tagName === 'TEXTAREA' || target.tagName === "INPUT" ||
target.isContentEditable target.tagName === "TEXTAREA" ||
target.isContentEditable;
// 这些快捷键在输入框中也生效 // 这些快捷键在输入框中也生效
const globalShortcuts = ['Escape', 'Enter'] const globalShortcuts = ["Escape", "Enter"];
const needsModifier = shortcut.ctrl || shortcut.alt || shortcut.meta const needsModifier = shortcut.ctrl || shortcut.alt || shortcut.meta;
if (isInput && !globalShortcuts.includes(shortcut.key) && !needsModifier) { if (
continue isInput &&
!globalShortcuts.includes(shortcut.key) &&
!needsModifier
) {
continue;
} }
event.preventDefault() event.preventDefault();
shortcut.action() shortcut.action();
break break;
}
} }
} }
};
const handleKeyUp = (event: KeyboardEvent) => { const handleKeyUp = (event: KeyboardEvent) => {
activeKeys.value.delete(event.key.toLowerCase()) activeKeys.value.delete(event.key.toLowerCase());
} };
onMounted(() => { onMounted(() => {
window.addEventListener('keydown', handleKeyDown) window.addEventListener("keydown", handleKeyDown);
window.addEventListener('keyup', handleKeyUp) window.addEventListener("keyup", handleKeyUp);
}) });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('keydown', handleKeyDown) window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener('keyup', handleKeyUp) window.removeEventListener("keyup", handleKeyUp);
}) });
return { return {
activeKeys, activeKeys,
} };
} }
// 预定义的快捷键配置 // 预定义的快捷键配置
export function getDefaultShortcuts(actions: { export function getDefaultShortcuts(actions: {
newChat: () => void newChat: () => void;
toggleSidebar: () => void toggleSidebar: () => void;
focusInput: () => void focusInput: () => void;
sendMessage: () => void sendMessage: () => void;
cancelStream: () => void cancelStream: () => void;
toggleTheme: () => void toggleTheme: () => void;
showShortcuts: () => void showShortcuts: () => void;
searchConversations: () => void searchConversations: () => void;
}): KeyboardShortcut[] { }): KeyboardShortcut[] {
return [ return [
{ {
key: 'n', key: "n",
ctrl: true, ctrl: true,
description: '新建对话', description: "新建对话",
action: actions.newChat, action: actions.newChat,
}, },
{ {
key: 'b', key: "b",
ctrl: true, ctrl: true,
description: '切换侧边栏', description: "切换侧边栏",
action: actions.toggleSidebar, action: actions.toggleSidebar,
}, },
{ {
key: '/', key: "/",
ctrl: true, ctrl: true,
description: '聚焦输入框', description: "聚焦输入框",
action: actions.focusInput, action: actions.focusInput,
}, },
{ {
key: 'Enter', key: "Enter",
ctrl: true, ctrl: true,
description: '发送消息', description: "发送消息",
action: actions.sendMessage, action: actions.sendMessage,
}, },
{ {
key: 'Escape', key: "Escape",
description: '取消生成', description: "取消生成",
action: actions.cancelStream, action: actions.cancelStream,
}, },
{ {
key: 'd', key: "d",
ctrl: true, ctrl: true,
shift: true, shift: true,
description: '切换主题', description: "切换主题",
action: actions.toggleTheme, action: actions.toggleTheme,
}, },
{ {
key: '?', key: "?",
ctrl: true, ctrl: true,
description: '显示快捷键', description: "显示快捷键",
action: actions.showShortcuts, action: actions.showShortcuts,
}, },
{ {
key: 'k', key: "k",
ctrl: true, ctrl: true,
description: '搜索对话', description: "搜索对话",
action: actions.searchConversations, action: actions.searchConversations,
}, },
] ];
} }

View File

@ -112,13 +112,16 @@ class ChatApi {
// 构建 messages 数组system + 历史消息 + 当前用户消息 // 构建 messages 数组system + 历史消息 + 当前用户消息
const systemMessage = { const systemMessage = {
role: "system", role: "system",
content: request.systemPrompt || "你是一个智能助手,可以分析用户发送的文字,文件或图片内容,并进行回答。", content:
request.systemPrompt ||
"你是一个智能助手,可以分析用户发送的文字,文件或图片内容,并进行回答。",
}; };
const currentUserMessage = { const currentUserMessage = {
role: "user", role: "user",
content: userContent, content: userContent,
}; };
const allMessages = request.history && request.history.length > 0 const allMessages =
request.history && request.history.length > 0
? [systemMessage, ...request.history, currentUserMessage] ? [systemMessage, ...request.history, currentUserMessage]
: [systemMessage, currentUserMessage]; : [systemMessage, currentUserMessage];

View File

@ -1,14 +1,14 @@
import { defineStore } from 'pinia' import { defineStore } from "pinia";
import { ref } from 'vue' import { ref } from "vue";
import type { AppSettings, AIModel } from '@/types/chat' import type { AppSettings, AIModel } from "@/types/chat";
export const useSettingsStore = defineStore('settings', () => { export const useSettingsStore = defineStore("settings", () => {
// 默认设置 // 默认设置
const defaultSettings: AppSettings = { const defaultSettings: AppSettings = {
// 外观设置 // 外观设置
theme: 'system', theme: "system",
language: 'zh-CN', language: "zh-CN",
fontSize: 'medium', fontSize: "medium",
// 对话设置 // 对话设置
sendOnEnter: false, sendOnEnter: false,
@ -16,10 +16,10 @@ export const useSettingsStore = defineStore('settings', () => {
compactMode: false, compactMode: false,
// AI 默认设置 // AI 默认设置
defaultModel: 'glm-4.6', defaultModel: "glm-4.6",
defaultTemperature: 0.7, defaultTemperature: 0.7,
defaultMaxTokens: 4096, defaultMaxTokens: 4096,
defaultSystemPrompt: '你是一个有帮助的 AI 助手。', defaultSystemPrompt: "你是一个有帮助的 AI 助手。",
// 功能设置 // 功能设置
enableSound: true, enableSound: true,
@ -29,7 +29,7 @@ export const useSettingsStore = defineStore('settings', () => {
// 隐私设置 // 隐私设置
saveHistory: true, saveHistory: true,
shareAnalytics: false, shareAnalytics: false,
} };
// 可用的 AI 模型 // 可用的 AI 模型
const availableModels: AIModel[] = [ const availableModels: AIModel[] = [
@ -48,225 +48,231 @@ export const useSettingsStore = defineStore('settings', () => {
provider: "Zhipu", provider: "Zhipu",
}, },
{ {
id: 'glm-4-flash', id: "glm-4-flash",
name: '智普 GLM-4-Flash', name: "智普 GLM-4-Flash",
description: '快速高效,适合日常对话', description: "快速高效,适合日常对话",
maxTokens: 8192, maxTokens: 8192,
provider: 'Zhipu', provider: "Zhipu",
}, },
{ {
id: 'glm-4v-plus', id: "glm-4v-plus",
name: '智普 GLM-4V-Plus', name: "智普 GLM-4V-Plus",
description: '强大的视觉理解模型', description: "强大的视觉理解模型",
maxTokens: 8192, maxTokens: 8192,
provider: 'Zhipu', provider: "Zhipu",
}, },
] ];
// 状态 // 状态
const settings = ref<AppSettings>({ ...defaultSettings }) const settings = ref<AppSettings>({ ...defaultSettings });
const sidebarCollapsed = ref(false) const sidebarCollapsed = ref(false);
const sidebarWidth = ref(280) const sidebarWidth = ref(280);
const showShortcutsModal = ref(false) const showShortcutsModal = ref(false);
const showSearchModal = ref(false) const showSearchModal = ref(false);
const showSettingsModal = ref(false) const showSettingsModal = ref(false);
const showConversationSettingsModal = ref(false) const showConversationSettingsModal = ref(false);
// 主题相关 // 主题相关
function applyTheme(theme: AppSettings['theme']) { function applyTheme(theme: AppSettings["theme"]) {
const root = document.documentElement const root = document.documentElement;
if (theme === 'system') { if (theme === "system") {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches const prefersDark = window.matchMedia(
root.classList.toggle('dark', prefersDark) "(prefers-color-scheme: dark)",
).matches;
root.classList.toggle("dark", prefersDark);
} else { } else {
root.classList.toggle('dark', theme === 'dark') root.classList.toggle("dark", theme === "dark");
} }
} }
function toggleTheme() { function toggleTheme() {
const themes: AppSettings['theme'][] = ['light', 'dark', 'system'] const themes: AppSettings["theme"][] = ["light", "dark", "system"];
const currentIndex = themes.indexOf(settings.value.theme) const currentIndex = themes.indexOf(settings.value.theme);
settings.value.theme = themes[(currentIndex + 1) % themes.length] settings.value.theme = themes[(currentIndex + 1) % themes.length];
applyTheme(settings.value.theme) applyTheme(settings.value.theme);
saveToStorage() saveToStorage();
} }
function setTheme(theme: AppSettings['theme']) { function setTheme(theme: AppSettings["theme"]) {
settings.value.theme = theme settings.value.theme = theme;
applyTheme(theme) applyTheme(theme);
saveToStorage() saveToStorage();
} }
// 字体大小 // 字体大小
function applyFontSize(size: AppSettings['fontSize']) { function applyFontSize(size: AppSettings["fontSize"]) {
const root = document.documentElement const root = document.documentElement;
const sizeMap = { const sizeMap = {
small: '14px', small: "14px",
medium: '16px', medium: "16px",
large: '18px', large: "18px",
} };
root.style.setProperty('--base-font-size', sizeMap[size]) root.style.setProperty("--base-font-size", sizeMap[size]);
} }
function setFontSize(size: AppSettings['fontSize']) { function setFontSize(size: AppSettings["fontSize"]) {
settings.value.fontSize = size settings.value.fontSize = size;
applyFontSize(size) applyFontSize(size);
saveToStorage() saveToStorage();
} }
// 侧边栏 // 侧边栏
function toggleSidebar() { function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value sidebarCollapsed.value = !sidebarCollapsed.value;
saveToStorage() saveToStorage();
} }
function setSidebarWidth(width: number) { function setSidebarWidth(width: number) {
sidebarWidth.value = Math.max(200, Math.min(400, width)) sidebarWidth.value = Math.max(200, Math.min(400, width));
saveToStorage() saveToStorage();
} }
// 模态框 // 模态框
function openShortcutsModal() { function openShortcutsModal() {
showShortcutsModal.value = true showShortcutsModal.value = true;
} }
function closeShortcutsModal() { function closeShortcutsModal() {
showShortcutsModal.value = false showShortcutsModal.value = false;
} }
function openSearchModal() { function openSearchModal() {
showSearchModal.value = true showSearchModal.value = true;
} }
function closeSearchModal() { function closeSearchModal() {
showSearchModal.value = false showSearchModal.value = false;
} }
function openSettingsModal() { function openSettingsModal() {
showSettingsModal.value = true showSettingsModal.value = true;
} }
function closeSettingsModal() { function closeSettingsModal() {
showSettingsModal.value = false showSettingsModal.value = false;
} }
function openConversationSettingsModal() { function openConversationSettingsModal() {
showConversationSettingsModal.value = true showConversationSettingsModal.value = true;
} }
function closeConversationSettingsModal() { function closeConversationSettingsModal() {
showConversationSettingsModal.value = false showConversationSettingsModal.value = false;
} }
// 更新设置 // 更新设置
function updateSettings(updates: Partial<AppSettings>) { function updateSettings(updates: Partial<AppSettings>) {
Object.assign(settings.value, updates) Object.assign(settings.value, updates);
if (updates.theme) { if (updates.theme) {
applyTheme(updates.theme) applyTheme(updates.theme);
} }
if (updates.fontSize) { if (updates.fontSize) {
applyFontSize(updates.fontSize) applyFontSize(updates.fontSize);
} }
saveToStorage() saveToStorage();
} }
// 重置设置 // 重置设置
function resetSettings() { function resetSettings() {
settings.value = { ...defaultSettings } settings.value = { ...defaultSettings };
applyTheme(settings.value.theme) applyTheme(settings.value.theme);
applyFontSize(settings.value.fontSize) applyFontSize(settings.value.fontSize);
saveToStorage() saveToStorage();
} }
// 导出设置 // 导出设置
function exportSettings(): string { function exportSettings(): string {
return JSON.stringify(settings.value, null, 2) return JSON.stringify(settings.value, null, 2);
} }
// 导入设置 // 导入设置
function importSettings(json: string): boolean { function importSettings(json: string): boolean {
try { try {
const imported = JSON.parse(json) const imported = JSON.parse(json);
settings.value = { ...defaultSettings, ...imported } settings.value = { ...defaultSettings, ...imported };
applyTheme(settings.value.theme) applyTheme(settings.value.theme);
applyFontSize(settings.value.fontSize) applyFontSize(settings.value.fontSize);
saveToStorage() saveToStorage();
return true return true;
} catch { } catch {
return false return false;
} }
} }
// 存储 // 存储
function saveToStorage() { function saveToStorage() {
try { try {
localStorage.setItem('chat-settings', JSON.stringify(settings.value)) localStorage.setItem("chat-settings", JSON.stringify(settings.value));
localStorage.setItem('chat-sidebar-collapsed', JSON.stringify(sidebarCollapsed.value)) localStorage.setItem(
localStorage.setItem('chat-sidebar-width', JSON.stringify(sidebarWidth.value)) "chat-sidebar-collapsed",
JSON.stringify(sidebarCollapsed.value),
);
localStorage.setItem(
"chat-sidebar-width",
JSON.stringify(sidebarWidth.value),
);
} catch (e) { } catch (e) {
console.error('Failed to save settings:', e) console.error("Failed to save settings:", e);
} }
} }
// 存储选中模型 ID 的 localStorage key // 存储选中模型 ID 的 localStorage key
const MODEL_ID_KEY = 'modelSelectId' const MODEL_ID_KEY = "modelSelectId";
// 获取当前选择的模型 ID // 获取当前选择的模型 ID
function getSelectedModelId(): string { function getSelectedModelId(): string {
return defaultSettings.defaultModel;
return defaultSettings.defaultModel
} }
// 设置当前选择的模型 ID // 设置当前选择的模型 ID
function setSelectedModelId(modelId: string) { function setSelectedModelId(modelId: string) {
localStorage.setItem(MODEL_ID_KEY, modelId) localStorage.setItem(MODEL_ID_KEY, modelId);
// 同时更新 settings 中的 defaultModel // 同时更新 settings 中的 defaultModel
settings.value.defaultModel = modelId settings.value.defaultModel = modelId;
saveToStorage() saveToStorage();
} }
function loadFromStorage() { function loadFromStorage() {
try { try {
const stored = localStorage.getItem('chat-settings') const stored = localStorage.getItem("chat-settings");
if (stored) { if (stored) {
settings.value = { ...defaultSettings, ...JSON.parse(stored) } settings.value = { ...defaultSettings, ...JSON.parse(stored) };
} }
const collapsedStored = localStorage.getItem('chat-sidebar-collapsed') const collapsedStored = localStorage.getItem("chat-sidebar-collapsed");
if (collapsedStored) { if (collapsedStored) {
sidebarCollapsed.value = JSON.parse(collapsedStored) sidebarCollapsed.value = JSON.parse(collapsedStored);
} }
const widthStored = localStorage.getItem('chat-sidebar-width') const widthStored = localStorage.getItem("chat-sidebar-width");
if (widthStored) { if (widthStored) {
sidebarWidth.value = JSON.parse(widthStored) sidebarWidth.value = JSON.parse(widthStored);
} }
// 应用主题和字体 // 应用主题和字体
applyTheme(settings.value.theme) applyTheme(settings.value.theme);
applyFontSize(settings.value.fontSize) applyFontSize(settings.value.fontSize);
} catch (e) { } catch (e) {
console.error('Failed to load settings:', e) console.error("Failed to load settings:", e);
} }
} }
// 监听系统主题变化 // 监听系统主题变化
if (typeof window !== 'undefined') { if (typeof window !== "undefined") {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)') const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener('change', () => { mediaQuery.addEventListener("change", () => {
if (settings.value.theme === 'system') { if (settings.value.theme === "system") {
applyTheme('system') applyTheme("system");
} }
}) });
} }
// 初始化 // 初始化
loadFromStorage() loadFromStorage();
return { return {
// 状态 // 状态
@ -300,5 +306,5 @@ export const useSettingsStore = defineStore('settings', () => {
loadFromStorage, loadFromStorage,
getSelectedModelId, getSelectedModelId,
setSelectedModelId, setSelectedModelId,
} };
}) });

View File

@ -46,7 +46,13 @@
body { body {
margin: 0; margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family:
"Inter",
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }