Compare commits

...

9 Commits

Author SHA1 Message Date
Lexcubia 995eb85166 refactor(settings): 将设置面板拆分为多个组件
- 新增 DisplayModeSettings、ThemeSettings 和 InterfaceSettings 组件
- 重构 SettingsPanel 组件,使用新组件替换原有的设置项
- 删除 ThemeSwitcher 组件
- 更新样式和布局,优化用户体验
2025-04-22 16:43:32 +08:00
Lexcubia f65dd67087 feat(settings): 实现主题切换和显示模式功能
- 新增主题切换功能,支持多种主题颜色配置
- 实现显示模式切换,包括系统、深色和浅色模式
- 添加动画效果开关功能
- 优化设置面板组件,集中管理所有设置项
- 重构部分代码以支持新功能,包括 store、样式和组件
2025-04-22 16:36:54 +08:00
Lexcubia 9d4d52260f update:调整聊天头部的历史按钮显示逻辑,确保在侧边栏未收起时也能正常显示 2025-04-21 19:15:37 +08:00
Lexcubia ca8532815a update:替换网站图标为新设计的 logo.svg,删除旧的 vite.svg 文件,调整聊天界面样式以增强移动端适配,修改聊天模式选择器的布局和样式,优化主布局以隐藏侧边栏组件 2025-04-21 19:12:10 +08:00
Lexcubia fcd3cbdee4 update: 修改 package.json 中的开发和生产模式脚本,添加 --host 参数以支持主机设置 2025-04-21 16:47:06 +08:00
Lexcubia 9f55780796 update: 修改聊天模式的token值,确保与新配置一致,增强系统安全性 2025-04-21 16:45:28 +08:00
Lexcubia d6decd1979 update: 在配置文件中添加服务器端口设置,更新聊天模式的客户信息背景,增强对话场景的真实感 2025-04-21 16:30:44 +08:00
Lexcubia 6537775767 update: 在代理设置中添加对聊天消息的支持,设置相应的请求和响应头以处理事件流 2025-04-21 14:37:57 +08:00
Lexcubia e6832941d5 update: 在项目中添加 vite-plugin-mkcert 插件,配置 HTTPS 和 CORS,优化代理设置,调整构建选项以支持手动分块和压缩设置 2025-04-21 14:17:50 +08:00
35 changed files with 2692 additions and 868 deletions

View File

@ -5,11 +5,10 @@
## 技术栈
- Vue 3 - 渐进式 JavaScript 框架
- Vite - 下一代前端构建工具
- Vue Router - 官方路由管理器
- Pinia - Vue 的官方状态管理库
- TypeScript - JavaScript 的超集
- Tailwind CSS - 实用优先的 CSS 框架
- Element Plus - 基于 Vue 3 的组件库
- Vite - 下一代前端构建工具
- Sass - CSS 预处理器
## 功能特性

BIN
dist.zip Normal file

Binary file not shown.

View File

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<title>对话</title>
</head>

View File

@ -4,8 +4,8 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode development",
"dev:test": "vite --mode test",
"dev": "vite --mode development --host",
"dev:prod": "vite --mode production --host",
"build": "vite build",
"build:dev": "vite build --mode development",
"build:prod": "vite build --mode production",
@ -48,9 +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"
"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"
}
}

12
postcss.config.js Normal file
View File

@ -0,0 +1,12 @@
export default {
plugins: {
'postcss-import': {},
'postcss-preset-env': {
stage: 3,
features: {
'nesting-rules': true
}
},
autoprefixer: {}
}
}

