mirror of
				https://github.com/Kakune55/PyGetGPT.git
				synced 2025-11-04 13:44:40 +08:00 
			
		
		
		
	feat:完成简易的的前端和后端api
This commit is contained in:
		
							
								
								
									
										3
									
								
								frontend/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/env.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
/// <reference types="vite/client" />
 | 
			
		||||
 | 
			
		||||
declare module "*.vue"
 | 
			
		||||
							
								
								
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="/favicon.ico">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Vite App</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.ts"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										1861
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1861
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										30
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								frontend/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "frontend",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "run-p type-check \"build-only {@}\" --",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "build-only": "vite build",
 | 
			
		||||
    "type-check": "vue-tsc --build --force"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^1.7.7",
 | 
			
		||||
    "element-plus": "^2.8.5",
 | 
			
		||||
    "marked": "^14.1.3",
 | 
			
		||||
    "vue": "^3.5.11",
 | 
			
		||||
    "vue-router": "^4.4.5"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@tsconfig/node20": "^20.1.4",
 | 
			
		||||
    "@types/node": "^20.16.11",
 | 
			
		||||
    "@vitejs/plugin-vue": "^5.1.4",
 | 
			
		||||
    "@vue/tsconfig": "^0.5.1",
 | 
			
		||||
    "npm-run-all2": "^6.2.3",
 | 
			
		||||
    "typescript": "~5.5.4",
 | 
			
		||||
    "vite": "^5.4.8",
 | 
			
		||||
    "vue-tsc": "^2.1.6"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								frontend/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										34
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								frontend/src/App.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { RouterView } from 'vue-router'
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="chat-container">
 | 
			
		||||
    <RouterView />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* 聊天框容器样式 */
 | 
			
		||||
.chat-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  height: 90vh; /* 高度占据视口的 90% */
 | 
			
		||||
  width: 90vw;  /* 宽度占据视口的 90% */
 | 
			
		||||
  margin: auto;
 | 
			
		||||
  background-color: #f5f5f5;
 | 
			
		||||
  border-radius: 10px;
 | 
			
		||||
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
 | 
			
		||||
  overflow: hidden; /* 防止内容溢出 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 聊天框内部内容样式 */
 | 
			
		||||
