Initial commit: Project structure and layout
This commit is contained in:
6
client/.dockerignore
Normal file
6
client/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.env
|
||||
.git
|
||||
.idea
|
||||
.vscode
|
||||
37
client/.env.example
Normal file
37
client/.env.example
Normal file
@@ -0,0 +1,37 @@
|
||||
# =====================================================
|
||||
# API Configuration
|
||||
# =====================================================
|
||||
VITE_API_URL=http://localhost:23337/api
|
||||
VITE_WS_URL=ws://localhost:23337
|
||||
|
||||
# =====================================================
|
||||
# Application Configuration
|
||||
# =====================================================
|
||||
VITE_APP_NAME=SillyTavern Replace
|
||||
VITE_APP_VERSION=1.0.0
|
||||
|
||||
# =====================================================
|
||||
# Feature Flags
|
||||
# =====================================================
|
||||
VITE_ENABLE_WEBSOCKET=true
|
||||
VITE_ENABLE_SSE=true
|
||||
|
||||
# =====================================================
|
||||
# LLM Configuration (Public - Safe to Expose)
|
||||
# =====================================================
|
||||
VITE_DEFAULT_LLM_PROVIDER=openai
|
||||
VITE_MAX_TOKENS=4096
|
||||
VITE_DEFAULT_TEMPERATURE=0.7
|
||||
|
||||
# =====================================================
|
||||
# UI Configuration
|
||||
# =====================================================
|
||||
VITE_THEME=light
|
||||
VITE_LANGUAGE=zh-CN
|
||||
VITE_ITEMS_PER_PAGE=20
|
||||
|
||||
# =====================================================
|
||||
# Upload Configuration
|
||||
# =====================================================
|
||||
VITE_MAX_UPLOAD_SIZE=10485760
|
||||
VITE_ALLOWED_FILE_TYPES=.json,.csv,.txt,.md,.png,.jpg
|
||||
17
client/Dockerfile.dev
Normal file
17
client/Dockerfile.dev
Normal file
@@ -0,0 +1,17 @@
|
||||
# Development Dockerfile for Frontend (Vue3 + Vite)
|
||||
FROM node:20-alpine
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Expose Vite dev server port
|
||||
EXPOSE 5173
|
||||
|
||||
# Default command (will be overridden by docker-compose)
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
|
||||
42
client/README.md
Normal file
42
client/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# client
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
1
client/env.d.ts
vendored
Normal file
1
client/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
13
client/index.html
Normal file
13
client/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<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>
|
||||
3148
client/package-lock.json
generated
Normal file
3148
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
client/package.json
Normal file
33
client/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "client",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"vue": "^3.5.32",
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node24": "^24.0.4",
|
||||
"@types/node": "^24.12.2",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"@vue/tsconfig": "^0.9.1",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"typescript": "~6.0.0",
|
||||
"vite": "^8.0.8",
|
||||
"vite-plugin-vue-devtools": "^8.1.1",
|
||||
"vue-tsc": "^3.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
BIN
client/public/favicon.ico
Normal file
BIN
client/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
41
client/src/App.vue
Normal file
41
client/src/App.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<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>
|
||||
86
client/src/assets/base.css
Normal file
86
client/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;
|
||||
}
|
||||
1
client/src/assets/logo.svg
Normal file
1
client/src/assets/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 276 B |
35
client/src/assets/main.css
Normal file
35
client/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;
|
||||
}
|
||||
}
|
||||
72
client/src/components/features/center/MessageItem.vue
Normal file
72
client/src/components/features/center/MessageItem.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<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>
|
||||
21
client/src/components/features/left/ApiConfigView.vue
Normal file
21
client/src/components/features/left/ApiConfigView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
21
client/src/components/features/left/GalleryView.vue
Normal file
21
client/src/components/features/left/GalleryView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
21
client/src/components/features/left/PresetView.vue
Normal file
21
client/src/components/features/left/PresetView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
21
client/src/components/features/left/WorldBookView.vue
Normal file
21
client/src/components/features/left/WorldBookView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
21
client/src/components/features/right/DiceVice.vue
Normal file
21
client/src/components/features/right/DiceVice.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
21
client/src/components/features/right/RagView.vue
Normal file
21
client/src/components/features/right/RagView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
21
client/src/components/features/right/WorldBookCallView.vue
Normal file
21
client/src/components/features/right/WorldBookCallView.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
175
client/src/components/layout/CenterChat.vue
Normal file
175
client/src/components/layout/CenterChat.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<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>
|
||||
99
client/src/components/layout/LeftPanel.vue
Normal file
99
client/src/components/layout/LeftPanel.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<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>
|
||||
267
client/src/components/layout/RightPanel.vue
Normal file
267
client/src/components/layout/RightPanel.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<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>
|
||||
18
client/src/components/layout/RightToolbar.vue
Normal file
18
client/src/components/layout/RightToolbar.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
125
client/src/components/layout/TopToolbar.vue
Normal file
125
client/src/components/layout/TopToolbar.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<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>
|
||||
51
client/src/composables/useApi.ts
Normal file
51
client/src/composables/useApi.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
14
client/src/main.ts
Normal file
14
client/src/main.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
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')
|
||||
19
client/tsconfig.app.json
Normal file
19
client/tsconfig.app.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
// Extra safety for array and object lookups, but may have false positives.
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Path mapping for cleaner imports.
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@shared/*": ["../shared/types/*"]
|
||||
},
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
11
client/tsconfig.json
Normal file
11
client/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
client/tsconfig.node.json
Normal file
27
client/tsconfig.node.json
Normal file
@@ -0,0 +1,27 @@
|
||||
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
|
||||
{
|
||||
"extends": "@tsconfig/node24/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
// Most tools use transpilation instead of Node.js's native type-stripping.
|
||||
// Bundler mode provides a smoother developer experience.
|
||||
"module": "preserve",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
// Include Node.js types and avoid accidentally including other `@types/*` packages.
|
||||
"types": ["node"],
|
||||
|
||||
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
|
||||
"noEmit": true,
|
||||
|
||||
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
|
||||
// Specified here to keep it out of the root directory.
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
|
||||
}
|
||||
}
|
||||
23
client/vite.config.ts
Normal file
23
client/vite.config.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('../shared/types', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 8080,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user