6
public/lgogo.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="169" height="51" viewBox="0 0 169 51" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M81.76 17.5466C79.9529 15.6083 77.7663 14.0627 75.3361 13.0059C72.906 11.9491 70.2844 11.4037 67.6344 11.4037C64.9844 11.4037 62.3628 11.9491 59.9327 13.0059C57.5025 14.0627 55.3159 15.6083 53.5088 17.5466C50.1929 21.2167 48.3801 26.0007 48.4312 30.9467C48.4823 35.8926 50.3936 40.6381 53.7847 44.2389C55.5931 46.0736 57.7481 47.5306 60.1245 48.525C62.501 49.5195 65.0514 50.0316 67.6275 50.0316C70.2036 50.0316 72.754 49.5195 75.1304 48.525C77.5069 47.5306 79.6619 46.0736 81.4703 44.2389C84.8662 40.633 86.7812 35.8815 86.8349 30.9286C86.8885 25.9757 85.0769 21.1838 81.76 17.5052V17.5466ZM67.6206 18.5536C69.1876 18.5355 70.7408 18.848 72.1788 19.4707C73.6169 20.0935 74.9074 21.0125 75.9663 22.1677C77.1207 23.3101 78.0294 24.6765 78.6366 26.1828C79.2439 27.6891 79.5369 29.3036 79.4977 30.9272C79.5096 33.0868 78.9581 35.2121 77.8975 37.0934C76.8989 38.9229 75.4143 40.441 73.6074 41.48C71.7797 42.4933 69.7242 43.025 67.6344 43.025C65.5446 43.025 63.4891 42.4933 61.6614 41.48C59.8361 40.4511 58.3356 38.9315 57.3299 37.0934C56.2651 35.2138 55.7132 33.0874 55.7297 30.9272C55.6917 29.3062 55.9826 27.6943 56.5848 26.1887C57.187 24.6832 58.0881 23.3154 59.2335 22.1677C60.297 21.0072 61.5943 20.0852 63.0399 19.4622C64.4855 18.8393 66.0467 18.5296 67.6206 18.5536V18.5536Z" fill="#A4CE8A"/>
<path d="M160.596 35.7139C159.687 37.375 158.528 38.887 157.161 40.1971C156.059 41.1327 154.791 41.8533 153.423 42.3214C151.967 42.8683 150.425 43.1487 148.87 43.1491C147.374 43.1859 145.886 42.9063 144.505 42.3287C143.124 41.7511 141.881 40.8884 140.856 39.797C138.949 37.8115 137.785 35.2295 137.559 32.486H168.114V31.1065C168.193 26.5874 166.733 22.1756 163.975 18.5949C162.215 16.2968 159.934 14.4503 157.32 13.2078C154.705 11.9654 151.833 11.3626 148.939 11.4494C146.119 11.3651 143.32 11.9558 140.774 13.1723C138.228 14.3888 136.01 16.1959 134.303 18.4432C131.398 22.1416 129.882 26.7426 130.02 31.444C130.159 36.1455 131.943 40.6491 135.062 44.1699C136.822 46.1556 139.004 47.7216 141.449 48.7529C143.893 49.7841 146.538 50.2544 149.188 50.1291C151.662 50.1596 154.121 49.7389 156.444 48.8876C158.592 48.0633 160.563 46.8366 162.251 45.2734C164.11 43.503 165.637 41.4148 166.762 39.1073L167.41 37.8382L161.244 34.5965L160.596 35.7139ZM159.892 25.8508H138.469C139.101 24.1268 140.132 22.5765 141.477 21.3262C143.566 19.4332 146.3 18.4115 149.119 18.4707C150.915 18.4687 152.69 18.8594 154.319 19.6157C155.833 20.2951 157.169 21.3148 158.223 22.5953C158.955 23.581 159.519 24.681 159.892 25.8508V25.8508Z" fill="#A4CE8A"/>
<path d="M123.405 20.2917C122.394 17.8965 120.689 15.8593 118.508 14.4428C116.26 12.983 113.629 12.2243 110.949 12.2633C108.289 12.2483 105.67 12.908 103.334 14.1807C101.981 14.9318 100.733 15.8596 99.6237 16.9396V0H92.354V50.074H99.6237V36.1416C99.5151 33.1638 99.7092 30.1827 100.203 27.2441C100.766 24.9664 102.054 22.9331 103.872 21.4504C105.587 19.955 107.791 19.1403 110.066 19.1605C111.75 19.0711 113.414 19.5576 114.784 20.54C116.057 21.6551 116.917 23.1664 117.225 24.8301C117.627 27.3147 117.775 29.8336 117.667 32.3481V50.074H124.923V31.1066C124.923 26.2233 124.44 22.6781 123.405 20.2917Z" fill="#45886C"/>
<path d="M26.0716 1.15869H23.1196L0 50.074H8.33187L24.568 15.7671L40.7351 50.074H48.8877L26.0716 1.15869Z" fill="#45886C"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

