mirror of
https://github.com/Kakune55/PyGetGPT.git
synced 2025-09-15 03:39:31 +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