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",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"tailwindcss": "^4.1.18",
"typescript": "~5.9.3",
@ -4363,6 +4364,22 @@
"dev": true,
"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": {
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/property-information/-/property-information-7.1.0.tgz",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -67,12 +67,19 @@ async function textCopy(data: any) {
<div class="thinking-title">
<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-2" />
<span class="dot dot-3" />
</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>
</div>
@ -157,7 +164,9 @@ async function textCopy(data: any) {
.thinking-content {
max-height: 2000px;
overflow: hidden;
transition: max-height 0.35s ease, opacity 0.25s ease;
transition:
max-height 0.35s ease,
opacity 0.25s ease;
opacity: 1;
}
.thinking-content.collapsed {

View File

@ -218,7 +218,9 @@ onMounted(() => {
chatApi.getModels().then((res: any) => {
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) {
modelSelect.value = model.name;
} else if (availableModels.value.length > 0) {

View File

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

View File

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

View File

@ -9,7 +9,7 @@
<div class="sidebar-header">
<div class="logo">
<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>
<button
class="collapse-btn"

View File

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

View File

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

View File

@ -4,7 +4,9 @@
type="checkbox"
:checked="modelValue"
: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>
</label>
@ -12,13 +14,13 @@
<script setup lang="ts">
defineProps<{
modelValue: boolean
disabled?: boolean
}>()
modelValue: boolean;
disabled?: boolean;
}>();
defineEmits<{
'update:modelValue': [value: boolean]
}>()
"update:modelValue": [value: boolean];
}>();
</script>
<style lang="scss" scoped>
@ -65,7 +67,7 @@ defineEmits<{
}
&::before {
content: '';
content: "";
position: absolute;
width: 20px;
height: 20px;

View File

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

View File

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

View File

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

View File

@ -46,7 +46,13 @@
body {
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;
-moz-osx-font-smoothing: grayscale;
}