规定数据类型

This commit is contained in:
2026-04-24 01:45:29 +08:00
parent ab860c61d9
commit d1943f564a
18 changed files with 546 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
<template>
<router-view />
</template>
<script setup lang="ts">
// Root component
</script>
<style>
#app {
width: 100%;
height: 100vh;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,17 @@
<template>
<div class="center-panel">
<p>Center Panel - Chat Interface</p>
</div>
</template>
<script setup lang="ts">
// CenterPanel component placeholder
</script>
<style scoped>
.center-panel {
flex: 1;
background-color: var(--color-bg-primary);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="left-panel">
<p>Left Panel - Character List & Chat History</p>
</div>
</template>
<script setup lang="ts">
// LeftPanel component placeholder
</script>
<style scoped>
.left-panel {
width: 300px;
background-color: var(--color-bg-secondary);
border-right: 1px solid var(--color-border);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,18 @@
<template>
<div class="right-panel">
<p>Right Panel - Character Detail & Workflow Editor</p>
</div>
</template>
<script setup lang="ts">
// RightPanel component placeholder
</script>
<style scoped>
.right-panel {
width: 400px;
background-color: var(--color-bg-secondary);
border-left: 1px solid var(--color-border);
overflow-y: auto;
}
</style>

View File

@@ -0,0 +1,20 @@
<template>
<div class="top-bar">
<h1>SillyTavern Repalice</h1>
</div>
</template>
<script setup lang="ts">
// TopBar component placeholder
</script>
<style scoped>
.top-bar {
height: 60px;
display: flex;
align-items: center;
padding: 0 var(--spacing-md);
background-color: var(--color-bg-secondary);
border-bottom: 1px solid var(--color-border);
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<div class="main-layout">
<TopBar />
<div class="main-content">
<LeftPanel />
<CenterPanel />
<RightPanel />
</div>
</div>
</template>
<script setup lang="ts">
import TopBar from '@/components/TopBar/TopBar.vue';
import LeftPanel from '@/components/LeftPanel/LeftPanel.vue';
import CenterPanel from '@/components/CenterPanel/CenterPanel.vue';
import RightPanel from '@/components/RightPanel/RightPanel.vue';
</script>
<style scoped>
.main-layout {
display: flex;
flex-direction: column;
width: 100%;
height: 100vh;
background-color: var(--color-bg-primary);
color: var(--color-text-primary);
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,14 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import './styles/reset.css';
import './styles/variables.css';
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.use(router);
app.mount('#app');

View File

@@ -0,0 +1,35 @@
import { createRouter, createWebHistory } from 'vue-router';
import MainLayout from '@/layouts/MainLayout/MainLayout.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: MainLayout,
children: [
{
path: '',
redirect: '/chat',
},
{
path: 'chat',
name: 'Chat',
component: () => import('@/components/CenterPanel/CenterPanel.vue'),
},
{
path: 'characters',
name: 'Characters',
component: () => import('@/components/LeftPanel/LeftPanel.vue'),
},
{
path: 'character/:id',
name: 'CharacterDetail',
component: () => import('@/components/RightPanel/RightPanel.vue'),
},
],
},
],
});
export default router;

View File

@@ -0,0 +1,24 @@
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useAppStore = defineStore('app', () => {
// State
const theme = ref<'light' | 'dark'>('dark');
const sidebarCollapsed = ref(false);
// Actions
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light';
}
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value;
}
return {
theme,
sidebarCollapsed,
toggleTheme,
toggleSidebar,
};
});

View File

@@ -0,0 +1,49 @@
/* CSS Reset */
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body,
#app {
width: 100%;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
ul,
ol {
list-style: none;
}
a {
text-decoration: none;
color: inherit;
}
button {
border: none;
background: none;
cursor: pointer;
font: inherit;
color: inherit;
}
input,
textarea,
select {
font: inherit;
color: inherit;
}
img,
video {
max-width: 100%;
height: auto;
display: block;
}

View File

@@ -0,0 +1,72 @@
/* CSS Variables */
:root {
/* Colors - Dark Theme (Default) */
--color-bg-primary: #1a1a1a;
--color-bg-secondary: #252525;
--color-bg-tertiary: #303030;
--color-text-primary: #e0e0e0;
--color-text-secondary: #b0b0b0;
--color-text-muted: #808080;
--color-border: #404040;
--color-border-light: #505050;
--color-accent: #6c63ff;
--color-accent-hover: #7b73ff;
--color-accent-active: #5a52d5;
--color-success: #4caf50;
--color-warning: #ff9800;
--color-error: #f44336;
/* Spacing */
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--transition-slow: 350ms ease;
/* Z-index layers */
--z-dropdown: 1000;
--z-sticky: 1020;
--z-fixed: 1030;
--z-modal-backdrop: 1040;
--z-modal: 1050;
--z-popover: 1060;
--z-tooltip: 1070;
}
/* Light Theme */
[data-theme='light'] {
--color-bg-primary: #ffffff;
--color-bg-secondary: #f5f5f5;
--color-bg-tertiary: #e8e8e8;
--color-text-primary: #212121;
--color-text-secondary: #616161;
--color-text-muted: #9e9e9e;
--color-border: #e0e0e0;
--color-border-light: #d0d0d0;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.15);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.2);
}