8
public/lgoo.svg Normal file
View File

@ -0,0 +1,8 @@
<svg width="1931" height="978" viewBox="0 0 1931 978" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M355.325 541.386H318.177C282.033 387.562 263.966 230.046 264.34 72.034C264.34 66.758 264.34 61.2666 264.34 56.4213L272.524 21.5348L247.651 0L221.809 21.5348L229.562 56.4213C229.562 61.5896 229.562 67.1887 229.562 72.034C229.936 230.046 211.868 387.562 175.724 541.386H139.115L148.483 588.009H164.526C160.327 603.298 156.128 618.48 151.713 633.447H215.349C217.825 618.373 220.302 603.191 222.563 588.009H272.631C274.892 603.191 277.369 618.373 279.845 633.447H343.05C338.635 618.48 334.436 603.298 330.345 588.009H346.28L355.325 541.386ZM228.7 541.386C237.745 471.074 243.99 398.394 247.651 324.207C250.773 398.502 257.018 471.074 266.171 541.386H228.7Z" fill="#A4CE8A"/>
<path d="M367.169 977.143H494.332C462.616 926.646 434.654 873.887 410.669 819.293H442.218L452.232 762.333H387.627C373.091 725.401 359.632 686.961 347.465 647.014H282.86C289.751 686.315 297.504 724.862 306.118 762.333H189.184C197.905 724.862 205.658 686.315 212.549 647.014H147.944C135.777 686.853 122.318 725.401 107.782 762.333H42.3159L52.1143 819.293H83.7705C59.7079 873.869 31.7107 926.625 0 977.143H127.271C145.073 926.895 161.116 874.278 175.401 819.293H318.931C333.287 874.206 349.367 926.823 367.169 977.143Z" fill="#45886C"/>
<path d="M1256.67 723.247C1242.56 708.118 1225.49 696.054 1206.52 687.805C1187.56 679.556 1167.09 675.299 1146.41 675.299C1125.72 675.299 1105.26 679.556 1086.29 687.805C1067.32 696.054 1050.25 708.118 1036.15 723.247C1010.27 751.895 996.116 789.237 996.515 827.843C996.914 866.449 1011.83 903.491 1038.3 931.597C1052.42 945.918 1069.24 957.29 1087.79 965.053C1106.34 972.815 1126.25 976.812 1146.35 976.812C1166.46 976.812 1186.37 972.815 1204.92 965.053C1223.47 957.29 1240.29 945.918 1254.4 931.597C1280.91 903.451 1295.86 866.363 1296.28 827.702C1296.7 789.042 1282.56 751.638 1256.67 722.924V723.247ZM1146.3 731.108C1158.53 730.966 1170.65 733.406 1181.88 738.267C1193.1 743.128 1203.18 750.301 1211.44 759.318C1220.45 768.236 1227.55 778.9 1232.29 790.658C1237.03 802.416 1239.31 815.018 1239.01 827.691C1239.1 844.548 1234.8 861.138 1226.52 875.822C1218.72 890.102 1207.13 901.952 1193.03 910.062C1178.76 917.972 1162.72 922.122 1146.41 922.122C1130.09 922.122 1114.05 917.972 1099.78 910.062C1085.54 902.031 1073.82 890.169 1065.97 875.822C1057.66 861.151 1053.36 844.553 1053.48 827.691C1053.19 815.038 1055.46 802.456 1060.16 790.705C1064.86 778.953 1071.89 768.276 1080.83 759.318C1089.13 750.26 1099.26 743.063 1110.54 738.2C1121.83 733.338 1134.01 730.921 1146.3 731.108Z" fill="#A4CE8A"/>
<path d="M1872.02 865.054C1864.93 878.02 1855.88 889.822 1845.21 900.048C1836.61 907.352 1826.71 912.976 1816.03 916.63C1804.67 920.899 1792.64 923.087 1780.5 923.091C1768.82 923.378 1757.21 921.195 1746.43 916.687C1735.65 912.178 1725.94 905.444 1717.94 896.926C1703.06 881.427 1693.97 861.273 1692.21 839.858H1930.71V829.091C1931.32 793.817 1919.93 759.38 1898.4 731.43C1884.67 713.492 1866.86 699.079 1846.45 689.381C1826.04 679.683 1803.62 674.978 1781.04 675.655C1759.02 674.998 1737.17 679.608 1717.3 689.104C1697.43 698.6 1680.11 712.705 1666.8 730.246C1644.11 759.115 1632.28 795.028 1633.36 831.725C1634.45 868.423 1648.37 903.577 1672.72 931.058C1686.45 946.558 1703.49 958.781 1722.57 966.831C1741.65 974.881 1762.29 978.552 1782.98 977.574C1802.29 977.811 1821.48 974.527 1839.61 967.883C1856.38 961.449 1871.77 951.874 1884.94 939.672C1899.45 925.853 1911.38 909.553 1920.15 891.542L1925.21 881.636L1877.08 856.333L1872.02 865.054ZM1866.53 788.067H1699.31C1704.25 774.61 1712.29 762.509 1722.79 752.75C1739.09 737.974 1760.44 729.999 1782.44 730.461C1796.46 730.445 1810.31 733.495 1823.03 739.398C1834.84 744.702 1845.27 752.661 1853.5 762.656C1859.22 770.35 1863.62 778.936 1866.53 788.067Z" fill="#A4CE8A"/>
<path d="M1581.73 744.675C1573.84 725.979 1560.53 710.077 1543.51 699.021C1525.96 687.626 1505.42 681.704 1484.5 682.008C1463.74 681.891 1443.3 687.04 1425.07 696.975C1414.5 702.838 1404.76 710.079 1396.1 718.51V586.286H1339.36V977.143H1396.1V868.392C1395.26 845.149 1396.77 821.879 1400.63 798.942C1405.02 781.163 1415.07 765.293 1429.27 753.719C1442.65 742.046 1459.85 735.687 1477.61 735.845C1490.75 735.147 1503.74 738.945 1514.44 746.613C1524.37 755.317 1531.08 767.113 1533.5 780.099C1536.63 799.493 1537.78 819.155 1536.94 838.782V977.143H1593.58V829.091C1593.58 790.974 1589.81 763.302 1581.73 744.675Z" fill="#45886C"/>
<path d="M821.985 595.33H798.942L618.48 977.143H683.516L810.248 709.357L936.442 977.143H1000.08L821.985 595.33Z" fill="#45886C"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

