perf(code): 格式化代码以提升可读性与一致性,统一代码风格 [优化:使用 Prettier 进行格式化]
This commit is contained in:
parent
972d92ba1a
commit
6984a09737
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
99
src/App.vue
99
src/App.vue
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
};
|
||||||
})
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue