feat(settings): 实现主题切换和显示模式功能
- 新增主题切换功能,支持多种主题颜色配置 - 实现显示模式切换,包括系统、深色和浅色模式 - 添加动画效果开关功能 - 优化设置面板组件,集中管理所有设置项 - 重构部分代码以支持新功能,包括 store、样式和组件
This commit is contained in:
parent
9d4d52260f
commit
f65dd67087
|
@ -5,11 +5,10 @@
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- Vue 3 - 渐进式 JavaScript 框架
|
- Vue 3 - 渐进式 JavaScript 框架
|
||||||
- Vite - 下一代前端构建工具
|
|
||||||
- Vue Router - 官方路由管理器
|
|
||||||
- Pinia - Vue 的官方状态管理库
|
|
||||||
- TypeScript - JavaScript 的超集
|
- 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"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"sass": "^1.86.3",
|
"@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",
|
"sass-embedded": "^1.86.3",
|
||||||
"vite": "^6.2.0",
|
"typescript": "^5.3.3",
|
||||||
"vite-plugin-mkcert": "^1.17.8"
|
"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>
|
<script setup>
|
||||||
import { RouterView } from "vue-router";
|
import { RouterView } from "vue-router";
|
||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
onMounted(() => {
|
import { useSettingsStore } from '@/store/settings';
|
||||||
|
import { setTheme } from '@/styles/theme';
|
||||||
|
|
||||||
})
|
const settingsStore = useSettingsStore();
|
||||||
onMounted(() => {
|
|
||||||
//首先我们获得视口高度并将其乘以1%以获得1vh单位的值
|
|
||||||
let vh = window.innerHeight * 0.01
|
|
||||||
// 然后,我们将——vh自定义属性中的值设置为文档的根
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`)
|
|
||||||
|
|
||||||
// 我们监听resize事件 视图大小发生变化就重新计算1vh的值
|
onMounted(() => {
|
||||||
window.addEventListener('resize', () => {
|
// 应用启动时,根据 store 中持久化的主题设置样式
|
||||||
// 我们执行与前面相同的脚本
|
setTheme(settingsStore.theme);
|
||||||
let vh = window.innerHeight * 0.01
|
});
|
||||||
console.log(vh);
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss">
|
||||||
.logo {
|
html, body, #app {
|
||||||
height: 6em;
|
height: 100%;
|
||||||
padding: 1.5em;
|
margin: 0;
|
||||||
will-change: filter;
|
padding: 0;
|
||||||
transition: filter 300ms;
|
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>
|
</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 { createApp } from "vue";
|
||||||
import "./style.css";
|
import "@/styles/style.css";
|
||||||
|
// 使用自定义主题
|
||||||
|
import '@/styles/element-variables.scss'
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import {router} from "@/router/index";
|
import {router} from "@/router/index";
|
||||||
import { createPinia } from "pinia";
|
import { createPinia } from "pinia";
|
||||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
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
|
// 引入 Element Plus
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
// 使用自定义主题
|
|
||||||
import './styles/element-variables.scss'
|
|
||||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
pinia.use(piniaPluginPersistedstate)
|
pinia.use(piniaPluginPersistedstate)
|
||||||
|
|
||||||
const app = createApp(App);
|
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(router);
|
||||||
app.use(ElementPlus, {
|
app.use(ElementPlus, {
|
||||||
locale: zhCn,
|
locale: zhCn,
|
||||||
|
|
|
@ -18,17 +18,11 @@ const routes = [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/chat",
|
path: "/chat",
|
||||||
component: () => import("@/layouts/MainLayout.vue"),
|
name: "chat",
|
||||||
children: [
|
meta: {
|
||||||
{
|
icon: ChatRound
|
||||||
path: "",
|
},
|
||||||
name: "chat",
|
component: () => import("@/views/chat/index.vue"),
|
||||||
meta: {
|
|
||||||
icon: ChatRound
|
|
||||||
},
|
|
||||||
component: () => import("@/views/chat/index.vue"),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,34 +1,40 @@
|
||||||
import { defineStore } from 'pinia'
|
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', {
|
export const useSettingsStore = defineStore('settings', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
// 侧边栏状态
|
// 侧边栏状态
|
||||||
sidebarCollapsed: false,
|
sidebarCollapsed: false,
|
||||||
|
// 显示模式: 'light', 'dark', 'system'
|
||||||
// 界面配置
|
displayMode: 'system', // Default to system preference
|
||||||
interface: {
|
// 主题配置 (e.g., 'default', 'fresh', 'warm')
|
||||||
// 是否显示时间戳
|
theme: 'default',
|
||||||
showTimestamp: true,
|
// 动画效果开关
|
||||||
// 消息气泡最大宽度(px)
|
animationsEnabled: true,
|
||||||
messageMaxWidth: 800,
|
|
||||||
// 字体大小(px)
|
|
||||||
fontSize: 14
|
|
||||||
},
|
|
||||||
|
|
||||||
// 主题配置
|
|
||||||
theme: {
|
|
||||||
// 主色调
|
|
||||||
primary: '#07c160',
|
|
||||||
// 背景色
|
|
||||||
background: '#f5f5f5',
|
|
||||||
// 文字颜色
|
|
||||||
textColor: '#333333'
|
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
persist: {
|
persist: {
|
||||||
key: 'settings',
|
key: 'settings',
|
||||||
paths: ['sidebarCollapsed']
|
// Persist displayMode and theme
|
||||||
|
paths: ['sidebarCollapsed', 'displayMode', 'theme', 'animationsEnabled']
|
||||||
},
|
},
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -37,25 +43,54 @@ export const useSettingsStore = defineStore('settings', {
|
||||||
this.sidebarCollapsed = !this.sidebarCollapsed
|
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新界面配置
|
// 切换动画状态
|
||||||
updateInterface(config) {
|
toggleAnimations(enabled) {
|
||||||
this.interface = {
|
if (typeof enabled === 'boolean') {
|
||||||
...this.interface,
|
this.animationsEnabled = enabled;
|
||||||
...config
|
} 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) {
|
updateDisplayMode(newMode) {
|
||||||
this.theme = {
|
if (['light', 'dark', 'system'].includes(newMode)) {
|
||||||
...this.theme,
|
this.displayMode = newMode;
|
||||||
...config
|
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() {
|
resetSettings() {
|
||||||
this.$reset()
|
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 {
|
:root {
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
@ -58,10 +77,6 @@ button:focus-visible {
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
/* max-width: 100vh; */
|
/* max-width: 100vh; */
|
||||||
height: 100%;
|
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="header-content">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="isMobile || settingsStore.sidebarCollapsed"
|
|
||||||
class="desktop-sidebar-toggle"
|
class="desktop-sidebar-toggle"
|
||||||
:icon="Expand"
|
:icon="Expand"
|
||||||
@click="settingsStore.toggleSidebar"
|
@click="settingsStore.toggleSidebar"
|
||||||
|
@ -128,7 +127,7 @@ onUnmounted(() => {
|
||||||
.chat-header {
|
.chat-header {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
border-bottom: 1px solid #e5e5e5;
|
border-bottom: 1px solid #e5e5e5;
|
||||||
background: #fff;
|
background: #FFF;
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
|
|
@ -709,7 +709,6 @@ onUnmounted(() => {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
border-top: 1px solid #e5e5e5;
|
border-top: 1px solid #e5e5e5;
|
||||||
z-index: 101;
|
|
||||||
|
|
||||||
:deep(.el-textarea) {
|
:deep(.el-textarea) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="mode-selector-container">
|
||||||
<div
|
<div
|
||||||
class="mode-selector-backdrop"
|
class="mode-selector-backdrop"
|
||||||
:class="{ visible: !settingsStore.sidebarCollapsed }"
|
:class="{ visible: !settingsStore.sidebarCollapsed }"
|
||||||
|
@ -8,57 +8,40 @@
|
||||||
></div>
|
></div>
|
||||||
<div class="mode-selector" :class="{ 'collapsed': settingsStore.sidebarCollapsed }">
|
<div class="mode-selector" :class="{ 'collapsed': settingsStore.sidebarCollapsed }">
|
||||||
<div class="mode-header">
|
<div class="mode-header">
|
||||||
<div class="mode-title" v-if="!settingsStore.sidebarCollapsed">
|
<div class="mode-header-title" v-if="!settingsStore.sidebarCollapsed">
|
||||||
<img
|
<img src="/lgogo.svg" alt="Logo" class="mode-header-icon" />
|
||||||
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>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -67,10 +50,9 @@
|
||||||
import { useChatStore } from '@/store/chat'
|
import { useChatStore } from '@/store/chat'
|
||||||
import { useSettingsStore } from '@/store/settings'
|
import { useSettingsStore } from '@/store/settings'
|
||||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { Delete, EditPen, RefreshRight, Expand, Fold } from '@element-plus/icons-vue'
|
import { ChatDotRound, ChatLineRound, ChatRound, Service, Setting } from '@element-plus/icons-vue'
|
||||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElMessageBox } from 'element-plus'
|
import { ElMenu, ElMenuItem, ElIcon, ElPopover } from 'element-plus'
|
||||||
import ModeSetting from '@/components/chat/ModeSetting.vue'
|
import SettingsPanel from '@/components/settings/SettingsPanel.vue'
|
||||||
import HistoryButton from '@/components/chat/HistoryButton.vue'
|
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
@ -79,7 +61,9 @@ const chatModes = computed(() => {
|
||||||
return Object.keys(chatStore.chatModes).map(key => ({
|
return Object.keys(chatStore.chatModes).map(key => ({
|
||||||
id: key,
|
id: key,
|
||||||
name: chatStore.chatModes[key].name,
|
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)
|
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) => {
|
const selectMode = (modeId) => {
|
||||||
chatStore.setCurrentMode(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 isMobile = ref(false)
|
||||||
|
|
||||||
const checkMobile = () => {
|
const checkMobile = () => {
|
||||||
|
@ -157,341 +93,275 @@ onUnmounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
$primary-color: #4CAF50;
|
.mode-selector-container {
|
||||||
$text-color: #333;
|
height: 100vh;
|
||||||
$text-color-dark: #fff;
|
position: relative;
|
||||||
$background-color: #F7F7F7;
|
}
|
||||||
$background-color-dark: #1e1e1e;
|
|
||||||
$border-color: #e0e0e0;
|
|
||||||
$border-color-dark: #333;
|
|
||||||
$hover-color: #f5f5f5;
|
|
||||||
$hover-color-dark: #2d2d2d;
|
|
||||||
|
|
||||||
.mode-selector {
|
.mode-selector {
|
||||||
width: 240px;
|
width: 240px;
|
||||||
min-width: 240px;
|
|
||||||
background: $background-color;
|
|
||||||
border-right: 1px solid $border-color;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
background-color: var(--el-bg-color);
|
||||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
border-right: 1px solid var(--el-border-color-light);
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
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 {
|
&.collapsed {
|
||||||
width: 64px;
|
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) {
|
.mode-header {
|
||||||
font-size: 13px;
|
height: 60px;
|
||||||
padding: 8px 16px;
|
padding: 0 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 {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
&-title {
|
||||||
width: 100%;
|
display: flex;
|
||||||
position: relative;
|
align-items: center;
|
||||||
min-width: 0;
|
|
||||||
|
.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 {
|
.mode-menu {
|
||||||
flex: 1;
|
background-color: transparent;
|
||||||
white-space: nowrap;
|
border: none;
|
||||||
overflow: hidden;
|
padding: 8px;
|
||||||
text-overflow: ellipsis;
|
flex-grow: 1;
|
||||||
margin-right: 24px;
|
overflow-y: auto;
|
||||||
min-width: 0;
|
|
||||||
|
: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;
|
position: absolute;
|
||||||
right: -8px;
|
right: 8px;
|
||||||
opacity: 0;
|
top: 50%;
|
||||||
transform: translateX(10px);
|
transform: translateY(-50%);
|
||||||
transition: all 0.3s ease;
|
color: var(--el-text-color-secondary);
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
padding: 0;
|
|
||||||
color: #f56c6c;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #f56c6c;
|
color: var(--el-color-primary);
|
||||||
background-color: rgba(245, 108, 108, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-icon) {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移动端样式 */
|
/* 移动端样式 */
|
||||||
@media screen and (max-width: 768px) {
|
@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 {
|
.mode-selector-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background-color: var(--el-overlay-color-lighter);
|
||||||
z-index: 999;
|
z-index: 9;
|
||||||
opacity: 0;
|
display: none;
|
||||||
visibility: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.visible {
|
&.visible {
|
||||||
opacity: 1;
|
display: block;
|
||||||
visibility: visible;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-selector {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
|
||||||
|
&.collapsed {
|
||||||
|
transform: translateX(-100%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// @media (prefers-color-scheme: dark) {
|
.mode-selector.collapsed {
|
||||||
// .mode-selector {
|
.mode-menu {
|
||||||
// background: $background-color-dark;
|
padding: 8px 12px;
|
||||||
// border-right-color: $border-color-dark;
|
}
|
||||||
|
|
||||||
// .mode-title {
|
:deep(.el-menu-item) {
|
||||||
// color: $text-color-dark;
|
width: 40px;
|
||||||
// }
|
height: 40px;
|
||||||
|
padding: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto 4px auto;
|
||||||
|
|
||||||
// .mode-item {
|
.el-icon {
|
||||||
// color: $text-color-dark;
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
// &:hover {
|
.mode-name {
|
||||||
// background: $hover-color-dark;
|
display: none;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .history-btn {
|
.settings-button {
|
||||||
// color: $text-color-dark;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
// &:hover {
|
.mode-name,
|
||||||
// color: $primary-color;
|
.button-text {
|
||||||
// background: rgba($primary-color, 0.15);
|
opacity: 0;
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// .delete-btn {
|
.sidebar-footer {
|
||||||
// &:hover {
|
padding: 8px;
|
||||||
// background-color: rgba(245, 108, 108, 0.15);
|
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>
|
</style>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="chat">
|
<div class="chat-page">
|
||||||
<div class="chat-container">
|
<div class="chat-container">
|
||||||
<ChatModeSelector @mode-changed="handleModeChange" />
|
<ChatModeSelector @mode-changed="handleModeChange" />
|
||||||
<div class="chat-wrapper">
|
<div class="chat-wrapper">
|
||||||
|
@ -27,7 +27,7 @@ const handleModeChange = (mode) => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style lang="scss" scoped>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 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;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat {
|
.chat-page {
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
|
|
Loading…
Reference in New Issue