27
public/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 676 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,82 @@
<template>
<div>
<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>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useSettingsStore } from '@/store/settings';
import { ElRadioGroup, ElRadioButton } from 'element-plus';
const settingsStore = useSettingsStore();
const currentDisplayMode = ref(settingsStore.displayMode);
const changeDisplayMode = (newMode) => {
settingsStore.updateDisplayMode(newMode);
currentDisplayMode.value = newMode;
};
</script>
<style lang="scss" scoped>
.display-mode-group {
display: flex;
width: 100%;
margin-bottom: 5px;
gap: 8px;
.el-radio-button {
flex: 1;
:deep(.el-radio-button__inner) {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px !important;
border: 1px solid var(--el-border-color-lighter);
box-shadow: none;
padding: 8px 5px;
font-size: 12px;
.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; }
}
}
&.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;
}
}
.el-radio-button + .el-radio-button {
margin-left: 0;
}
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div>
<h4>界面设置</h4>
<div class="setting-item">
<span>启用动画效果</span>
<el-switch
v-model="animationsEnabled"
@change="toggleAnimationsSetting"
/>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useSettingsStore } from '@/store/settings';
import { ElSwitch } from 'element-plus';
const settingsStore = useSettingsStore();
const animationsEnabled = ref(settingsStore.animationsEnabled);
const toggleAnimationsSetting = (value) => {
settingsStore.toggleAnimations(value);
animationsEnabled.value = value;
};
</script>
<style lang="scss" scoped>
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
font-size: 13px;
color: var(--el-text-color-regular);
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<div class="settings-popover">
<DisplayModeSettings />
<el-divider />
<ThemeSettings />
<el-divider />
<InterfaceSettings />
</div>
</template>
<script setup>
import { ElDivider } from 'element-plus';
import DisplayModeSettings from './DisplayModeSettings.vue';
import ThemeSettings from './ThemeSettings.vue';
import InterfaceSettings from './InterfaceSettings.vue';
</script>
<style lang="scss" scoped>
.settings-popover {
padding: 15px;
min-width: 240px;
h4 {
margin-top: 0;
margin-bottom: 12px;
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.el-divider {
margin: 18px 0;
}
}
</style>

View File

@ -0,0 +1,87 @@
<template>
<div>
<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>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { useSettingsStore } from '@/store/settings';
import { themes as availableThemes } from '@/styles/theme';
const settingsStore = useSettingsStore();
const currentTheme = ref(settingsStore.theme);
const filteredThemes = computed(() => {
const { dark, ...rest } = availableThemes;
return rest;
});
const themeLabels = {
default: '默认',
fresh: '清新',
warm: '暖色',
business: '商务',
};
const getThemeLabel = (themeName) => {
return themeLabels[themeName] || themeName;
};
const changeTheme = (newTheme) => {
settingsStore.updateTheme(newTheme);
currentTheme.value = newTheme;
};
</script>
<style lang="scss" scoped>
.theme-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(60px, 1fr));
gap: 10px;
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;
border-radius: 4px;
margin-bottom: 5px;
border: 1px solid var(--el-border-color-lighter);
}
.theme-label {
font-size: 12px;
color: var(--el-text-color-regular);
display: block;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="main-layout">
<Sidebar class="sidebar" />
<!-- <Sidebar class="sidebar" /> -->
<main class="content-area">
<!-- 路由视图将渲染在这里 -->
<router-view />

View File

@ -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,

View File

@ -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"),
},
];

