diff --git a/README.md b/README.md index e5d40c8..1b6a2b5 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,10 @@ ## 技术栈 - Vue 3 - 渐进式 JavaScript 框架 -- Vite - 下一代前端构建工具 -- Vue Router - 官方路由管理器 -- Pinia - Vue 的官方状态管理库 - TypeScript - JavaScript 的超集 -- Tailwind CSS - 实用优先的 CSS 框架 +- Element Plus - 基于 Vue 3 的组件库 +- Vite - 下一代前端构建工具 +- Sass - CSS 预处理器 ## 功能特性 diff --git a/dist.zip b/dist.zip new file mode 100644 index 0000000..23bed64 Binary files /dev/null and b/dist.zip differ diff --git a/package.json b/package.json index 2e33ffd..e38523b 100644 --- a/package.json +++ b/package.json @@ -48,10 +48,19 @@ "vue-router": "^4.5.0" }, "devDependencies": { - "@vitejs/plugin-vue": "^5.2.1", - "sass": "^1.86.3", + "@vitejs/plugin-vue": "^5.0.4", + "@types/node": "^20.11.19", + "@vue/compiler-sfc": "^3.4.21", + "autoprefixer": "^10.4.17", + "postcss": "^8.4.35", + "postcss-import": "^16.0.1", + "postcss-preset-env": "^9.3.0", + "sass": "^1.71.1", "sass-embedded": "^1.86.3", - "vite": "^6.2.0", - "vite-plugin-mkcert": "^1.17.8" + "typescript": "^5.3.3", + "vite": "^5.1.4", + "vite-plugin-mkcert": "^1.17.8", + "vite-plugin-vue-devtools": "^7.0.19", + "vue-tsc": "^1.8.27" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..0fa6f18 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,12 @@ +export default { + plugins: { + 'postcss-import': {}, + 'postcss-preset-env': { + stage: 3, + features: { + 'nesting-rules': true + } + }, + autoprefixer: {} + } +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 7de1880..f762c1e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,40 +1,58 @@ - \ No newline at end of file diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue deleted file mode 100644 index 546ebbc..0000000 --- a/src/components/HelloWorld.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - - diff --git a/src/components/chat/ModeSetting.vue b/src/components/chat/ModeSetting.vue deleted file mode 100644 index 4a9a614..0000000 --- a/src/components/chat/ModeSetting.vue +++ /dev/null @@ -1,90 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/components/settings/SettingsPanel.vue b/src/components/settings/SettingsPanel.vue new file mode 100644 index 0000000..7bcb87a --- /dev/null +++ b/src/components/settings/SettingsPanel.vue @@ -0,0 +1,243 @@ + + + + + \ No newline at end of file diff --git a/src/components/settings/ThemeSwitcher.vue b/src/components/settings/ThemeSwitcher.vue new file mode 100644 index 0000000..2e37058 --- /dev/null +++ b/src/components/settings/ThemeSwitcher.vue @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/src/main.js b/src/main.js index 66d3ead..241e295 100644 --- a/src/main.js +++ b/src/main.js @@ -1,21 +1,39 @@ import { createApp } from "vue"; -import "./style.css"; +import "@/styles/style.css"; +// 使用自定义主题 +import '@/styles/element-variables.scss' import App from "./App.vue"; import {router} from "@/router/index"; import { createPinia } from "pinia"; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' +import { useSettingsStore, applyDisplayMode } from '@/store/settings'; // Import store and helper +import { setTheme } from '@/styles/theme'; // Import setTheme // 引入 Element Plus import ElementPlus from 'element-plus' -// 使用自定义主题 -import './styles/element-variables.scss' import zhCn from 'element-plus/dist/locale/zh-cn.mjs' const pinia = createPinia(); pinia.use(piniaPluginPersistedstate) const app = createApp(App); -app.use(pinia); +app.use(pinia); // Pinia is now available + +// Access the store *after* app.use(pinia) +// The store state will be hydrated from localStorage here by the persistence plugin +const settingsStore = useSettingsStore(); + +// Apply persisted/initial settings +applyDisplayMode(settingsStore.displayMode); +setTheme(settingsStore.theme); +// Apply initial animation setting +if (!settingsStore.animationsEnabled) { + document.body.classList.add('no-animations'); +} else { + // Ensure the class is not present if animations are enabled initially + document.body.classList.remove('no-animations'); +} + app.use(router); app.use(ElementPlus, { locale: zhCn, diff --git a/src/router/index.js b/src/router/index.js index c8350cf..b478c06 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -18,17 +18,11 @@ const routes = [ }, { path: "/chat", - component: () => import("@/layouts/MainLayout.vue"), - children: [ - { - path: "", - name: "chat", - meta: { - icon: ChatRound - }, - component: () => import("@/views/chat/index.vue"), - }, - ], + name: "chat", + meta: { + icon: ChatRound + }, + component: () => import("@/views/chat/index.vue"), }, ]; diff --git a/src/store/settings.js b/src/store/settings.js index c8ce361..136404b 100644 --- a/src/store/settings.js +++ b/src/store/settings.js @@ -1,34 +1,40 @@ import { defineStore } from 'pinia' +import { setTheme, themes } from '@/styles/theme' + +// Helper function to apply display mode +export function applyDisplayMode(mode) { + const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + let darkModeEnabled = false; + + if (mode === 'dark') { + darkModeEnabled = true; + } else if (mode === 'system') { + darkModeEnabled = prefersDark; + } // else mode === 'light', darkModeEnabled remains false + + if (darkModeEnabled) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } +} export const useSettingsStore = defineStore('settings', { state: () => ({ // 侧边栏状态 sidebarCollapsed: false, - - // 界面配置 - interface: { - // 是否显示时间戳 - showTimestamp: true, - // 消息气泡最大宽度(px) - messageMaxWidth: 800, - // 字体大小(px) - fontSize: 14 - }, - - // 主题配置 - theme: { - // 主色调 - primary: '#07c160', - // 背景色 - background: '#f5f5f5', - // 文字颜色 - textColor: '#333333' - } + // 显示模式: 'light', 'dark', 'system' + displayMode: 'system', // Default to system preference + // 主题配置 (e.g., 'default', 'fresh', 'warm') + theme: 'default', + // 动画效果开关 + animationsEnabled: true, }), persist: { key: 'settings', - paths: ['sidebarCollapsed'] + // Persist displayMode and theme + paths: ['sidebarCollapsed', 'displayMode', 'theme', 'animationsEnabled'] }, actions: { @@ -37,25 +43,54 @@ export const useSettingsStore = defineStore('settings', { this.sidebarCollapsed = !this.sidebarCollapsed }, - // 更新界面配置 - updateInterface(config) { - this.interface = { - ...this.interface, - ...config + // 切换动画状态 + toggleAnimations(enabled) { + if (typeof enabled === 'boolean') { + this.animationsEnabled = enabled; + } else { + this.animationsEnabled = !this.animationsEnabled; + } + // Apply animation class to body + if (this.animationsEnabled) { + document.body.classList.remove('no-animations'); + } else { + document.body.classList.add('no-animations'); } }, - // 更新主题配置 - updateTheme(config) { - this.theme = { - ...this.theme, - ...config + // 更新显示模式 + updateDisplayMode(newMode) { + if (['light', 'dark', 'system'].includes(newMode)) { + this.displayMode = newMode; + applyDisplayMode(newMode); // Apply the change immediately + // Optional: Re-apply theme colors if they differ significantly in dark mode + // setTheme(this.theme); // Uncomment if needed + } else { + console.warn(`Invalid display mode: ${newMode}`); + } + }, + + // 更新主题颜色配置 + updateTheme(newTheme) { + if (themes.hasOwnProperty(newTheme)) { // Check if the theme exists + this.theme = newTheme; + setTheme(newTheme); // Apply the theme colors + } else { + console.warn(`Invalid theme: ${newTheme}`); } }, // 重置所有设置 resetSettings() { this.$reset() + // Re-apply settings after reset + setTheme(this.theme); + applyDisplayMode(this.displayMode); + this.toggleAnimations(this.animationsEnabled); // Re-apply animation class } } -}) \ No newline at end of file +}) + +// --- Helper Function to apply animation class --- +// Moved applying logic into the toggleAnimations action directly +// export function applyAnimationSetting(enabled) { ... } \ No newline at end of file diff --git a/src/style.css b/src/styles/style.css similarity index 81% rename from src/style.css rename to src/styles/style.css index 71c27b1..c701ab1 100644 --- a/src/style.css +++ b/src/styles/style.css @@ -1,3 +1,22 @@ +/* 基础样式 */ +:root { + --primary-color: #409eff; + --success-color: #67c23a; + --warning-color: #e6a23c; + --danger-color: #f56c6c; + --info-color: #909399; +} + +/* 全局样式 */ +body { + margin: 0; + padding: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + + :root { font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; @@ -58,10 +77,6 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } -.card { - padding: 2em; -} - #app { /* max-width: 100vh; */ height: 100%; diff --git a/src/styles/theme.js b/src/styles/theme.js new file mode 100644 index 0000000..5094f1c --- /dev/null +++ b/src/styles/theme.js @@ -0,0 +1,38 @@ +// 使用 import.meta.glob 动态导入 themes 目录下的所有 .js 文件 +// { eager: true } 表示同步导入,立即获取模块内容 +const themeModules = import.meta.glob('./themes/*.js', { eager: true }); + +// 构建 themes 对象 +export const themes = {}; +for (const path in themeModules) { + // 从路径中提取主题名称 (e.g., './themes/default.js' -> 'default') + const themeName = path.replace('./themes/', '').replace('.js', ''); + // 获取模块的默认导出 (即主题配置对象) + if (themeModules[path].default) { + themes[themeName] = themeModules[path].default; + } +} + +// 主题切换函数 +export function setTheme(themeName) { + const theme = themes[themeName] || themes.default // Fallback to default + if (!theme) { + console.error(`Theme "${themeName}" not found.`); + return; + } + + const root = document.documentElement + + // 设置主题色变量 + Object.keys(theme).forEach(key => { + root.style.setProperty(`--el-color-${key}`, theme[key]) + }) + + // 设置主题类名 + // 查找 body 上是否已有 theme- 开头的类 + const currentThemeClass = Array.from(document.body.classList).find(cls => cls.startsWith('theme-')); + if (currentThemeClass) { + document.body.classList.remove(currentThemeClass); + } + document.body.classList.add(`theme-${themeName}`); +} \ No newline at end of file diff --git a/src/styles/themes/business.js b/src/styles/themes/business.js new file mode 100644 index 0000000..21a5887 --- /dev/null +++ b/src/styles/themes/business.js @@ -0,0 +1,7 @@ +export default { + primary: '#2C3E50', + success: '#27AE60', + warning: '#F39C12', + danger: '#E74C3C', + info: '#3498DB' +}; \ No newline at end of file diff --git a/src/styles/themes/dark.js b/src/styles/themes/dark.js new file mode 100644 index 0000000..9a17238 --- /dev/null +++ b/src/styles/themes/dark.js @@ -0,0 +1,7 @@ +export default { + primary: '#5CACEE', + success: '#85CE61', + warning: '#E6A23C', + danger: '#F78989', + info: '#A6A9AD' +}; \ No newline at end of file diff --git a/src/styles/themes/default.js b/src/styles/themes/default.js new file mode 100644 index 0000000..0eca09b --- /dev/null +++ b/src/styles/themes/default.js @@ -0,0 +1,7 @@ +export default { + primary: '#409EFF', + success: '#67C23A', + warning: '#E6A23C', + danger: '#F56C6C', + info: '#909399' +}; \ No newline at end of file diff --git a/src/styles/themes/fresh.js b/src/styles/themes/fresh.js new file mode 100644 index 0000000..07570c0 --- /dev/null +++ b/src/styles/themes/fresh.js @@ -0,0 +1,7 @@ +export default { + primary: '#36D1DC', + success: '#5CB85C', + warning: '#F0AD4E', + danger: '#D9534F', + info: '#5BC0DE' +}; \ No newline at end of file diff --git a/src/styles/themes/warm.js b/src/styles/themes/warm.js new file mode 100644 index 0000000..3810ed8 --- /dev/null +++ b/src/styles/themes/warm.js @@ -0,0 +1,7 @@ +export default { + primary: '#FF6B6B', + success: '#4ECDC4', + warning: '#FFE66D', + danger: '#FF6B6B', + info: '#45B7D1' +}; \ No newline at end of file diff --git a/src/views/chat/ChatHeader.vue b/src/views/chat/ChatHeader.vue index 31206a4..2038356 100644 --- a/src/views/chat/ChatHeader.vue +++ b/src/views/chat/ChatHeader.vue @@ -3,7 +3,6 @@
{ .chat-header { height: 60px; border-bottom: 1px solid #e5e5e5; - background: #fff; + background: #FFF; padding: 0 20px; flex-shrink: 0; diff --git a/src/views/chat/ChatInterface.vue b/src/views/chat/ChatInterface.vue index e4a3194..c7864ad 100644 --- a/src/views/chat/ChatInterface.vue +++ b/src/views/chat/ChatInterface.vue @@ -709,7 +709,6 @@ onUnmounted(() => { padding: 16px; background: #f5f5f5; border-top: 1px solid #e5e5e5; - z-index: 101; :deep(.el-textarea) { flex: 1; diff --git a/src/views/chat/ChatModeSelector.vue b/src/views/chat/ChatModeSelector.vue index 1429f40..bd1815d 100644 --- a/src/views/chat/ChatModeSelector.vue +++ b/src/views/chat/ChatModeSelector.vue @@ -1,5 +1,5 @@ @@ -67,10 +50,9 @@ import { useChatStore } from '@/store/chat' import { useSettingsStore } from '@/store/settings' import { computed, ref, onMounted, onUnmounted } from 'vue' -import { Delete, EditPen, RefreshRight, Expand, Fold } from '@element-plus/icons-vue' -import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElMessageBox } from 'element-plus' -import ModeSetting from '@/components/chat/ModeSetting.vue' -import HistoryButton from '@/components/chat/HistoryButton.vue' +import { ChatDotRound, ChatLineRound, ChatRound, Service, Setting } from '@element-plus/icons-vue' +import { ElMenu, ElMenuItem, ElIcon, ElPopover } from 'element-plus' +import SettingsPanel from '@/components/settings/SettingsPanel.vue' const chatStore = useChatStore() const settingsStore = useSettingsStore() @@ -79,7 +61,9 @@ const chatModes = computed(() => { return Object.keys(chatStore.chatModes).map(key => ({ id: key, name: chatStore.chatModes[key].name, - icon: chatStore.chatModes[key].icon + icon: key === 'general' ? ChatRound : + key === 'assistant' ? Service : + key === 'chat' ? ChatDotRound : ChatLineRound })) }) @@ -88,58 +72,10 @@ const currentModeHistory = computed(() => { return chatStore.conversations.filter(chat => chat.mode === chatStore.currentMode) }) -// 格式化会话标题 -const formatChatTitle = (chat) => { - // 使用第一条消息作为标题,如果没有则使用默认标题 - if (chat.messages && chat.messages.length > 0) { - const firstMessage = chat.messages[0].question - return firstMessage.length > 20 ? firstMessage.substring(0, 20) + '...' : firstMessage - } - return `会话 ${chat.conversationId.substring(0, 8)}` -} - const selectMode = (modeId) => { chatStore.setCurrentMode(modeId) } -const handleHistorySelect = (conversationId) => { - chatStore.switchConversation(conversationId) -} - -const handleDelete = async (conversationId) => { - try { - await ElMessageBox.confirm( - '确定要删除这个会话吗?删除后无法恢复。', - '删除确认', - { - confirmButtonText: '确定', - cancelButtonText: '取消', - type: 'warning', - } - ) - chatStore.deleteConversation(conversationId) - } catch { - // 用户取消删除 - } -} - -const handleModeSettings = (command, modeId) => { - switch (command) { - case 'edit': - // TODO: 实现编辑设置的逻辑 - console.log('编辑设置:', modeId) - break - case 'reset': - // TODO: 实现重置设置的逻辑 - console.log('重置设置:', modeId) - break - case 'delete': - // TODO: 实现删除场景的逻辑 - console.log('删除场景:', modeId) - break - } -} - const isMobile = ref(false) const checkMobile = () => { @@ -157,341 +93,275 @@ onUnmounted(() => { + + \ No newline at end of file diff --git a/src/views/chat/index.vue b/src/views/chat/index.vue index 278b5a5..8729542 100644 --- a/src/views/chat/index.vue +++ b/src/views/chat/index.vue @@ -1,5 +1,5 @@