feat(settings): 实现主题切换和显示模式功能
- 新增主题切换功能,支持多种主题颜色配置 - 实现显示模式切换,包括系统、深色和浅色模式 - 添加动画效果开关功能 - 优化设置面板组件,集中管理所有设置项 - 重构部分代码以支持新功能,包括 store、样式和组件
This commit is contained in:
parent
9d4d52260f
commit
f65dd67087
|
@ -5,11 +5,10 @@
|
|||
## 技术栈
|
||||
|
||||
- Vue 3 - 渐进式 JavaScript 框架
|
||||
- Vite - 下一代前端构建工具
|
||||
- Vue Router - 官方路由管理器
|
||||
- Pinia - Vue 的官方状态管理库
|
||||
- TypeScript - JavaScript 的超集
|
||||
- Tailwind CSS - 实用优先的 CSS 框架
|
||||
- Element Plus - 基于 Vue 3 的组件库
|
||||
- Vite - 下一代前端构建工具
|
||||
- Sass - CSS 预处理器
|
||||
|
||||
## 功能特性
|
||||
|
||||
|
|
17
package.json
17
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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'postcss-import': {},
|
||||
'postcss-preset-env': {
|
||||
stage: 3,
|
||||
features: {
|
||||
'nesting-rules': true
|
||||
}
|
||||
},
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
68
src/App.vue
68
src/App.vue
|
@ -1,40 +1,58 @@
|
|||
<script setup>
|
||||
import { RouterView } from "vue-router";
|
||||
import { onMounted } from 'vue'
|
||||
onMounted(() => {
|
||||
import { useSettingsStore } from '@/store/settings';
|
||||
import { setTheme } from '@/styles/theme';
|
||||
|
||||
})
|
||||
onMounted(() => {
|
||||
//首先我们获得视口高度并将其乘以1%以获得1vh单位的值
|
||||
let vh = window.innerHeight * 0.01
|
||||
// 然后,我们将——vh自定义属性中的值设置为文档的根
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`)
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// 我们监听resize事件 视图大小发生变化就重新计算1vh的值
|
||||
window.addEventListener('resize', () => {
|
||||
// 我们执行与前面相同的脚本
|
||||
let vh = window.innerHeight * 0.01
|
||||
console.log(vh);
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`)
|
||||
})
|
||||
})
|
||||
onMounted(() => {
|
||||
// 应用启动时,根据 store 中持久化的主题设置样式
|
||||
setTheme(settingsStore.theme);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
<style lang="scss">
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '\5FAE\8F6F\96C5\9ED1', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: var(--el-bg-color);
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px; /* 滚动条宽度 */
|
||||
height: 6px; /* 滚动条高度,主要用于水平滚动条 */
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent; /* 滚动条轨道背景 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(144, 147, 153, 0.3); /* 滚动条滑块颜色 */
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(144, 147, 153, 0.5); /* 悬停时滑块颜色 */
|
||||
}
|
||||
|
||||
// 全局动画控制
|
||||
body.no-animations {
|
||||
* {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,43 +0,0 @@
|
|||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
|
@ -1,90 +0,0 @@
|
|||
<template>
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="handleCommand"
|
||||
class="mode-setting"
|
||||
>
|
||||
<el-button
|
||||
class="setting-btn"
|
||||
:icon="Setting"
|
||||
text
|
||||
bg
|
||||
@click.stop
|
||||
/>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<slot>
|
||||
<!-- 默认选项 -->
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="reset">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
重置设置
|
||||
</el-dropdown-item>
|
||||
</slot>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Setting, Edit, RefreshRight } from '@element-plus/icons-vue'
|
||||
|
||||
const emit = defineEmits(['command'])
|
||||
|
||||
const handleCommand = (command) => {
|
||||
emit('command', command)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mode-setting {
|
||||
.setting-btn {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
transition: all 0.3s ease;
|
||||
border: none;
|
||||
background: transparent !important;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在深色模式下的样式
|
||||
.mode-item.active {
|
||||
.setting-btn {
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,243 @@
|
|||
<template>
|
||||
<div class="settings-popover">
|
||||
<h4>整体外观</h4>
|
||||
<el-radio-group
|
||||
v-model="currentDisplayMode"
|
||||
@change="changeDisplayMode"
|
||||
size="small"
|
||||
class="display-mode-group"
|
||||
>
|
||||
<el-radio-button label="system">
|
||||
<span class="icon system"></span> 系统默认
|
||||
</el-radio-button>
|
||||
<el-radio-button label="light">
|
||||
<span class="icon light"></span> 浅色
|
||||
</el-radio-button>
|
||||
<el-radio-button label="dark">
|
||||
<span class="icon dark"></span> 深色
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>主题</h4>
|
||||
<div class="theme-grid">
|
||||
<div
|
||||
v-for="(theme, name) in filteredThemes"
|
||||
:key="name"
|
||||
class="theme-option"
|
||||
:class="{ 'active': currentTheme === name }"
|
||||
@click="changeTheme(name)"
|
||||
>
|
||||
<div class="theme-preview" :style="{ backgroundColor: theme.primary }"></div>
|
||||
<span class="theme-label">{{ getThemeLabel(name) }}</span>
|
||||
</div>
|
||||
<!-- 可以添加 '选择一种颜色...' 的选项 -->
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<h4>界面设置</h4>
|
||||
<div class="setting-item">
|
||||
<span>启用动画效果</span>
|
||||
<el-switch
|
||||
v-model="animationsEnabled"
|
||||
@change="toggleAnimationsSetting"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 可以添加更多设置项 -->
|
||||
<!--
|
||||
<el-divider />
|
||||
<h4>其他设置</h4>
|
||||
<div class="setting-item">
|
||||
<span>收起侧边栏</span>
|
||||
<el-switch v-model="sidebarCollapsed" @change="toggleSidebarSetting" />
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useSettingsStore } from '@/store/settings';
|
||||
// Import themes object and omit 'dark' theme from selection
|
||||
import { themes as availableThemes } from '@/styles/theme';
|
||||
import {
|
||||
ElButton, ElDropdown, ElDropdownMenu, ElDropdownItem,
|
||||
ElDivider, ElSwitch, ElIcon, ElRadioGroup, ElRadioButton
|
||||
} from 'element-plus';
|
||||
import { ArrowDown } from '@element-plus/icons-vue'; // Keep if needed elsewhere, remove if not
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// --- Display Mode ---
|
||||
const currentDisplayMode = ref(settingsStore.displayMode);
|
||||
|
||||
const changeDisplayMode = (newMode) => {
|
||||
settingsStore.updateDisplayMode(newMode);
|
||||
// currentDisplayMode will update reactively from the store if using computed,
|
||||
// but direct update here ensures immediate visual feedback if needed
|
||||
currentDisplayMode.value = newMode;
|
||||
};
|
||||
|
||||
// --- Theme Settings ---
|
||||
const currentTheme = ref(settingsStore.theme);
|
||||
|
||||
// Filter out the 'dark' theme as it's handled by displayMode
|
||||
const filteredThemes = computed(() => {
|
||||
const { dark, ...rest } = availableThemes;
|
||||
return rest;
|
||||
});
|
||||
|
||||
|
||||
const themeLabels = {
|
||||
default: '默认',
|
||||
// dark: '深色', // No longer selectable as a base theme color palette
|
||||
fresh: '清新',
|
||||
warm: '暖色',
|
||||
business: '商务',
|
||||
// Add labels for other themes from your image if they exist in styles/theme.js
|
||||
// e.g., cool: '凉风', pink: '柔和的粉色', bubblegum: '泡泡糖', sunny: '晴天', etc.
|
||||
// Make sure the keys match the keys in your `themes` object in theme.js
|
||||
};
|
||||
|
||||
const getThemeLabel = (themeName) => {
|
||||
return themeLabels[themeName] || themeName;
|
||||
};
|
||||
|
||||
const changeTheme = (newTheme) => {
|
||||
settingsStore.updateTheme(newTheme); // Use the action
|
||||
currentTheme.value = newTheme; // Update local ref for immediate feedback
|
||||
};
|
||||
|
||||
// --- Animation Settings ---
|
||||
const animationsEnabled = ref(settingsStore.animationsEnabled);
|
||||
|
||||
const toggleAnimationsSetting = (value) => {
|
||||
settingsStore.toggleAnimations(value); // Use the action
|
||||
animationsEnabled.value = value; // Update local ref
|
||||
};
|
||||
|
||||
// Watch for external store changes (e.g., reset button) if necessary
|
||||
// settingsStore.$subscribe((mutation, state) => {
|
||||
// currentDisplayMode.value = state.displayMode;
|
||||
// currentTheme.value = state.theme;
|
||||
// animationsEnabled.value = state.animationsEnabled;
|
||||
// });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-popover {
|
||||
padding: 15px;
|
||||
min-width: 240px; // Increased width for better layout
|
||||
|
||||
h4 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600; // Slightly bolder heading
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.display-mode-group {
|
||||
display: flex; // Use flex for equal spacing
|
||||
width: 100%;
|
||||
margin-bottom: 5px; // Add some space below
|
||||
|
||||
.el-radio-button {
|
||||
flex: 1; // Make buttons fill the space equally
|
||||
:deep(.el-radio-button__inner) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center; // Center content
|
||||
border-radius: 4px !important; // Slightly round corners
|
||||
border: 1px solid var(--el-border-color-lighter); // Use variable for border
|
||||
box-shadow: none; // Remove default shadow
|
||||
padding: 8px 5px; // Adjust padding
|
||||
font-size: 12px; // Smaller font size
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 5px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
|
||||
&.system { background: linear-gradient(135deg, #fff 50%, #222 50%); border-color: #888; }
|
||||
&.light { background-color: #fff; }
|
||||
&.dark { background-color: #222; border-color: #555; }
|
||||
}
|
||||
}
|
||||
|
||||
// Style for selected button
|
||||
&.is-active :deep(.el-radio-button__inner) {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove spacing between buttons created by inline-block nature
|
||||
.el-radio-button + .el-radio-button {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); // Responsive grid
|
||||
gap: 10px; // Gap between items
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-border-color-hover);
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.theme-preview {
|
||||
width: 100%;
|
||||
height: 30px; // Adjust height as needed
|
||||
border-radius: 4px;
|
||||
margin-bottom: 5px;
|
||||
border: 1px solid var(--el-border-color-lighter); // Add subtle border
|
||||
}
|
||||
|
||||
.theme-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: block; // Ensure label is on new line
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px; // Consistent spacing
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.el-divider {
|
||||
margin: 18px 0; // Increase divider margin
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<el-button type="primary">
|
||||
主题切换
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-for="(theme, name) in themes" :key="name" :command="name">
|
||||
<div class="theme-item">
|
||||
<span class="theme-name">{{ name }}</span>
|
||||
<div class="theme-colors">
|
||||
<span v-for="(color, type) in theme" :key="type"
|
||||
class="color-dot"
|
||||
:style="{ backgroundColor: color }">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ArrowDown } from '@element-plus/icons-vue'
|
||||
import { themes, setTheme } from '@/styles/theme'
|
||||
|
||||
const handleCommand = (themeName) => {
|
||||
setTheme(themeName)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.theme-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.theme-name {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.theme-colors {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.color-dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
26
src/main.js
26
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,
|
||||
|
|
|
@ -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"),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// --- Helper Function to apply animation class ---
|
||||
// Moved applying logic into the toggleAnimations action directly
|
||||
// export function applyAnimationSetting(enabled) { ... }
|
|
@ -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%;
|
|
@ -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}`);
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
primary: '#2C3E50',
|
||||
success: '#27AE60',
|
||||
warning: '#F39C12',
|
||||
danger: '#E74C3C',
|
||||
info: '#3498DB'
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
primary: '#5CACEE',
|
||||
success: '#85CE61',
|
||||
warning: '#E6A23C',
|
||||
danger: '#F78989',
|
||||
info: '#A6A9AD'
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
primary: '#409EFF',
|
||||
success: '#67C23A',
|
||||
warning: '#E6A23C',
|
||||
danger: '#F56C6C',
|
||||
info: '#909399'
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
primary: '#36D1DC',
|
||||
success: '#5CB85C',
|
||||
warning: '#F0AD4E',
|
||||
danger: '#D9534F',
|
||||
info: '#5BC0DE'
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
primary: '#FF6B6B',
|
||||
success: '#4ECDC4',
|
||||
warning: '#FFE66D',
|
||||
danger: '#FF6B6B',
|
||||
info: '#45B7D1'
|
||||
};
|
|
@ -3,7 +3,6 @@
|
|||
<div class="header-content">
|
||||
<div class="title">
|
||||
<el-button
|
||||
v-if="isMobile || settingsStore.sidebarCollapsed"
|
||||
class="desktop-sidebar-toggle"
|
||||
:icon="Expand"
|
||||
@click="settingsStore.toggleSidebar"
|
||||
|
@ -128,7 +127,7 @@ onUnmounted(() => {
|
|||
.chat-header {
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
background: #fff;
|
||||
background: #FFF;
|
||||
padding: 0 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
|
|
|
@ -709,7 +709,6 @@ onUnmounted(() => {
|
|||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
z-index: 101;
|
||||
|
||||
:deep(.el-textarea) {
|
||||
flex: 1;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="mode-selector-container">
|
||||
<div
|
||||
class="mode-selector-backdrop"
|
||||
:class="{ visible: !settingsStore.sidebarCollapsed }"
|
||||
|
@ -8,57 +8,40 @@
|
|||
></div>
|
||||
<div class="mode-selector" :class="{ 'collapsed': settingsStore.sidebarCollapsed }">
|
||||
<div class="mode-header">
|
||||
<div class="mode-title" v-if="!settingsStore.sidebarCollapsed">
|
||||
<img
|
||||
src="/lgogo.svg"
|
||||
alt="Logo"
|
||||
class="lgoo-icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="collapsed-logo" v-else>
|
||||
<img
|
||||
src="/logo.svg"
|
||||
alt="Collapsed Logo"
|
||||
class="lgoo-icon-small"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!settingsStore.sidebarCollapsed" class="header-actions">
|
||||
<!-- <HistoryButton v-if="!settingsStore.sidebarCollapsed" :icon-only="true" /> -->
|
||||
<el-button
|
||||
|
||||
class="collapse-btn"
|
||||
:icon="Fold"
|
||||
text
|
||||
bg
|
||||
@click="settingsStore.toggleSidebar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-list">
|
||||
<div
|
||||
v-for="mode in chatModes"
|
||||
:key="mode.id"
|
||||
:class="['mode-item', { active: chatStore.currentMode === mode.id }]"
|
||||
@click="selectMode(mode.id)"
|
||||
>
|
||||
<span class="emoji-icon">{{ mode.icon }}</span>
|
||||
<span v-if="!settingsStore.sidebarCollapsed" class="mode-name">{{ mode.name }}</span>
|
||||
<ModeSetting v-if="!settingsStore.sidebarCollapsed" @command="(cmd) => handleModeSettings(cmd, mode.id)">
|
||||
<el-dropdown-item command="edit">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
编辑设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="reset">
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
重置设置
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" divided>
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除场景
|
||||
</el-dropdown-item>
|
||||
</ModeSetting>
|
||||
<div class="mode-header-title" v-if="!settingsStore.sidebarCollapsed">
|
||||
<img src="/lgogo.svg" alt="Logo" class="mode-header-icon" />
|
||||
</div>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="chatStore.currentMode"
|
||||
class="mode-menu"
|
||||
:collapse="settingsStore.sidebarCollapsed"
|
||||
@select="selectMode"
|
||||
>
|
||||
<el-menu-item v-for="mode in chatModes" :key="mode.id" :index="mode.id" class="mode-item">
|
||||
<el-icon><component :is="mode.icon" /></el-icon>
|
||||
<span class="mode-name">{{ mode.name }}</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
|
||||
<!-- Settings Button Popover -->
|
||||
<el-popover
|
||||
placement="top-end"
|
||||
trigger="click"
|
||||
popper-class="settings-popover-popper"
|
||||
>
|
||||
<template #reference>
|
||||
<div class="sidebar-footer">
|
||||
<div class="settings-button" title="设置">
|
||||
<el-icon><Setting /></el-icon>
|
||||
<span class="button-text">设置</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Popover Content -->
|
||||
<SettingsPanel />
|
||||
</el-popover>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -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(() => {
|
|||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$primary-color: #4CAF50;
|
||||
$text-color: #333;
|
||||
$text-color-dark: #fff;
|
||||
$background-color: #F7F7F7;
|
||||
$background-color-dark: #1e1e1e;
|
||||
$border-color: #e0e0e0;
|
||||
$border-color-dark: #333;
|
||||
$hover-color: #f5f5f5;
|
||||
$hover-color-dark: #2d2d2d;
|
||||
.mode-selector-container {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mode-selector {
|
||||
width: 240px;
|
||||
min-width: 240px;
|
||||
background: $background-color;
|
||||
border-right: 1px solid $border-color;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--el-bg-color);
|
||||
border-right: 1px solid var(--el-border-color-light);
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
|
||||
.mode-header {
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
padding: 15px 0;
|
||||
justify-content: center;
|
||||
|
||||
.mode-name {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
font-size: 24px;
|
||||
width: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-header {
|
||||
padding: 0 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mode-title {
|
||||
color: $text-color;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.lgoo-icon {
|
||||
vertical-align: middle;
|
||||
transition: all 0.3s ease;
|
||||
height: 36px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsed-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
.lgoo-icon-small {
|
||||
height: 36px;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.history-btn {
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
padding: 4px 12px;
|
||||
color: $text-color;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
color: #07c160;
|
||||
background: rgba(7, 193, 96, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
font-size: 14px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
color: $text-color;
|
||||
transition: all 0.3s ease;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
|
||||
&:hover {
|
||||
color: $primary-color;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mode-list {
|
||||
padding: 0 0 20px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mode-item {
|
||||
padding: 15px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: $text-color;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: $hover-color;
|
||||
|
||||
:deep(.setting-btn) {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $primary-color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.emoji-icon {
|
||||
font-size: 20px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mode-name {
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
:deep(.mode-setting) {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu__item) {
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
line-height: 1.4;
|
||||
max-width: 300px;
|
||||
overflow-x: hidden;
|
||||
|
||||
&:hover .delete-btn {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dropdown-menu) {
|
||||
min-width: 200px;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
.mode-header {
|
||||
height: 60px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mode-header-icon {
|
||||
height: 32px;
|
||||
will-change: filter;
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: filter 300ms;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
filter: drop-shadow(0 0 0.75em rgba(var(--el-color-primary-rgb), 0.6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 24px;
|
||||
min-width: 0;
|
||||
.mode-menu {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 8px;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
color: var(--el-text-color-regular);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 4px;
|
||||
padding: 0 12px !important;
|
||||
overflow: hidden;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-image: linear-gradient(135deg, var(--el-color-primary) 0%, var(--el-color-primary-light-3) 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.el-icon {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.is-active):hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.mode-name {
|
||||
font-size: 14px;
|
||||
margin-left: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
.mode-setting-btn {
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
transition: all 0.3s ease;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
padding: 0;
|
||||
color: #f56c6c;
|
||||
flex-shrink: 0;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c;
|
||||
background-color: rgba(245, 108, 108, 0.1);
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
font-size: 14px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端样式 */
|
||||
@media screen and (max-width: 768px) {
|
||||
.mode-selector {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
transform: translateX(-100%);
|
||||
z-index: 1000;
|
||||
|
||||
&.collapsed {
|
||||
transform: translateX(-100%);
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
|
||||
.collapsed-logo {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.mode-selector-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
z-index: 9;
|
||||
display: none;
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-selector {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
|
||||
&.collapsed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @media (prefers-color-scheme: dark) {
|
||||
// .mode-selector {
|
||||
// background: $background-color-dark;
|
||||
// border-right-color: $border-color-dark;
|
||||
.mode-selector.collapsed {
|
||||
.mode-menu {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
// .mode-title {
|
||||
// color: $text-color-dark;
|
||||
// }
|
||||
:deep(.el-menu-item) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 4px auto;
|
||||
|
||||
// .mode-item {
|
||||
// color: $text-color-dark;
|
||||
.el-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
// &:hover {
|
||||
// background: $hover-color-dark;
|
||||
// }
|
||||
// }
|
||||
.mode-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// .history-btn {
|
||||
// color: $text-color-dark;
|
||||
|
||||
// &:hover {
|
||||
// color: $primary-color;
|
||||
// background: rgba($primary-color, 0.15);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.settings-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
.mode-name,
|
||||
.button-text {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .delete-btn {
|
||||
// &:hover {
|
||||
// background-color: rgba(245, 108, 108, 0.15);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
.sidebar-footer {
|
||||
padding: 8px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
color: var(--el-text-color-regular);
|
||||
border-radius: 8px;
|
||||
padding: 0 12px !important;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: background-color 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 12px;
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
color: var(--el-text-color-regular);
|
||||
body:not(.no-animations) & {
|
||||
transition: color 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
.button-text {
|
||||
font-size: 14px;
|
||||
margin-left: 12px;
|
||||
white-space: nowrap;
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
|
||||
body:not(.no-animations) & {
|
||||
transition: opacity 0.3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mode-selector.collapsed {
|
||||
.sidebar-footer {
|
||||
padding: 8px 12px;
|
||||
.settings-button {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.mode-menu {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 4px auto;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.mode-name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-button {
|
||||
.mode-name,
|
||||
.button-text {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.settings-popover-popper {
|
||||
padding: 0 !important;
|
||||
min-width: 240px !important;
|
||||
width: auto !important;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="chat">
|
||||
<div class="chat-page">
|
||||
<div class="chat-container">
|
||||
<ChatModeSelector @mode-changed="handleModeChange" />
|
||||
<div class="chat-wrapper">
|
||||
|
@ -27,7 +27,7 @@ const handleModeChange = (mode) => {
|
|||
|
||||
|
||||
|
||||
<style>
|
||||
<style lang="scss" scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
@ -35,8 +35,8 @@ const handleModeChange = (mode) => {
|
|||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
.chat {
|
||||
height: 100%;
|
||||
.chat-page {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f5f5;
|
||||
|
|
Loading…
Reference in New Issue