refactor: 重构项目结构和代码组织
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
<template>
|
||||
<div class="app-layout">
|
||||
<!-- 顶部工具栏 -->
|
||||
<TopToolbar />
|
||||
|
||||
<!-- 主体内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 左侧面板 -->
|
||||
<LeftPanel />
|
||||
|
||||
<!-- 中间聊天区 -->
|
||||
<CenterChat />
|
||||
|
||||
<!-- 右侧工具面板 -->
|
||||
<RightPanel />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TopToolbar from './components/layout/TopToolbar.vue';
|
||||
import LeftPanel from './components/layout/LeftPanel.vue';
|
||||
import CenterChat from './components/layout/CenterChat.vue';
|
||||
import RightPanel from './components/layout/RightPanel.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
0
client/src/api/index.ts
Normal file
0
client/src/api/index.ts
Normal file
@@ -1,86 +0,0 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
0
client/src/assets/logo.png
Normal file
0
client/src/assets/logo.png
Normal file
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
Before Width: | Height: | Size: 276 B |
@@ -1,35 +0,0 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
||||
0
client/src/components/CenterPanel/CenterPanel.vue
Normal file
0
client/src/components/CenterPanel/CenterPanel.vue
Normal file
0
client/src/components/CenterPanel/center-panel.css
Normal file
0
client/src/components/CenterPanel/center-panel.css
Normal file
0
client/src/components/CenterPanel/useCenterPanel.ts
Normal file
0
client/src/components/CenterPanel/useCenterPanel.ts
Normal file
0
client/src/components/LeftPanel/LeftPanel.vue
Normal file
0
client/src/components/LeftPanel/LeftPanel.vue
Normal file
0
client/src/components/LeftPanel/left-panel.css
Normal file
0
client/src/components/LeftPanel/left-panel.css
Normal file
0
client/src/components/LeftPanel/useLeftPanel.ts
Normal file
0
client/src/components/LeftPanel/useLeftPanel.ts
Normal file
0
client/src/components/RightPanel/RightPanel.vue
Normal file
0
client/src/components/RightPanel/RightPanel.vue
Normal file
0
client/src/components/RightPanel/right-panel.css
Normal file
0
client/src/components/RightPanel/right-panel.css
Normal file
0
client/src/components/RightPanel/useRightPanel.ts
Normal file
0
client/src/components/RightPanel/useRightPanel.ts
Normal file
0
client/src/components/TopBar/TopBar.vue
Normal file
0
client/src/components/TopBar/TopBar.vue
Normal file
0
client/src/components/TopBar/top-bar.css
Normal file
0
client/src/components/TopBar/top-bar.css
Normal file
0
client/src/components/TopBar/useTopBar.ts
Normal file
0
client/src/components/TopBar/useTopBar.ts
Normal file
0
client/src/components/common/BaseIcon/BaseIcon.vue
Normal file
0
client/src/components/common/BaseIcon/BaseIcon.vue
Normal file
0
client/src/components/common/BaseIcon/base-icon.css
Normal file
0
client/src/components/common/BaseIcon/base-icon.css
Normal file
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<div :class="['message-item', message.senderRole]">
|
||||
<div class="message-header">
|
||||
<span class="sender-name">{{ message.senderName }}</span>
|
||||
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="message-content">{{ message.mes }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Message {
|
||||
id: string;
|
||||
senderName: string;
|
||||
senderRole: 'user' | 'assistant' | 'system';
|
||||
mes: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
message: Message;
|
||||
}>();
|
||||
|
||||
function formatTime(date: Date) {
|
||||
return new Date(date).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-item {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.message-item.user {
|
||||
background: #e3f2fd;
|
||||
border-color: #bbdefb;
|
||||
margin-left: 20%;
|
||||
}
|
||||
|
||||
.message-item.assistant {
|
||||
background: #f1f8e9;
|
||||
border-color: #dcedc8;
|
||||
margin-right: 20%;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="api-config-view">
|
||||
<h3>🔌 API 配置</h3>
|
||||
<p>API 配置管理界面</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.api-config-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="gallery-view">
|
||||
<h3>🖼️ 画廊</h3>
|
||||
<p>LLM 生成的图片将显示在这里</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.gallery-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="preset-view">
|
||||
<h3>⚙️ 预设管理</h3>
|
||||
<p>预设配置和条目展示</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.preset-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="world-book-view">
|
||||
<h3>📚 世界书管理</h3>
|
||||
<p>全局世界书激活和条目管理</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.world-book-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="dice-view">
|
||||
<h3>🎲 骰子工具</h3>
|
||||
<p>纯前端骰子功能</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dice-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="rag-view">
|
||||
<h3>🔍 RAG 检索</h3>
|
||||
<p>RAG 知识检索功能</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,21 +0,0 @@
|
||||
<template>
|
||||
<div class="world-book-call-view">
|
||||
<h3>📚 世界书调用</h3>
|
||||
<p>世界书条目调用记录</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.world-book-call-view {
|
||||
padding: 8px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 14px;
|
||||
color: #2c3e50;
|
||||
margin: 0 0 12px 0;
|
||||
}
|
||||
p {
|
||||
font-size: 13px;
|
||||
color: #7f8c8d;
|
||||
}
|
||||
</style>
|
||||
@@ -1,175 +0,0 @@
|
||||
<template>
|
||||
<main class="center-chat">
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list">
|
||||
<CenterMessageItem
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
:message="msg"
|
||||
/>
|
||||
</div>
|
||||
<!-- 输入区域 -->
|
||||
<div class="input-area">
|
||||
<!-- 选项框 -->
|
||||
<div class="input-options">
|
||||
<select v-model="sendOptions.role" class="option-select">
|
||||
<option value="user">用户</option>
|
||||
<option value="assistant">AI</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<textarea
|
||||
v-model="inputMessage"
|
||||
class="message-input"
|
||||
placeholder="输入消息..."
|
||||
@keydown.enter.ctrl="sendMessage"
|
||||
></textarea>
|
||||
|
||||
<!-- 发送/停止按钮 -->
|
||||
<button
|
||||
:class="['send-btn', { sending: isSending }]"
|
||||
@click="isSending ? stopGeneration() : sendMessage()"
|
||||
>
|
||||
{{ isSending ? '⏹ 停止' : '📤 发送' }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import CenterMessageItem from '../features/center/MessageItem.vue';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
senderName: string;
|
||||
senderRole: 'user' | 'assistant' | 'system';
|
||||
mes: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const messages = ref<Message[]>([
|
||||
{
|
||||
id: '1',
|
||||
senderName: 'Assistant',
|
||||
senderRole: 'assistant',
|
||||
mes: '你好!有什么可以帮助你的吗?',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
]);
|
||||
|
||||
const inputMessage = ref('');
|
||||
const isSending = ref(false);
|
||||
const sendOptions = ref({
|
||||
role: 'user' as 'user' | 'assistant',
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
if (!inputMessage.value.trim()) return;
|
||||
|
||||
const newMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
senderName: sendOptions.value.role === 'user' ? 'User' : 'Assistant',
|
||||
senderRole: sendOptions.value.role,
|
||||
mes: inputMessage.value,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
|
||||
messages.value.push(newMessage);
|
||||
inputMessage.value = '';
|
||||
isSending.value = true;
|
||||
|
||||
// 模拟 AI 回复
|
||||
setTimeout(() => {
|
||||
isSending.value = false;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
function stopGeneration() {
|
||||
isSending.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.center-chat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
background: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-top: 2px solid #dee2e6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-options {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
resize: none;
|
||||
font-size: 14px;
|
||||
min-height: 60px;
|
||||
max-height: 200px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.send-btn.sending {
|
||||
background: #e74c3c;
|
||||
}
|
||||
|
||||
.send-btn.sending:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<aside class="left-panel">
|
||||
<!-- 左侧-顶部分页标签 -->
|
||||
<div class="panel-tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:class="['tab-btn', { active: currentTab === tab.id }]"
|
||||
@click="currentTab = tab.id"
|
||||
>
|
||||
{{ tab.icon }} {{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 左侧-内容区域 -->
|
||||
<div class="panel-content">
|
||||
<LeftGalleryView v-if="currentTab === 'gallery'" />
|
||||
<LeftApiConfigView v-if="currentTab === 'api'" />
|
||||
<LeftPresetView v-if="currentTab === 'preset'" />
|
||||
<LeftWorldBookView v-if="currentTab === 'worldbook'" />
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import LeftGalleryView from '../features/left/GalleryView.vue';
|
||||
import LeftApiConfigView from '../features/left/ApiConfigView.vue';
|
||||
import LeftPresetView from '../features/left/PresetView.vue';
|
||||
import LeftWorldBookView from '../features/left/WorldBookView.vue';
|
||||
|
||||
const currentTab = ref('gallery');
|
||||
|
||||
const tabs = [
|
||||
{ id: 'gallery', label: '画廊', icon: '🖼️' },
|
||||
{ id: 'api', label: 'API', icon: '🔌' },
|
||||
{ id: 'preset', label: '预设', icon: '⚙️' },
|
||||
{ id: 'worldbook', label: '世界书', icon: '📚' },
|
||||
];
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25%;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
background: #ffffff;
|
||||
border-right: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 8px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #7f8c8d;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: #ffffff;
|
||||
border-color: #e0e0e0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #ffffff;
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
box-shadow: 0 1px 3px rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
@@ -1,267 +0,0 @@
|
||||
<template>
|
||||
<aside class="right-panel">
|
||||
<!-- 右侧-顶部分页标签 -->
|
||||
<div class="panel-tabs">
|
||||
<button
|
||||
v-for="tool in availableToolsList"
|
||||
:key="tool.id"
|
||||
:class="['tab-btn', { active: isToolActive(tool.id) }]"
|
||||
@click="selectTool(tool.id)"
|
||||
>
|
||||
{{ tool.icon }} {{ tool.shortLabel }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 右侧-上部分区(最新使用的工具) -->
|
||||
<div class="panel-section top-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">{{ recentTools[0]?.label || '未选择工具' }}</span>
|
||||
<button class="collapse-btn" @click="toggleTop" title="折叠/展开">
|
||||
{{ isTopCollapsed ? '▼' : '▲' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="!isTopCollapsed" class="section-content">
|
||||
<component :is="recentTools[0]?.component" v-if="recentTools[0]" />
|
||||
<EmptyState v-else message="点击上方标签选择工具" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧-下部分区(次新使用的工具) -->
|
||||
<div class="panel-section bottom-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">{{ recentTools[1]?.label || '未选择工具' }}</span>
|
||||
<button class="collapse-btn" @click="toggleBottom" title="折叠/展开">
|
||||
{{ isBottomCollapsed ? '▼' : '▲' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-show="!isBottomCollapsed" class="section-content">
|
||||
<component :is="recentTools[1]?.component" v-if="recentTools[1]" />
|
||||
<EmptyState v-else message="点击上方标签选择工具" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import RightToolDiceView from '../features/right/DiceView.vue';
|
||||
import RightToolRagView from '../features/right/RagView.vue';
|
||||
import RightToolWorldBookCallView from '../features/right/WorldBookCallView.vue';
|
||||
import RightToolDynamicTableView from '../features/right/DynamicTableView.vue';
|
||||
import EmptyState from '../shared/EmptyState.vue';
|
||||
|
||||
interface ToolItem {
|
||||
id: string;
|
||||
label: string;
|
||||
shortLabel: string;
|
||||
icon: string;
|
||||
component: any;
|
||||
}
|
||||
|
||||
const isTopCollapsed = ref(false);
|
||||
const isBottomCollapsed = ref(false);
|
||||
const recentTools = ref<ToolItem[]>([]);
|
||||
|
||||
// 所有可用工具
|
||||
const availableTools: Record<string, ToolItem> = {
|
||||
dice: {
|
||||
id: 'dice',
|
||||
label: '🎲 骰子工具',
|
||||
shortLabel: '骰子',
|
||||
icon: '🎲',
|
||||
component: RightToolDiceView,
|
||||
},
|
||||
rag: {
|
||||
id: 'rag',
|
||||
label: '🔍 RAG 检索',
|
||||
shortLabel: 'RAG',
|
||||
icon: '🔍',
|
||||
component: RightToolRagView,
|
||||
},
|
||||
worldbook: {
|
||||
id: 'worldbook',
|
||||
label: '📚 世界书调用',
|
||||
shortLabel: '世界书',
|
||||
icon: '📚',
|
||||
component: RightToolWorldBookCallView,
|
||||
},
|
||||
table: {
|
||||
id: 'table',
|
||||
label: '📊 动态表格',
|
||||
shortLabel: '表格',
|
||||
icon: '📊',
|
||||
component: RightToolDynamicTableView,
|
||||
},
|
||||
};
|
||||
|
||||
// 获取工具列表(用于渲染标签)
|
||||
const availableToolsList = Object.values(availableTools);
|
||||
|
||||
// 判断工具是否在当前显示的两个中
|
||||
function isToolActive(toolId: string): boolean {
|
||||
return recentTools.value.some((tool) => tool.id === toolId);
|
||||
}
|
||||
|
||||
// 选择工具(添加到最近使用队列)
|
||||
function selectTool(toolId: string) {
|
||||
const tool = availableTools[toolId];
|
||||
if (!tool) return;
|
||||
|
||||
// 移除已存在的相同工具
|
||||
recentTools.value = recentTools.value.filter((t) => t.id !== toolId);
|
||||
|
||||
// 添加到最前面
|
||||
recentTools.value.unshift(tool);
|
||||
|
||||
// 只保留最近两个
|
||||
if (recentTools.value.length > 2) {
|
||||
recentTools.value = recentTools.value.slice(0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTop() {
|
||||
isTopCollapsed.value = !isTopCollapsed.value;
|
||||
}
|
||||
|
||||
function toggleBottom() {
|
||||
isBottomCollapsed.value = !isBottomCollapsed.value;
|
||||
}
|
||||
|
||||
// 暴露方法供外部调用
|
||||
defineExpose({
|
||||
selectTool,
|
||||
});
|
||||
|
||||
// 初始化默认工具
|
||||
selectTool('dice');
|
||||
selectTool('worldbook');
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.right-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 25%;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
background: #ffffff;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding: 6px 8px;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
min-width: 60px;
|
||||
padding: 8px 6px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
color: #7f8c8d;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
background: #ffffff;
|
||||
border-color: #e0e0e0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
background: #ffffff;
|
||||
border-color: #3498db;
|
||||
color: #3498db;
|
||||
box-shadow: 0 1px 3px rgba(52, 152, 219, 0.1);
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.panel-section:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 16px;
|
||||
background: #ffffff;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.collapse-btn {
|
||||
padding: 4px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #7f8c8d;
|
||||
transition: color 0.2s;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.collapse-btn:hover {
|
||||
color: #2c3e50;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.panel-tabs::-webkit-scrollbar,
|
||||
.section-content::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.panel-tabs::-webkit-scrollbar-track,
|
||||
.section-content::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.panel-tabs::-webkit-scrollbar-thumb,
|
||||
.section-content::-webkit-scrollbar-thumb {
|
||||
background: #d0d0d0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.panel-tabs::-webkit-scrollbar-thumb:hover,
|
||||
.section-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #b0b0b0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +0,0 @@
|
||||
<template>
|
||||
<div class="right-toolbar">
|
||||
<span style="color: #bdc3c7; font-size: 12px;">工具栏</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
rightPanelRef?: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.right-toolbar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,125 +0,0 @@
|
||||
<template>
|
||||
<header class="top-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<!-- API 配置快速切换 -->
|
||||
<div class="toolbar-item">
|
||||
<span class="label">API:</span>
|
||||
<select v-model="currentApi" class="toolbar-select">
|
||||
<option value="primary">主 API</option>
|
||||
<option value="secondary">副 API</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-center">
|
||||
<!-- 当前用户角色 -->
|
||||
<div class="toolbar-item">
|
||||
<span class="label">用户:</span>
|
||||
<span class="value">{{ currentUser }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 当前 AI 角色 -->
|
||||
<div class="toolbar-item">
|
||||
<span class="label">AI:</span>
|
||||
<span class="value">{{ currentAICharacter }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 激活的全局世界书 -->
|
||||
<div class="toolbar-item">
|
||||
<span class="label">世界书:</span>
|
||||
<span class="value">{{ activeWorldBooks.length }} 个激活</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-right">
|
||||
<!-- 设置按钮 -->
|
||||
<button class="toolbar-btn" @click="openSettings">
|
||||
⚙️ 设置
|
||||
</button>
|
||||
|
||||
<!-- 拓展按钮 -->
|
||||
<button class="toolbar-btn" @click="openExtensions">
|
||||
🧩 拓展
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const currentApi = ref('primary');
|
||||
const currentUser = ref('User');
|
||||
const currentAICharacter = ref('Assistant');
|
||||
const activeWorldBooks = ref(['奇幻世界', '现代都市']);
|
||||
|
||||
function openSettings() {
|
||||
console.log('Open settings');
|
||||
}
|
||||
|
||||
function openExtensions() {
|
||||
console.log('Open extensions');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.top-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
border-bottom: 2px solid #34495e;
|
||||
height: 50px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-left,
|
||||
.toolbar-center,
|
||||
.toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toolbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toolbar-select {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #34495e;
|
||||
background: #34495e;
|
||||
color: white;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 6px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
</style>
|
||||
@@ -1,51 +0,0 @@
|
||||
/**
|
||||
* useApi Composable
|
||||
* Provides API client with error handling and loading states
|
||||
*/
|
||||
|
||||
import { ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
|
||||
interface UseApiOptions<T> {
|
||||
immediate?: boolean;
|
||||
onError?: (error: Error) => void;
|
||||
onSuccess?: (data: T) => void;
|
||||
}
|
||||
|
||||
export function useApi<T>(
|
||||
apiCall: () => Promise<T>,
|
||||
options: UseApiOptions<T> = {}
|
||||
) {
|
||||
const data: Ref<T | null> = ref(null);
|
||||
const error: Ref<Error | null> = ref(null);
|
||||
const loading: Ref<boolean> = ref(false);
|
||||
|
||||
const execute = async () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const result = await apiCall();
|
||||
data.value = result;
|
||||
options.onSuccess?.(result);
|
||||
return result;
|
||||
} catch (err) {
|
||||
error.value = err instanceof Error ? err : new Error(String(err));
|
||||
options.onError?.(error.value);
|
||||
throw error.value;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
if (options.immediate) {
|
||||
execute();
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
execute,
|
||||
};
|
||||
}
|
||||
0
client/src/composables/useTheme.ts
Normal file
0
client/src/composables/useTheme.ts
Normal file
0
client/src/layouts/MainLayout/MainLayout.vue
Normal file
0
client/src/layouts/MainLayout/MainLayout.vue
Normal file
0
client/src/layouts/MainLayout/main-layout.css
Normal file
0
client/src/layouts/MainLayout/main-layout.css
Normal file
@@ -1,14 +0,0 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
0
client/src/router/index.ts
Normal file
0
client/src/router/index.ts
Normal file
0
client/src/stores/useAppStore.ts
Normal file
0
client/src/stores/useAppStore.ts
Normal file
0
client/src/styles/reset.css
Normal file
0
client/src/styles/reset.css
Normal file
0
client/src/styles/variables.css
Normal file
0
client/src/styles/variables.css
Normal file
Reference in New Issue
Block a user