View File

@@ -0,0 +1,43 @@
version: '3.8'
services:
# Backend Service
backend:
build:
context: .
dockerfile: docker/backend.Dockerfile
container_name: sillytavern-repalice-backend
ports:
- "3000:3000"
volumes:
- ./data:/app/data
environment:
- NODE_ENV=production
- PORT=3000
- DATA_DIR=/app/data
- FRONTEND_URL=http://localhost:5173
env_file:
- .env
networks:
- app-network
restart: unless-stopped
# Frontend Service
frontend:
build:
context: .
dockerfile: docker/frontend.Dockerfile
container_name: sillytavern-repalice-frontend
ports:
- "5173:5173"
environment:
- VITE_API_URL=http://localhost:3000/api
depends_on:
- backend
networks:
- app-network
restart: unless-stopped
networks:
app-network:
driver: bridge

View File

@@ -0,0 +1,40 @@
# Backend Dockerfile - NestJS
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY server/package*.json ./server/
COPY shared/package*.json ./shared/
# Install dependencies
RUN cd server && npm ci
# Copy source code
COPY server/src ./server/src
COPY server/tsconfig.json ./server/
COPY server/nest-cli.json ./server/
COPY shared ./shared
# Build
RUN cd server && npm run build
# Production stage
FROM node:20-alpine
WORKDIR /app
# Copy package files and install production dependencies
COPY server/package*.json ./
RUN npm ci --only=production
# Copy built application
COPY --from=builder /app/server/dist ./dist
COPY shared ./shared
# Create data directory
RUN mkdir -p /app/data
EXPOSE 3000
CMD ["node", "dist/main.js"]

View File

@@ -0,0 +1,31 @@
# Frontend Dockerfile - Vue3 + Vite
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY client/package*.json ./client/
COPY shared/package*.json ./shared/
# Install dependencies
RUN cd client && npm ci
# Copy source code
COPY client ./client
COPY shared ./shared
# Build
RUN cd client && npm run build
# Production stage - serve with nginx
FROM nginx:alpine
# Copy custom nginx config
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
# Copy built files
COPY --from=builder /app/client/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@@ -0,0 +1,37 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml+rss application/json application/javascript;
# Serve static files
location / {
try_files $uri $uri/ /index.html;
}
# API proxy to backend
location /api/ {
proxy_pass http://backend:3000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}

View File

@@ -0,0 +1,29 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { PersistenceModule } from './persistence/persistence.module';
import { ChatModule } from './modules/chat/chat.module';
import { CharacterModule } from './modules/character/character.module';
import { LLMModule } from './modules/llm/llm.module';
import { ImportExportModule } from './modules/import-export/import-export.module';
import { WorkflowModule } from './modules/workflow/workflow.module';
@Module({
imports: [
// 配置模块
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.local', '.env'],
}),
// 持久化层
PersistenceModule,
// 业务模块
ChatModule,
CharacterModule,
LLMModule,
ImportExportModule,
WorkflowModule,
],
})
export class AppModule {}

View File

@@ -0,0 +1,41 @@
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './core/filters/http-exception.filter';
import { LoggingInterceptor, ResponseInterceptor } from './core/interceptors';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 全局前缀
app.setGlobalPrefix('api');
// CORS 配置
app.enableCors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
});
// 全局验证管道
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
transform: true,
forbidNonWhitelisted: true,
}),
);
// 全局异常过滤器
app.useGlobalFilters(new HttpExceptionFilter());
// 全局拦截器
app.useGlobalInterceptors(new LoggingInterceptor());
app.useGlobalInterceptors(new ResponseInterceptor());
const port = process.env.PORT || 3000;
await app.listen(port);
console.log(`🚀 Server is running on http://localhost:${port}`);
}
bootstrap();

View File

@@ -0,0 +1,9 @@
import { Module, Global } from '@nestjs/common';
import { FileSystemRepository } from './file-system/file-system.repository';
@Global()
@Module({
providers: [FileSystemRepository],
exports: [FileSystemRepository],
})
export class PersistenceModule {}