.chat-content {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  overflow-y: auto; /* 允许垂直滚动 */
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										86
									
								
								frontend/src/assets/base.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								frontend/src/assets/base.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								frontend/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								frontend/src/assets/main.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
@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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								frontend/src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/main.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import './assets/main.css'
 | 
			
		||||
 | 
			
		||||
import { createApp } from 'vue';
 | 
			
		||||
import App from './App.vue';
 | 
			
		||||
import router from './router';
 | 
			
		||||
// import ElementPlus from 'element-plus';
 | 
			
		||||
// import 'element-plus/dist/index.css';
 | 
			
		||||
 | 
			
		||||
const app = createApp(App);
 | 
			
		||||
 | 
			
		||||
app.use(router);
 | 
			
		||||
// app.use(ElementPlus);
 | 
			
		||||
 | 
			
		||||
app.mount('#app');
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										29
									
								
								frontend/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/router/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
import { createRouter, createWebHistory } from 'vue-router';
 | 
			
		||||
import ChatPage from '../views/ChatPage.vue';
 | 
			
		||||
import LoginPage from '../views/LoginPage.vue';
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
  { path: '/', redirect: '/chat' },
 | 
			
		||||
  { path: '/login', component: LoginPage },
 | 
			
		||||
  { path: '/chat', component: ChatPage }, // 聊天页面
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(),
 | 
			
		||||
  routes,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 路由守卫,确保用户登录后才能访问聊天页面
 | 
			
		||||
router.beforeEach((to, from, next) => {
 | 
			
		||||
  const isAuthenticated = document.cookie.includes('auth_token'); // 假设后端返回的是这个Cookie
 | 
			
		||||
  if (to.path === '/chat' && !isAuthenticated) {
 | 
			
		||||
    next('/login'); // 未登录时跳转到登录页面
 | 
			
		||||
  } else {
 | 
			
		||||
    next(); // 允许进入
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default router;
 | 
			
		||||
							
								
								
									
										340
									
								
								frontend/src/views/ChatPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										340
									
								
								frontend/src/views/ChatPage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,340 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="chat-app">
 | 
			
		||||
        <!-- 左侧对话列表 -->
 | 
			
		||||
        <aside :class="['conversation-list', { 'collapsed': isCollapsed }]">
 | 
			
		||||
            <div v-if="!isCollapsed" v-for="(conversation, index) in conversations" :key="index"
 | 
			
		||||
                class="conversation-item" @click="selectConversation(index)"
 | 
			
		||||
                :class="{ 'active': index === selectedConversation }">
 | 
			
		||||
                <span>对话 {{ index + 1 }}</span>
 | 
			
		||||
                <button @click.stop="deleteConversation(index)" class="delete-btn">删除</button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <button v-if="!isCollapsed" @click="newConversation" class="new-conversation-btn">新建对话</button>
 | 
			
		||||
            <button @click="toggleCollapse" class="collapse-btn">{{ isCollapsed ? '>' : '折叠' }}</button>
 | 
			
		||||
        </aside>
 | 
			
		||||
 | 
			
		||||
        <!-- 聊天内容区域 -->
 | 
			
		||||
        <section class="chat-container">
 | 
			
		||||
            <!-- 模型选择 -->
 | 
			
		||||
            <div class="model-selection">
 | 
			
		||||
                <select v-model="selectedModel" @change="fetchMessages" class="model-dropdown">
 | 
			
		||||
                    <option v-for="model in models" :key="model.id" :value="model.id">
 | 
			
		||||
                        {{ model.name }}
 | 
			
		||||
                    </option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 聊天窗口 -->
 | 
			
		||||
            <div ref="chatWindow" class="chat-window">
 | 
			
		||||
                <div v-for="(message, index) in conversations[selectedConversation].messages" :key="index"
 | 
			
		||||
                    :class="['message', message.sender]">
 | 
			
		||||
                    <div class="message-bubble" v-html="renderMarkdown(message.text)"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <!-- 输入框 -->
 | 
			
		||||
            <div class="input-area">
 | 
			
		||||
                <input v-model="userInput" @keyup.enter="sendMessage" placeholder="请输入您的消息..." class="chat-input" />
 | 
			
		||||
                <button @click="sendMessage" class="send-btn">发送</button>
 | 
			
		||||
                <button @click="clearHistory" class="clear-btn">清除记录</button>
 | 
			
		||||
            </div>
 | 
			
		||||
        </section>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            userInput: '',
 | 
			
		||||
            conversations: JSON.parse(localStorage.getItem('conversations')) || [
 | 
			
		||||
                {
 | 
			
		||||
                    messages: [{ sender: 'bot', text: '你好!请选择一个模型开始聊天。' }],
 | 
			
		||||
                },
 | 
			
		||||
            ],
 | 
			
		||||
            selectedConversation: 0,
 | 
			
		||||
            models: [],
 | 
			
		||||
            selectedModel: '',
 | 
			
		||||
            isCollapsed: false, // 控制菜单折叠的状态
 | 
			
		||||
            conversationLimit: 6, // 对话数量上限
 | 
			
		||||
        };
 | 
			
		||||
    },
 | 
			
		||||
    created() {
 | 
			
		||||
        this.fetchModels();
 | 
			
		||||
    },
 | 
			
		||||
    methods: {
 | 
			
		||||
        async fetchModels() {
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await axios.get('/api/chat/models');
 | 
			
		||||
                this.models = response.data.models;
 | 
			
		||||
                this.selectedModel = this.models[0].id;
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('获取模型列表时出错:', error);
 | 
			
		||||
                this.conversations[this.selectedConversation].messages.push({ sender: 'bot', text: '获取模型列表失败,请稍后再试。' });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        async sendMessage() {
 | 
			
		||||
            if (this.userInput.trim() === '' || !this.selectedModel) return;
 | 
			
		||||
 | 
			
		||||
            const currentMessages = this.conversations[this.selectedConversation].messages;
 | 
			
		||||
            currentMessages.push({ sender: 'user', text: this.userInput });
 | 
			
		||||
            currentMessages.push({ sender: 'bot', text: '正在思考...' });
 | 
			
		||||
 | 
			
		||||
            const userMessage = this.userInput;
 | 
			
		||||
            this.userInput = '';
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const response = await axios.post('/api/chat', {
 | 
			
		||||
                    message: userMessage,
 | 
			
		||||
                    model: this.selectedModel,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                currentMessages.pop();
 | 
			
		||||
                currentMessages.push({ sender: 'bot', text: response.data.message });
 | 
			
		||||
                this.saveConversations();
 | 
			
		||||
                this.scrollToBottom();
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
                console.error('发送消息出错:', error);
 | 
			
		||||
                currentMessages.push({ sender: 'bot', text: '抱歉!出现了错误。' });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        renderMarkdown(text) {
 | 
			
		||||
            return marked(text);
 | 
			
		||||
        },
 | 
			
		||||
        scrollToBottom() {
 | 
			
		||||
            const chatWindow = this.$refs.chatWindow;
 | 
			
		||||
            chatWindow.scrollTop = chatWindow.scrollHeight;
 | 
			
		||||
        },
 | 
			
		||||
        saveConversations() {
 | 
			
		||||
            localStorage.setItem('conversations', JSON.stringify(this.conversations));
 | 
			
		||||
        },
 | 
			
		||||
        loadConversation() {
 | 
			
		||||
            this.scrollToBottom();
 | 
			
		||||
        },
 | 
			
		||||
        clearHistory() {
 | 
			
		||||
            this.conversations[this.selectedConversation].messages = [];
 | 
			
		||||
            this.saveConversations();
 | 
			
		||||
        },
 | 
			
		||||
        newConversation() {
 | 
			
		||||
            if (this.conversations.length >= this.conversationLimit) {
 | 
			
		||||
                alert(`对话数量已达到上限(${this.conversationLimit}条)。`);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            this.conversations.push({
 | 
			
		||||
                messages: [{ sender: 'bot', text: '新对话已创建,请选择一个模型开始聊天。' }],
 | 
			
		||||
            });
 | 
			
		||||
            this.selectedConversation = this.conversations.length - 1;
 | 
			
		||||
            this.saveConversations();
 | 
			
		||||
        },
 | 
			
		||||
        deleteConversation(index) {
 | 
			
		||||
            if (this.conversations.length === 1) {
 | 
			
		||||
                alert('至少需要保留一个对话。');
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            if (confirm('确定要删除这个对话吗?')) {
 | 
			
		||||
                this.conversations.splice(index, 1);
 | 
			
		||||
                if (this.selectedConversation === index) {
 | 
			
		||||
                    this.selectedConversation = 0;
 | 
			
		||||
                }
 | 
			
		||||
                this.saveConversations();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        selectConversation(index) {
 | 
			
		||||
            this.selectedConversation = index;
 | 
			
		||||
            this.scrollToBottom();
 | 
			
		||||
        },
 | 
			
		||||
        toggleCollapse() {
 | 
			
		||||
            this.isCollapsed = !this.isCollapsed;
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    mounted() {
 | 
			
		||||
        this.scrollToBottom();
 | 
			
		||||
    },
 | 
			
		||||
    updated() {
 | 
			
		||||
        this.scrollToBottom();
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* 全局容器样式 */
 | 
			
		||||
.chat-app {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    height: 90vh;
 | 
			
		||||
    width: 90vw;
 | 
			
		||||
    margin: auto;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 左侧对话列表 */
 | 
			
		||||
.conversation-list {
 | 
			
		||||
    width: 20%;
 | 
			
		||||
    background-color: #f9f9f9;
 | 
			
		||||
    border-right: 1px solid #ddd;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    transition: width 0.3s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.conversation-list.collapsed {
 | 
			
		||||
    width: 50px;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.conversation-item {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    background-color: #fff;
 | 
			
		||||
    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.conversation-item.active {
 | 
			
		||||
    background-color: #007bff;
 | 
			
		||||
    color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.new-conversation-btn {
 | 
			
		||||
    background-color: #28a745;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    margin-top: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.new-conversation-btn:hover {
 | 
			
		||||
    background-color: #218838;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.delete-btn {
 | 
			
		||||
    background-color: #dc3545;
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 5px 10px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.delete-btn:hover {
 | 
			
		||||
    background-color: #c82333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.collapse-btn {
 | 
			
		||||
    margin-top: auto;
 | 
			
		||||
    padding: 5px 10px;
 | 
			
		||||
    background-color: #007bff;
 | 
			
		||||
    color: white;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.collapse-btn:hover {
 | 
			
		||||
    background-color: #0056b3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 右侧聊天窗口 */
 | 
			
		||||
.chat-container {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.model-selection {
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    background-color: #f1f1f1;
 | 
			
		||||
    border-bottom: 1px solid #ddd;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.model-dropdown {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    padding: 5px;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
    border: 1px solid #ddd;
 | 
			
		||||
    outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-window {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    padding: 20px;
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
    background-color: #fff;
 | 
			
		||||
    border-bottom: 1px solid #ddd;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message.user {
 | 
			
		||||
    justify-content: flex-end;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message.bot {
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message-bubble {
 | 
			
		||||
    max-width: 60%;
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    border-radius: 10px;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message.user .message-bubble {
 | 
			
		||||
    background-color: #007bff;
 | 
			
		||||
    color: white;
 | 
			
		||||
    border-bottom-right-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.message.bot .message-bubble {
 | 
			
		||||
    background-color: #f1f1f1;
 | 
			
		||||
    color: black;
 | 
			
		||||
    border-bottom-left-radius: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.input-area {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
    background-color: #f1f1f1;
 | 
			
		||||
    border-top: 1px solid #ddd;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.chat-input {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    padding: 10px;
 | 
			
		||||
    font-size: 14px;
 | 
			
		||||
    border: 1px solid #ddd;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.send-btn,
 | 
			
		||||
.clear-btn {
 | 
			
		||||
    background-color: #007bff;
 | 
			
		||||
    color: white;
 | 
			
		||||
    padding: 10px 15px;
 | 
			
		||||
    margin-left: 10px;
 | 
			
		||||
    border: none;
 | 
			
		||||
    border-radius: 20px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.send-btn:hover,
 | 
			
		||||
.clear-btn:hover {
 | 
			
		||||
    background-color: #0056b3;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										136
									
								
								frontend/src/views/LoginPage.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								frontend/src/views/LoginPage.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,136 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="login-container">
 | 
			
		||||
    <div class="login-box">
 | 
			
		||||
      <h2>登录</h2>
 | 
			
		||||
      <form @submit.prevent="handleLogin">
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <label for="userinfo">UID或邮箱</label>
 | 
			
		||||
          <input
 | 
			
		||||
            type="text"
 | 
			
		||||
            id="userinfo"
 | 
			
		||||
            v-model="loginForm.userinfo"
 | 
			
		||||
            placeholder="请输入用户名"
 | 
			
		||||
            class="form-input"
 | 
			
		||||
            required
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <label for="password">密码</label>
 | 
			
		||||
          <input
 | 
			
		||||
            type="password"
 | 
			
		||||
            id="password"
 | 
			
		||||
            v-model="loginForm.password"
 | 
			
		||||
            placeholder="请输入密码"
 | 
			
		||||
            class="form-input"
 | 
			
		||||
            required
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-group">
 | 
			
		||||
          <button type="submit" class="login-btn">登录</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </form>
 | 
			
		||||
      <p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  data() {
 | 
			
		||||
    return {
 | 
			
		||||
      loginForm: {
 | 
			
		||||
        userinfo: '',
 | 
			
		||||
        password: '',
 | 
			
		||||
      },
 | 
			
		||||
      errorMessage: '',
 | 
			
		||||
    };
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    async handleLogin() {
 | 
			
		||||
      try {
 | 
			
		||||
        const response = await axios.post('/api/user/login', this.loginForm);
 | 
			
		||||
        if (response.status === 200) {
 | 
			
		||||
          // 登录成功,跳转到聊天页面
 | 
			
		||||
          this.$router.push('/chat');
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        // 登录失败,显示错误信息
 | 
			
		||||
        this.errorMessage = '登录失败,请检查用户名或密码';
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.login-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  height: 100vh;
 | 
			
		||||
  background-color: #f5f5f5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-box {
 | 
			
		||||
  background-color: #fff;
 | 
			
		||||
  padding: 40px;
 | 
			
		||||
  border-radius: 10px;
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  max-width: 350px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h2 {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  color: #333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-group {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
label {
 | 
			
		||||
  display: block;
 | 
			
		||||
  margin-bottom: 8px;
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  color: #333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-input {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  border: 1px solid #ddd;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.form-input:focus {
 | 
			
		||||
  outline: none;
 | 
			
		||||
  border-color: #007bff;
 | 
			
		||||
  box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-btn {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  padding: 10px;
 | 
			
		||||
  background-color: #007bff;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  border: none;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.login-btn:hover {
 | 
			
		||||
  background-color: #0056b3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.error-message {
 | 
			
		||||
  color: #ff4d4f;
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										14
									
								
								frontend/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/tsconfig.app.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "@vue/tsconfig/tsconfig.dom.json",
 | 
			
		||||
  "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
 | 
			
		||||
  "exclude": ["src/**/__tests__/*"],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
 | 
			
		||||
 | 
			
		||||
    "baseUrl": ".",
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/tsconfig.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "files": [],
 | 
			
		||||
  "references": [
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.node.json"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "path": "./tsconfig.app.json"
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								frontend/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/tsconfig.node.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "@tsconfig/node20/tsconfig.json",
 | 
			
		||||
  "include": [
 | 
			
		||||
    "vite.config.*",
 | 
			
		||||
    "vitest.config.*",
 | 
			
		||||
    "cypress.config.*",
 | 
			
		||||
    "nightwatch.conf.*",
 | 
			
		||||
    "playwright.config.*"
 | 
			
		||||
  ],
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "composite": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
 | 
			
		||||
 | 
			
		||||
    "module": "ESNext",
 | 
			
		||||
    "moduleResolution": "Bundler",
 | 
			
		||||
    "types": ["node"]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								frontend/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/vite.config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { fileURLToPath, URL } from 'node:url'
 | 
			
		||||
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import vue from '@vitejs/plugin-vue'
 | 
			
		||||
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    vue(),
 | 
			
		||||
  ],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': fileURLToPath(new URL('./src', import.meta.url))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
		Reference in New Issue
	
	Block a user