16 KiB
16 KiB
web-app Vue 3 重构方案
📋 方案概述
将 web-app 从传统的 jQuery 项目重构为 Vue 3 现代化单页应用(SPA)。
🎯 重构目标
技术目标
- ✅ 使用 Vue 3 + Vite + TypeScript
- ✅ 组件化、模块化开发
- ✅ 统一前后端技术栈(与管理后台一致)
- ✅ 优化性能和用户体验
- ✅ 便于维护和扩展
业务目标
- ✅ 保留所有原有功能
- ✅ 改善 UI/UX 设计
- ✅ 适配移动端
- ✅ 支持主题切换
- ✅ 国际化支持
🏗️ 技术栈选择
核心技术
{
"框架": "Vue 3 (Composition API)",
"构建工具": "Vite 5",
"语言": "TypeScript",
"状态管理": "Pinia",
"路由": "Vue Router 4",
"HTTP": "Axios",
"WebSocket": "原生 WebSocket / Socket.io-client"
}
UI 框架选择
方案一:Element Plus(推荐)
# 优点
✅ 与管理后台统一(web/ 使用的就是 Element Plus)
✅ 组件丰富,开箱即用
✅ 中文文档完善
✅ 支持深色主题
✅ TypeScript 支持好
# 缺点
⚠️ 组件较重(如果只需要简单 UI,可能过度设计)
方案二:Naive UI
# 优点
✅ 轻量级,性能好
✅ TypeScript 原生支持
✅ 组件设计现代
✅ 支持深色主题
# 缺点
⚠️ 与管理后台不统一
方案三:自定义 UI(参考 auth.html)
# 优点
✅ 完全自定义,独特设计
✅ 轻量级
✅ 可以复用已有的 auth.css 样式
# 缺点
⚠️ 需要自己实现所有组件
⚠️ 开发时间较长
推荐:Element Plus + 自定义样式
- 使用 Element Plus 的基础组件(表单、按钮、对话框等)
- 自定义主题和关键页面样式
- 兼顾开发效率和设计独特性
📂 项目结构
新项目结构(推荐)
web-app-vue/ # 新建的 Vue 项目
├── public/
│ ├── favicon.ico
│ └── img/ # 静态图片资源
├── src/
│ ├── api/ # API 接口
│ │ ├── auth.ts # 认证接口
│ │ ├── character.ts # 角色接口
│ │ ├── chat.ts # 对话接口
│ │ └── index.ts # axios 配置
│ ├── assets/ # 资源文件
│ │ ├── styles/ # 全局样式
│ │ │ ├── index.scss
│ │ │ ├── variables.scss
│ │ │ └── dark-theme.scss
│ │ └── images/
│ ├── components/ # 通用组件
│ │ ├── common/ # 基础组件
│ │ │ ├── AppHeader.vue
│ │ │ ├── AppSidebar.vue
│ │ │ └── Loading.vue
│ │ ├── chat/ # 对话组件
│ │ │ ├── ChatMessage.vue
│ │ │ ├── ChatInput.vue
│ │ │ ├── MessageSwipe.vue
│ │ │ └── VoiceInput.vue
│ │ ├── character/ # 角色组件
│ │ │ ├── CharacterCard.vue
│ │ │ ├── CharacterList.vue
│ │ │ ├── CharacterEditor.vue
│ │ │ └── CharacterImport.vue
│ │ └── settings/ # 设置组件
│ │ ├── AIConfig.vue
│ │ ├── ThemeConfig.vue
│ │ └── UserProfile.vue
│ ├── composables/ # 组合式函数
│ │ ├── useAuth.ts # 认证相关
│ │ ├── useChat.ts # 对话相关
│ │ ├── useCharacter.ts # 角色相关
│ │ └── useWebSocket.ts # WebSocket
│ ├── layouts/ # 布局组件
│ │ ├── DefaultLayout.vue # 默认布局
│ │ ├── AuthLayout.vue # 认证页布局
│ │ └── ChatLayout.vue # 对话页布局
│ ├── router/ # 路由配置
│ │ ├── index.ts
│ │ └── guards.ts # 路由守卫
│ ├── stores/ # Pinia 状态管理
│ │ ├── auth.ts # 用户认证
│ │ ├── chat.ts # 对话状态
│ │ ├── character.ts # 角色管理
│ │ ├── settings.ts # 设置
│ │ └── index.ts
│ ├── types/ # TypeScript 类型
│ │ ├── api.d.ts
│ │ ├── character.d.ts
│ │ ├── chat.d.ts
│ │ └── user.d.ts
│ ├── utils/ # 工具函数
│ │ ├── request.ts # HTTP 请求封装
│ │ ├── storage.ts # 本地存储
│ │ ├── websocket.ts # WebSocket 封装
│ │ ├── format.ts # 格式化
│ │ └── validate.ts # 验证
│ ├── views/ # 页面视图
│ │ ├── auth/ # 认证页
│ │ │ ├── Login.vue
│ │ │ └── Register.vue
│ │ ├── home/ # 主页
│ │ │ └── Index.vue
│ │ ├── character/ # 角色页
│ │ │ ├── List.vue
│ │ │ ├── Detail.vue
│ │ │ └── Create.vue
│ │ ├── chat/ # 对话页
│ │ │ └── Index.vue
│ │ ├── settings/ # 设置页
│ │ │ └── Index.vue
│ │ └── user/ # 用户中心
│ │ └── Profile.vue
│ ├── App.vue # 根组件
│ └── main.ts # 入口文件
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md
与现有项目的关系
st/
├── server/ # Go 后端(不变)
├── web/ # Vue 管理后台(不变)
├── web-app/ # 旧项目(保留备份)
└── web-app-vue/ # 新的 Vue 前台(新建)
└── ...
🚀 实施步骤
阶段一:项目初始化(1-2 天)
1.1 创建 Vue 项目
# 使用 Vite 创建项目
npm create vite@latest web-app-vue -- --template vue-ts
cd web-app-vue
npm install
1.2 安装依赖
# UI 框架
npm install element-plus
# 路由和状态管理
npm install vue-router@4 pinia
# HTTP 和工具
npm install axios
npm install @vueuse/core # Vue 常用组合式函数
# 开发依赖
npm install -D sass
npm install -D unplugin-vue-components unplugin-auto-import
1.3 配置 Vite
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', 'pinia'],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
server: {
port: 3000,
proxy: {
'/app': {
target: 'http://localhost:8888',
changeOrigin: true,
},
},
},
})
1.4 基础配置
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router'
import App from './App.vue'
import './assets/styles/index.scss'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus)
app.mount('#app')
阶段二:核心功能开发(5-7 天)
2.1 认证模块(1 天)
- 登录页面
- 注册页面
- Token 管理
- 路由守卫
2.2 角色管理(2 天)
- 角色列表
- 角色详情
- 角色创建/编辑
- 角色导入/导出
- 收藏功能
2.3 对话功能(3 天)
- 对话界面
- 消息发送
- 消息渲染
- Swipe 功能
- WebSocket 实时通信
- 流式输出
2.4 设置页面(1 天)
- AI 配置
- 主题设置
- 用户偏好
阶段三:高级功能(3-5 天)
3.1 AI 功能
- 多模型支持
- Prompt 管理
- World Info
- 向量记忆
3.2 UI/UX 优化
- 响应式布局
- 移动端适配
- 主题切换
- 动画效果
3.3 文件管理
- 头像上传
- 文件管理
- 图片预览
阶段四:测试和优化(2-3 天)
- 单元测试
- E2E 测试
- 性能优化
- 打包优化
- 部署配置
📦 关键组件示例
1. 认证 Store
// src/stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login, register, getUserInfo } from '@/api/auth'
import type { LoginRequest, User } from '@/types/user'
export const useAuthStore = defineStore('auth', () => {
const token = ref<string>(localStorage.getItem('st_access_token') || '')
const user = ref<User | null>(null)
const isLoggedIn = computed(() => !!token.value)
async function handleLogin(data: LoginRequest) {
const res = await login(data)
token.value = res.data.token
user.value = res.data.user
localStorage.setItem('st_access_token', res.data.token)
localStorage.setItem('st_user_info', JSON.stringify(res.data.user))
}
async function handleLogout() {
token.value = ''
user.value = null
localStorage.removeItem('st_access_token')
localStorage.removeItem('st_user_info')
}
async function fetchUserInfo() {
const res = await getUserInfo()
user.value = res.data
}
return {
token,
user,
isLoggedIn,
handleLogin,
handleLogout,
fetchUserInfo,
}
})
2. HTTP 请求封装
// src/utils/request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { useAuthStore } from '@/stores/auth'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8888',
timeout: 10000,
})
// 请求拦截器
request.interceptors.request.use(
(config) => {
const authStore = useAuthStore()
if (authStore.token) {
config.headers['x-token'] = authStore.token
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
(response) => {
const res = response.data
if (res.code !== 0) {
ElMessage.error(res.msg || '请求失败')
return Promise.reject(new Error(res.msg || '请求失败'))
}
return res
},
(error) => {
ElMessage.error(error.message || '网络错误')
return Promise.reject(error)
}
)
export default request
3. 角色卡片组件
<!-- src/components/character/CharacterCard.vue -->
<template>
<div class="character-card" @click="handleClick">
<div class="card-header">
<img :src="character.avatar" :alt="character.name" class="avatar" />
<el-icon v-if="isFavorite" class="favorite-icon">
<Star />
</el-icon>
</div>
<div class="card-body">
<h3 class="character-name">{{ character.name }}</h3>
<p class="character-description">{{ character.description }}</p>
</div>
<div class="card-footer">
<el-tag v-for="tag in character.tags" :key="tag" size="small">
{{ tag }}
</el-tag>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { Character } from '@/types/character'
interface Props {
character: Character
}
const props = defineProps<Props>()
const emit = defineEmits<{
click: [character: Character]
}>()
const isFavorite = computed(() => props.character.isFavorite)
function handleClick() {
emit('click', props.character)
}
</script>
<style scoped lang="scss">
.character-card {
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
background: var(--el-bg-color);
&:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.card-header {
position: relative;
.avatar {
width: 100%;
height: 200px;
object-fit: cover;
}
.favorite-icon {
position: absolute;
top: 12px;
right: 12px;
color: #f59e0b;
font-size: 24px;
}
}
.card-body {
padding: 16px;
.character-name {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.character-description {
color: var(--el-text-color-secondary);
font-size: 14px;
line-height: 1.6;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
}
.card-footer {
padding: 0 16px 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
}
</style>
🔄 迁移策略
渐进式迁移(推荐)
-
Phase 1: 新功能使用 Vue
- 保留旧 web-app,用于稳定运行
- 新功能在 web-app-vue 开发
- 通过路由切换新旧页面
-
Phase 2: 逐步迁移
- 按模块迁移(认证 → 角色 → 对话 → 设置)
- 每个模块迁移后充分测试
- 确保功能完整
-
Phase 3: 完全替换
- 所有功能迁移完成
- 彻底删除旧代码
- web-app-vue 改名为 web-app
一次性重写(不推荐,风险高)
除非你有充足的时间和人力,否则不建议一次性重写整个项目。
📊 与现有项目的集成
Go 后端配置
// server/initialize/router.go
// 前台 Vue 应用静态文件
webAppVuePath := "../web-app-vue/dist"
if _, err := os.Stat(webAppVuePath); err == nil {
Router.Static("/assets", webAppVuePath+"/assets")
Router.StaticFile("/", webAppVuePath+"/index.html")
global.GVA_LOG.Info("前台 Vue 应用已启动: " + webAppVuePath)
}
环境配置
# .env.development
VITE_API_BASE_URL=http://localhost:8888
VITE_WS_URL=ws://localhost:8888
# .env.production
VITE_API_BASE_URL=https://your-domain.com
VITE_WS_URL=wss://your-domain.com
🎯 开发优先级
P0(必须)
- ✅ 用户认证(登录/注册)
- ✅ 角色列表和详情
- ✅ 基本对话功能
- ✅ AI 配置
P1(重要)
- ⚪ 角色编辑和创建
- ⚪ 消息 Swipe
- ⚪ 文件上传
- ⚪ 设置页面
P2(可选)
- ⚪ World Info
- ⚪ 向量记忆
- ⚪ 高级 AI 功能
- ⚪ 主题切换
📝 注意事项
1. 保持功能一致性
- 确保所有原有功能都能在 Vue 版本中实现
- 参考旧版的交互逻辑
- 不要遗漏边界情况
2. 性能优化
- 使用虚拟滚动(角色列表、消息列表)
- 图片懒加载
- 路由懒加载
- 打包分析和优化
3. 移动端适配
- 响应式设计
- 触摸手势支持
- 移动端专用布局
4. 国际化
- 使用 vue-i18n
- 支持多语言切换
- 本地化日期和数字
📚 学习资源
- Vue 3 官方文档: https://cn.vuejs.org/
- Vite 文档: https://cn.vitejs.dev/
- Element Plus: https://element-plus.org/zh-CN/
- Pinia 文档: https://pinia.vuejs.org/zh/
- TypeScript 文档: https://www.typescriptlang.org/zh/
🎉 总结
为什么要重构?
- ✅ 代码质量:从混乱的 jQuery 代码到清晰的组件化代码
- ✅ 开发效率:热更新、TypeScript、自动导入
- ✅ 性能提升:Vite 构建、按需加载、虚拟滚动
- ✅ 易于维护:模块化、类型安全、单一职责
- ✅ 技术统一:与管理后台使用相同技术栈
- ✅ 未来发展:易于扩展新功能
建议的时间线
- 快速原型(1 周):核心功能,可以开始使用
- MVP 版本(2-3 周):主要功能完成
- 完整版本(4-6 周):所有功能迁移完成
- 优化打磨(持续):性能优化、bug 修复
更新日期: 2026-02-10
版本: v1.0.0