View File

@ -8,39 +8,44 @@ const chatModes = {
token: 'app-88ae2GN49aUyNO6qGg7tbTfX',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**张伟
**年龄**34
**职业**腾讯员工
**家庭**妻子为全职太太女儿 9 双方父母健在
**性格**冷静理性务实偏好客观数据和逻辑分析对保险持观望态度防备心较强
**近期状态**
- 体检显示中度脂肪肝担心投保问题
- 已为全家配置百万医疗险妻子有重疾险但认为保障不足 保费 50W
- 对理财型保险收益不认可但对补充重疾险有潜在需求 `,
chatBackground: '通过顾先生介绍,你和他的老同学在他的家里首次面谈。他的家中布置简洁,茶几上摆放着一套茶具。',
},
quote_objection: {
name: '报价中异议',
icon: '💬',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX',
background: '',
chatBackground: '',
token: 'app-ur2Altw2LHR6niX8Q1S7Cn41',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**: 王力
**年龄**: 40
**职业**: 私营企业主
**车牌号**: 闽C12345
**车辆信息**拥有一辆行驶4年的大众途观L购买价格约25万主要用于商务出行及家庭使用
**投保信息**在其他保司购买交强险及商业险三者险车损险还有1个月到期有过一次轻微追尾事故已通过保司处理王先生对成本控制非常敏感但也能意识到保险的重要性他希望得到保障全面价格合理的产品和优质的服务`,
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价中的各类异议开展对练,着重训练处理“报价中异议”的能力。按照“保全保足”原则给出险种推荐方案。',
},
post_quote_objection: {
name: '报价后异议',
icon: '💰',
token: 'app-88ae2GN49aUyNO6qGg7tbTfX',
background: '',
chatBackground: '',
token: 'app-Yiccl0JoXs2QF2lkHxO6f822',
background: `# 客户信息
### **基础背景与性格设定**
**姓名**: 张琳女士
**年龄**: 32
**职业**: 公司行政主管
**车牌号**: 粤B56789
**车辆信息**拥有一辆行驶4年多的本田思域1.5TCVT燃动版购买价格约15万用于日常通勤及周末短途出行
**投保信息**在其他保司购买交强险及商业险三者险车损险不计免赔险车上人员责任险还有2个月到期无事故记录仅有两次违章停车记录张女士注重性价比和服务质量希望保险的保障全面价格合理服务贴心`,
chatBackground: '现在你将扮演坐席专员,与系统扮演的客户针对报价后的各类异议开展对练,着重训练处理“报价后异议”的能力。按照“保全保足”原则给出险种推荐方案。',
}
}
// 返回示例

View File

@ -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) { ... }

View File

@ -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%;

38
src/styles/theme.js Normal file
View File

@ -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}`);
}

View File

@ -0,0 +1,7 @@
export default {
primary: '#2C3E50',
success: '#27AE60',
warning: '#F39C12',
danger: '#E74C3C',
info: '#3498DB'
};

View File

@ -0,0 +1,7 @@
export default {
primary: '#5CACEE',
success: '#85CE61',
warning: '#E6A23C',
danger: '#F78989',
info: '#A6A9AD'
};

View File

@ -0,0 +1,7 @@
export default {
primary: '#409EFF',
success: '#67C23A',
warning: '#E6A23C',
danger: '#F56C6C',
info: '#909399'
};

View File

@ -0,0 +1,7 @@
export default {
primary: '#36D1DC',
success: '#5CB85C',
warning: '#F0AD4E',
danger: '#D9534F',
info: '#5BC0DE'
};

View File

@ -0,0 +1,7 @@
export default {
primary: '#FF6B6B',
success: '#4ECDC4',
warning: '#FFE66D',
danger: '#FF6B6B',
info: '#45B7D1'
};

View File

@ -3,13 +3,13 @@
<div class="header-content">
<div class="title">
<el-button
class="mobile-sidebar-toggle"
class="desktop-sidebar-toggle"
:icon="Expand"
@click="settingsStore.toggleSidebar"
text
v-if="isMobile"
/>
<span class="title-text">{{ title }}</span>
<el-tag v-if="chatStore.currentConversation?.conversationStatus === 'typing'"
size="small"
class="status-tag"
@ -17,9 +17,9 @@
>
对话中
</el-tag>
<HistoryButton v-if="settingsStore.sidebarCollapsed" :icon-only="false" class="title-history-btn" />
</div>
<div class="actions">
<HistoryButton :icon-only="false" class="title-history-btn" />
<el-tooltip content="删除会话" placement="bottom" v-if="chatStore.conversationId">
<el-button
class="action-btn delete-btn"
@ -29,7 +29,7 @@
>
</el-button>
</el-tooltip>
<el-tooltip content="设置" placement="bottom">
<!-- <el-tooltip content="设置" placement="bottom">
<el-button
class="action-btn"
:icon="Setting"
@ -37,7 +37,7 @@
text
>
</el-button>
</el-tooltip>
</el-tooltip> -->
<el-tooltip :content="hasBackground ? '客户背景' : '暂无客户背景信息'" placement="bottom" v-if="hasBackground">
<el-button
class="action-btn"
@ -127,7 +127,7 @@ onUnmounted(() => {
.chat-header {
height: 60px;
border-bottom: 1px solid #e5e5e5;
background: #fff;
background: #FFF;
padding: 0 20px;
flex-shrink: 0;
@ -186,6 +186,34 @@ onUnmounted(() => {
padding-top: 1px;
}
.desktop-sidebar-toggle {
height: 24px;
width: 24px;
padding: 0;
color: #666;
transition: all 0.3s ease;
border: none;
background: transparent !important;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
color: #07c160;
background: transparent !important;
}
&:focus {
outline: none;
box-shadow: none;
}
:deep(.el-icon) {
font-size: 18px;
margin-top: 1px;
}
}
.status-tag {
font-weight: normal;
flex-shrink: 0;

View File

@ -552,7 +552,7 @@ onUnmounted(() => {
line-height: 1.5;
font-size: 16px;
padding: 8px 12px;
border-radius: 4px;
border-radius: 6px;
white-space: pre-wrap;
position: relative;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
@ -709,7 +709,6 @@ onUnmounted(() => {
padding: 16px;
background: #f5f5f5;
border-top: 1px solid #e5e5e5;
z-index: 101;
:deep(.el-textarea) {
flex: 1;

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="mode-selector-container">
<div
class="mode-selector-backdrop"
:class="{ visible: !settingsStore.sidebarCollapsed }"
@ -8,43 +8,40 @@
></div>
<div class="mode-selector" :class="{ 'collapsed': settingsStore.sidebarCollapsed }">
<div class="mode-header">
<h3 class="mode-title">对话场景</h3>
<div class="header-actions">
<HistoryButton v-if="!settingsStore.sidebarCollapsed" :icon-only="true" />
<el-button
class="collapse-btn"
:icon="settingsStore.sidebarCollapsed ? Expand : 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>
@ -53,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()
@ -65,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
}))
})
@ -74,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 = () => {
@ -143,306 +93,275 @@ onUnmounted(() => {
</script>
<style lang="scss" scoped>
$primary-color: #4CAF50;
$text-color: #333;
$text-color-dark: #fff;
$background-color: #ffffff;
$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-title {
display: none;
}
.mode-item {
padding: 15px;
justify-content: center;
.mode-name {
display: none;
}
}
.header-actions {
width: 100%;
justify-content: center;
}
}
.mode-header {
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
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;
}
.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;
}
.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;
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(-240px);
}
&: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>

View File

@ -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;

View File

@ -5,6 +5,7 @@ import vue from '@vitejs/plugin-vue'
// import electron from 'vite-plugin-electron'
// import vueDevTools from 'vite-plugin-vue-devtools'
import mkcert from "vite-plugin-mkcert";
// https://vite.dev/config/
export default defineConfig(({ command, mode }) => {
const env = loadEnv(mode, process.cwd())
@ -18,6 +19,11 @@ export default defineConfig(({ command, mode }) => {
return {
plugins: [
vue(),
mkcert({
source: 'coding',
autoUpgrade: true,
force: true,
}),
// electron({
// entry: 'electron/main.js',
// }),
@ -36,12 +42,49 @@ export default defineConfig(({ command, mode }) => {
}
},
server: {
host: true,
port: 31003,
https: true,
cors: true,
proxy: {
'/api': {
target: env.VITE_APP_BASE_API,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
secure: false
secure: false,
configure: (proxy, options) => {
proxy.on('proxyReq', (proxyReq, req, res) => {
const targetOrigin = new URL(env.VITE_APP_BASE_API).origin;
proxyReq.setHeader('Origin', targetOrigin);
if (req.originalUrl && req.originalUrl.includes('chat-messages')) {
proxyReq.setHeader('Accept', 'text/event-stream');
}
});
proxy.on('proxyRes', (proxyRes, req, res) => {
if (req.originalUrl && req.originalUrl.includes('chat-messages')) {
proxyRes.headers['content-type'] = 'text/event-stream';
proxyRes.headers['Cache-Control'] = 'no-cache';
proxyRes.headers['Connection'] = 'keep-alive';
}
});
}
}
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia']
}
}
},
chunkSizeWarningLimit: 1500,
terserOptions: {
compress: {
// drop_console: true,
drop_debugger: true
}
}
},

2046
yarn.lock

File diff suppressed because it is too large Load Diff