Files
number/前端后端联调详细修改文档.md
2026-03-17 12:09:43 +08:00

1351 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OpenClaw Skills 前后端联调详细修改文档
## 📋 文档概述
本文档详细说明如何将前端从 localStorage 模式修改为与后端 API 联调模式。包含所有需要修改的文件、具体代码变更、数据结构映射关系和测试方案。
**文档版本**: v2.0
**创建日期**: 2026-03-17
**目标**: 完成前后端完全联调
---
## 📊 前后端架构对比
### 当前前端架构
```
前端
├── localStorage (数据存储)
├── mockData.js (数据初始化)
├── localService.js (业务逻辑)
└── stores (状态管理)
```
### 目标架构
```
前端 后端
├── apiService.js ←→ ├── RESTful API
├── stores ←→ ├── JWT 认证
└── 响应式渲染 ←→ └── MySQL + Redis
```
---
## 🔧 第一阶段:基础设施配置
### 1.1 安装 axios 依赖
**修改文件**: `frontend/package.json`
**修改内容**:
```json
{
"dependencies": {
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"pinia": "^2.1.7",
"element-plus": "^2.6.1",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.6.8"
}
}
```
**执行命令**:
```bash
cd frontend
npm install
```
---
### 1.2 创建 API 服务层
**新建文件**: `frontend/src/service/apiService.js`
**文件内容**:
```javascript
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080/api/v1',
timeout: 30000,
headers: {
'Content-Type': 'application/json'
}
})
apiClient.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
apiClient.interceptors.response.use(
response => {
const res = response.data
if (res.code === 200) {
return {
success: true,
data: res.data,
message: res.message || '操作成功'
}
} else {
ElMessage.error(res.message || '操作失败')
return {
success: false,
message: res.message || '操作失败',
code: res.code
}
}
},
error => {
if (error.response) {
const { status, data } = error.response
if (status === 401) {
ElMessage.error('登录已过期,请重新登录')
localStorage.removeItem('token')
localStorage.removeItem('user')
router.push('/login')
} else if (status === 403) {
ElMessage.error('没有权限访问')
} else if (status === 500) {
ElMessage.error('服务器错误')
} else if (data?.message) {
ElMessage.error(data.message)
} else {
ElMessage.error('网络错误')
}
} else {
ElMessage.error('网络连接失败')
}
return {
success: false,
message: error.message || '网络错误'
}
}
)
export const userService = {
async sendSmsCode(phone) {
return await apiClient.post('/users/sms-code', { phone })
},
async register(phone, password, smsCode, inviteCode = null) {
const data = { phone, password, smsCode }
if (inviteCode) data.inviteCode = inviteCode
const result = await apiClient.post('/users/register', data)
if (result.success && result.data?.token) {
localStorage.setItem('token', result.data.token)
if (result.data?.user) {
localStorage.setItem('user', JSON.stringify(result.data.user))
}
}
return result
},
async login(phone, password) {
const result = await apiClient.post('/users/login', { phone, password })
if (result.success && result.data?.token) {
localStorage.setItem('token', result.data.token)
if (result.data?.user) {
localStorage.setItem('user', JSON.stringify(result.data.user))
}
}
return result
},
async getProfile() {
return await apiClient.get('/users/profile')
},
async updateProfile(data) {
return await apiClient.put('/users/profile', data)
},
async updatePassword(oldPassword, newPassword) {
return await apiClient.put('/users/password', { oldPassword, newPassword })
},
async logout() {
const result = await apiClient.post('/users/logout')
localStorage.removeItem('token')
localStorage.removeItem('user')
return result
},
async dailySign() {
return await apiClient.post('/points/sign-in')
},
async joinGroup() {
return { success: true, message: '加入成功' }
},
async getAllUsers() {
return await apiClient.get('/admin/users')
},
async banUser(userId) {
return await apiClient.put(`/admin/users/${userId}/ban`)
},
async unbanUser(userId) {
return await apiClient.put(`/admin/users/${userId}/unban`)
}
}
export const skillService = {
async getSkills(params = {}) {
const { pageNum = 1, pageSize = 20, categoryId, keyword, sort = 'newest' } = params
const queryParams = new URLSearchParams()
queryParams.append('pageNum', pageNum)
queryParams.append('pageSize', pageSize)
if (categoryId) queryParams.append('categoryId', categoryId)
if (keyword) queryParams.append('keyword', keyword)
if (sort) queryParams.append('sort', sort)
return await apiClient.get(`/skills?${queryParams.toString()}`)
},
async getSkillById(skillId) {
return await apiClient.get(`/skills/${skillId}`)
},
async getCategories() {
return {
success: true,
data: [
{ id: 1, name: '办公自动化' },
{ id: 2, name: '数据分析' },
{ id: 3, name: '网络爬虫' },
{ id: 4, name: 'AI 工具' },
{ id: 5, name: '图像处理' }
]
}
},
async searchSkills(keyword, filters = {}) {
const params = { keyword, ...filters }
return await this.getSkills(params)
},
async uploadSkill(data) {
return await apiClient.post('/skills', data)
},
async updateSkill(skillId, data) {
return await apiClient.put(`/skills/${skillId}`, data)
},
async deleteSkill(skillId) {
return await apiClient.delete(`/skills/${skillId}`)
},
async approveSkill(skillId) {
return await apiClient.post(`/admin/skills/${skillId}/approve`)
},
async rejectSkill(skillId, reason) {
return await apiClient.post(`/admin/skills/${skillId}/reject`, { reason })
},
async setFeatured(skillId, featured) {
return await apiClient.put(`/admin/skills/${skillId}/featured`, { featured })
},
async setHot(skillId, hot) {
return await apiClient.put(`/admin/skills/${skillId}/hot`, { hot })
},
async getComments(skillId) {
return await apiClient.get(`/skills/${skillId}/reviews`)
},
async addComment(skillId, rating, content, images = []) {
return await apiClient.post(`/skills/${skillId}/reviews`, { rating, content, images })
},
async likeComment(commentId) {
return await apiClient.post(`/comments/${commentId}/like`)
},
async deleteComment(commentId) {
return await apiClient.delete(`/comments/${commentId}`)
},
async getAllComments() {
return await apiClient.get('/admin/comments')
},
async getAllSkills() {
return await this.getSkills({ pageNum: 1, pageSize: 1000 })
}
}
export const orderService = {
async createOrder(skillIds, pointsToUse = 0, paymentMethod = 'wechat') {
return await apiClient.post('/orders', { skillIds, pointsToUse, paymentMethod })
},
async getOrders(params = {}) {
const { pageNum = 1, pageSize = 20 } = params
return await apiClient.get(`/orders?pageNum=${pageNum}&pageSize=${pageSize}`)
},
async getOrderById(orderId) {
return await apiClient.get(`/orders/${orderId}`)
},
async payOrder(orderId, paymentNo) {
return await apiClient.post(`/orders/${orderId}/pay?paymentNo=${paymentNo}`)
},
async cancelOrder(orderId, reason = '') {
return await apiClient.post(`/orders/${orderId}/cancel?reason=${reason}`)
},
async applyRefund(orderId, reason, images = []) {
return await apiClient.post(`/orders/${orderId}/refund`, { reason, images })
},
async getUserPurchasedSkills(userId) {
const result = await this.getOrders()
if (result.success && result.data?.records) {
const purchasedSkills = result.data.records
.filter(o => o.status === 'completed')
.map(o => ({
...o.items?.[0] || {},
purchasedAt: o.completedAt,
orderId: o.id
}))
return { success: true, data: purchasedSkills }
}
return result
},
async getAllOrders() {
return await apiClient.get('/admin/orders')
}
}
export const pointService = {
async getBalance() {
return await apiClient.get('/points/balance')
},
async getPointRecords(params = {}) {
const { pageNum = 1, pageSize = 20, type, source } = params
let url = `/points/records?pageNum=${pageNum}&pageSize=${pageSize}`
if (type) url += `&type=${type}`
if (source) url += `&source=${source}`
return await apiClient.get(url)
},
async recharge(amount, paymentMethod = 'wechat') {
return await apiClient.post('/payments/recharge', { amount, paymentMethod })
},
async getRechargeTiers() {
return {
success: true,
data: [
{ amount: 10, bonusPoints: 10 },
{ amount: 50, bonusPoints: 60 },
{ amount: 100, bonusPoints: 150 },
{ amount: 500, bonusPoints: 800 },
{ amount: 1000, bonusPoints: 2000 }
]
}
},
async getPointRules() {
return {
success: true,
data: {
register: 100,
invite: 50,
signin: 5,
review: 10,
reviewWithImage: 20,
joinGroup: 20
}
}
},
async getPaymentRecords(params = {}) {
const { pageNum = 1, pageSize = 20 } = params
return await apiClient.get(`/payments/records?pageNum=${pageNum}&pageSize=${pageSize}`)
},
async getAllPointRecords() {
return await apiClient.get('/admin/points')
}
}
export const inviteService = {
async getMyInviteCode() {
return await apiClient.get('/invites/my-code')
},
async bindInviteCode(inviteCode) {
return await apiClient.post('/invites/bind', { inviteCode })
},
async getInviteRecords(params = {}) {
const { pageNum = 1, pageSize = 20 } = params
return await apiClient.get(`/invites/records?pageNum=${pageNum}&pageSize=${pageSize}`)
},
async getInviteStats() {
return await apiClient.get('/invites/stats')
}
}
export const notificationService = {
async getUserNotifications(userId) {
return []
},
async markAsRead(notificationId) {
return { success: true }
},
async markAllAsRead(userId) {
return { success: true }
},
async getUnreadCount(userId) {
return 0
},
async deleteNotification(notificationId) {
return { success: true }
}
}
export const adminService = {
async login(username, password) {
return await apiClient.post('/admin/login', { username, password })
},
async getDashboardStats() {
return await apiClient.get('/admin/dashboard/stats')
},
async getSystemConfig() {
return await apiClient.get('/admin/config')
},
async updateSystemConfig(config) {
return await apiClient.put('/admin/config', config)
}
}
export default {
userService,
skillService,
orderService,
pointService,
inviteService,
notificationService,
adminService
}
```
---
### 1.3 创建环境配置文件
**新建文件**: `frontend/.env.development`
**文件内容**:
```env
VITE_API_BASE_URL=http://localhost:8080/api/v1
```
**新建文件**: `frontend/.env.production`
**文件内容**:
```env
VITE_API_BASE_URL=https://api.openclaw.com/api/v1
```
---
### 1.4 修改 Vite 配置(添加代理)
**修改文件**: `frontend/vite.config.js`
**当前文件内容(如果不存在则新建)**:
```javascript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
})
```
---
## 🔧 第二阶段:状态管理修改
### 2.1 修改用户 Store
**修改文件**: `frontend/src/stores/user.js`
**完整替换内容**:
```javascript
import { defineStore } from 'pinia'
import { userService, notificationService } from '@/service/apiService'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: localStorage.getItem('token') || null,
isLoggedIn: !!localStorage.getItem('token'),
notifications: [],
unreadCount: 0
}),
getters: {
userInfo: (state) => state.user,
userPoints: (state) => state.user?.availablePoints || 0,
userLevel: (state) => state.user?.memberLevel || '普通会员',
isVip: (state) => false
},
actions: {
initUser() {
const savedUser = localStorage.getItem('user')
const token = localStorage.getItem('token')
if (savedUser && token) {
try {
this.user = JSON.parse(savedUser)
this.token = token
this.isLoggedIn = true
this.loadUserProfile()
} catch (e) {
this.logout()
}
}
},
async loadUserProfile() {
const result = await userService.getProfile()
if (result.success && result.data) {
this.user = result.data
localStorage.setItem('user', JSON.stringify(result.data))
}
},
async login(phone, password) {
const result = await userService.login(phone, password)
if (result.success) {
this.token = result.data.token
this.user = result.data.user
this.isLoggedIn = true
this.loadNotifications()
}
return result
},
async register(data) {
const result = await userService.register(
data.phone,
data.password,
data.smsCode,
data.inviteCode
)
if (result.success) {
this.token = result.data.token
this.user = result.data.user
this.isLoggedIn = true
this.loadNotifications()
}
return result
},
async logout() {
await userService.logout()
this.user = null
this.token = null
this.isLoggedIn = false
this.notifications = []
this.unreadCount = 0
},
async updateUserInfo(updates) {
if (this.user) {
const result = await userService.updateProfile(updates)
if (result.success && result.data) {
this.user = result.data
localStorage.setItem('user', JSON.stringify(result.data))
}
return result
}
return { success: false, message: '未登录' }
},
refreshUser() {
this.loadUserProfile()
},
async loadNotifications() {
if (this.user) {
const result = await notificationService.getUserNotifications(this.user.id)
this.notifications = result.success ? result.data : []
this.unreadCount = await notificationService.getUnreadCount(this.user.id)
}
},
async markNotificationRead(notificationId) {
await notificationService.markAsRead(notificationId)
this.loadNotifications()
},
async markAllNotificationsRead() {
if (this.user) {
await notificationService.markAllAsRead(this.user.id)
this.loadNotifications()
}
},
async dailySign() {
if (this.user) {
const result = await userService.dailySign()
if (result.success) {
this.refreshUser()
}
return result
}
return { success: false, message: '未登录' }
},
async joinGroup() {
if (this.user) {
const result = await userService.joinGroup()
if (result.success) {
this.refreshUser()
}
return result
}
return { success: false, message: '未登录' }
}
}
})
```
---
### 2.2 修改 Skill Store
**修改文件**: `frontend/src/stores/skill.js`
**完整替换内容**:
```javascript
import { defineStore } from 'pinia'
import { skillService, orderService } from '@/service/apiService'
export const useSkillStore = defineStore('skill', {
state: () => ({
skills: [],
categories: [],
currentSkill: null,
searchResults: [],
filters: {
keyword: '',
categoryId: null,
priceType: null,
minPrice: undefined,
maxPrice: undefined,
minRating: undefined,
sortBy: 'newest'
},
loading: false,
pagination: {
current: 1,
pageSize: 20,
total: 0,
pages: 0
}
}),
getters: {
featuredSkills: (state) => state.skills.filter(s => s.isFeatured),
hotSkills: (state) => state.skills.filter(s => s.isHot),
newSkills: (state) => state.skills.filter(s => s.isNew),
freeSkills: (state) => state.skills.filter(s => s.isFree),
paidSkills: (state) => state.skills.filter(s => !s.isFree)
},
actions: {
async loadSkills(params = {}) {
this.loading = true
const result = await skillService.getSkills({
pageNum: this.pagination.current,
pageSize: this.pagination.pageSize,
...params
})
if (result.success && result.data) {
this.skills = result.data.records || []
this.pagination = {
current: result.data.current || 1,
pageSize: result.data.size || 20,
total: result.data.total || 0,
pages: result.data.pages || 0
}
}
await this.loadCategories()
this.loading = false
return result
},
async loadCategories() {
const result = await skillService.getCategories()
if (result.success) {
this.categories = result.data || []
}
},
async loadSkillById(skillId) {
const result = await skillService.getSkillById(skillId)
if (result.success && result.data) {
this.currentSkill = result.data
}
return result
},
async searchSkills(keyword, filters = {}) {
this.filters = { ...this.filters, keyword, ...filters }
const result = await skillService.searchSkills(keyword, this.filters)
if (result.success && result.data) {
this.searchResults = result.data.records || []
}
return result
},
setFilters(filters) {
this.filters = { ...this.filters, ...filters }
},
clearFilters() {
this.filters = {
keyword: '',
categoryId: null,
priceType: null,
minPrice: undefined,
maxPrice: undefined,
minRating: undefined,
sortBy: 'newest'
}
this.searchResults = []
},
async getSkillsByCategory(categoryId) {
return await this.loadSkills({ categoryId })
},
async getComments(skillId) {
const result = await skillService.getComments(skillId)
return result.success ? result.data : []
},
async addComment(userId, skillId, rating, content, images) {
const result = await skillService.addComment(skillId, rating, content, images)
if (result.success) {
await this.loadSkillById(skillId)
}
return result
},
async likeComment(commentId) {
return await skillService.likeComment(commentId)
},
async hasUserPurchased(userId, skillId) {
const result = await orderService.getUserPurchasedSkills(userId)
if (result.success && result.data) {
return result.data.some(s => s.skillId === skillId)
}
return false
}
}
})
```
---
### 2.3 修改订单 Store
**修改文件**: `frontend/src/stores/order.js`
**完整替换内容**:
```javascript
import { defineStore } from 'pinia'
import { orderService } from '@/service/apiService'
export const useOrderStore = defineStore('order', {
state: () => ({
orders: [],
currentOrder: null,
loading: false,
pagination: {
current: 1,
pageSize: 20,
total: 0,
pages: 0
}
}),
getters: {
pendingOrders: (state) => state.orders.filter(o => o.status === 'pending'),
completedOrders: (state) => state.orders.filter(o => o.status === 'completed'),
refundedOrders: (state) => state.orders.filter(o => o.status === 'refunded')
},
actions: {
async loadUserOrders(userId, params = {}) {
this.loading = true
const result = await orderService.getOrders({
pageNum: this.pagination.current,
pageSize: this.pagination.pageSize,
...params
})
if (result.success && result.data) {
this.orders = result.data.records || []
this.pagination = {
current: result.data.current || 1,
pageSize: result.data.size || 20,
total: result.data.total || 0,
pages: result.data.pages || 0
}
}
this.loading = false
return result
},
async loadAllOrders() {
return await this.loadUserOrders(null)
},
async createOrder(skillIds, payType, pointsToUse = 0) {
const result = await orderService.createOrder(skillIds, pointsToUse, payType)
if (result.success) {
this.currentOrder = result.data
this.orders.unshift(result.data)
}
return result
},
async payOrder(orderId, paymentNo) {
const result = await orderService.payOrder(orderId, paymentNo)
if (result.success && result.data) {
const index = this.orders.findIndex(o => o.id === orderId)
if (index !== -1) {
this.orders[index] = result.data
}
}
return result
},
async cancelOrder(orderId, reason = '') {
const result = await orderService.cancelOrder(orderId, reason)
if (result.success) {
const index = this.orders.findIndex(o => o.id === orderId)
if (index !== -1) {
this.orders[index].status = 'cancelled'
}
}
return result
},
async applyRefund(orderId, reason, images = []) {
const result = await orderService.applyRefund(orderId, reason, images)
if (result.success) {
const index = this.orders.findIndex(o => o.id === orderId)
if (index !== -1) {
this.orders[index].status = 'refunding'
}
}
return result
},
async getOrderById(orderId) {
return await orderService.getOrderById(orderId)
},
async getUserPurchasedSkills(userId) {
return await orderService.getUserPurchasedSkills(userId)
}
}
})
```
---
### 2.4 修改积分 Store
**修改文件**: `frontend/src/stores/point.js`
**完整替换内容**:
```javascript
import { defineStore } from 'pinia'
import { pointService, userService } from '@/service/apiService'
export const usePointStore = defineStore('point', {
state: () => ({
records: [],
rechargeTiers: [],
pointRules: null,
loading: false,
pagination: {
current: 1,
pageSize: 20,
total: 0,
pages: 0
}
}),
getters: {
incomeRecords: (state) => state.records.filter(r => r.pointsType === 'earn'),
expenseRecords: (state) => state.records.filter(r => r.pointsType === 'expense'),
totalIncome: (state) => state.records.filter(r => r.pointsType === 'earn').reduce((sum, r) => sum + r.amount, 0),
totalExpense: (state) => state.records.filter(r => r.pointsType === 'expense').reduce((sum, r) => sum + r.amount, 0)
},
actions: {
async loadUserRecords(userId, filters = {}) {
this.loading = true
const result = await pointService.getPointRecords({
pageNum: this.pagination.current,
pageSize: this.pagination.pageSize,
...filters
})
if (result.success && result.data) {
this.records = result.data.records || []
this.pagination = {
current: result.data.current || 1,
pageSize: result.data.size || 20,
total: result.data.total || 0,
pages: result.data.pages || 0
}
}
this.loading = false
return result
},
async loadAllRecords() {
return await this.loadUserRecords(null)
},
async loadRechargeTiers() {
const result = await pointService.getRechargeTiers()
if (result.success) {
this.rechargeTiers = result.data
}
},
async loadPointRules() {
const result = await pointService.getPointRules()
if (result.success) {
this.pointRules = result.data
}
},
async recharge(userId, amount) {
const result = await pointService.recharge(amount)
if (result.success) {
await this.loadUserRecords(userId)
}
return result
},
async getInviteRecords(userId) {
const result = await userService.getInviteRecords?.(userId)
return result?.data || []
}
}
})
```
---
### 2.5 修改管理后台 Store
**修改文件**: `frontend/src/stores/admin.js`
**完整替换内容**:
```javascript
import { defineStore } from 'pinia'
import { adminService, userService, skillService, orderService } from '@/service/apiService'
export const useAdminStore = defineStore('admin', {
state: () => ({
admin: null,
isLoggedIn: false,
dashboardStats: null,
users: [],
skills: [],
orders: [],
comments: [],
systemConfig: null,
loading: false
}),
actions: {
async login(username, password) {
const result = await adminService.login(username, password)
if (result.success) {
this.admin = result.data
this.isLoggedIn = true
}
return result
},
logout() {
this.admin = null
this.isLoggedIn = false
sessionStorage.removeItem('admin_user')
},
async loadDashboardStats() {
this.loading = true
const result = await adminService.getDashboardStats()
if (result.success) {
this.dashboardStats = result.data
}
this.loading = false
return this.dashboardStats
},
async loadUsers() {
this.loading = true
const result = await userService.getAllUsers()
if (result.success) {
this.users = result.data?.records || result.data || []
}
this.loading = false
return this.users
},
async loadSkills() {
this.loading = true
const result = await skillService.getAllSkills()
if (result.success) {
this.skills = result.data?.records || result.data || []
}
this.loading = false
return this.skills
},
async loadOrders() {
this.loading = true
const result = await orderService.getAllOrders()
if (result.success) {
this.orders = result.data?.records || result.data || []
}
this.loading = false
return this.orders
},
async loadComments() {
this.loading = true
const result = await skillService.getAllComments()
if (result.success) {
this.comments = result.data?.records || result.data || []
}
this.loading = false
return this.comments
},
async loadSystemConfig() {
const result = await adminService.getSystemConfig()
if (result.success) {
this.systemConfig = result.data
}
return this.systemConfig
},
async updateSystemConfig(config) {
const result = await adminService.updateSystemConfig(config)
if (result.success) {
this.systemConfig = config
}
return result
},
async banUser(userId) {
const result = await userService.banUser(userId)
if (result.success) {
await this.loadUsers()
}
return result
},
async unbanUser(userId) {
const result = await userService.unbanUser(userId)
if (result.success) {
await this.loadUsers()
}
return result
},
async approveSkill(skillId) {
const result = await skillService.approveSkill(skillId)
if (result.success) {
await this.loadSkills()
}
return result
},
async rejectSkill(skillId, reason) {
const result = await skillService.rejectSkill(skillId, reason)
if (result.success) {
await this.loadSkills()
}
return result
}
}
})
```
---
## 🔧 第三阶段:路由和主入口修改
### 3.1 修改路由守卫
**修改文件**: `frontend/src/router/index.js`
**修改内容**: 替换整个路由守卫部分
```javascript
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
// ... 保持原有的路由配置不变
]
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
router.beforeEach((to, from, next) => {
document.title = to.meta.title ? `${to.meta.title} - OpenClaw Skills` : 'OpenClaw Skills'
if (to.meta.requiresAuth) {
const token = localStorage.getItem('token')
if (!token) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
return
}
}
if (to.meta.requiresAdmin) {
const admin = sessionStorage.getItem('admin_user')
if (!admin) {
next('/admin/login')
return
}
}
next()
})
export default router
```
---
### 3.2 修改主入口文件
**修改文件**: `frontend/src/main.js`
**完整替换内容**:
```javascript
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import { useUserStore } from './stores'
import './styles/index.scss'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.use(router)
app.use(ElementPlus, {
locale: zhCn
})
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
const userStore = useUserStore()
userStore.initUser()
app.mount('#app')
```
---
## 📊 数据结构映射关系
### 用户数据映射
| 前端字段 (localStorage) | 后端字段 (API) | 说明 |
|------------------------|----------------|------|
| id | id | 用户ID |
| phone | phone | 手机号 |
| nickname | nickname | 昵称 |
| avatar | avatarUrl | 头像URL |
| points | availablePoints | 可用积分 |
| level | memberLevel | 会员等级 |
| levelName | - | 前端计算 |
| inviteCode | inviteCode | 邀请码 |
| isVip | - | 后端暂无 |
### Skill 数据映射
| 前端字段 | 后端字段 | 说明 |
|---------|---------|------|
| id | id | Skill ID |
| name | name | 名称 |
| description | description | 描述 |
| cover | coverImageUrl | 封面图 |
| price | price | 价格 |
| categoryId | categoryId | 分类ID |
| downloadCount | downloadCount | 下载次数 |
| rating | rating | 评分 |
| status | - | 前端使用 |
### 订单数据映射
| 前端字段 | 后端字段 | 说明 |
|---------|---------|------|
| id | id | 订单ID |
| orderNo | orderNo | 订单号 |
| skillId | items[0].skillId | Skill ID |
| skillName | items[0].skillName | Skill 名称 |
| status | status | 订单状态 |
| payType | paymentMethod | 支付方式 |
---
## 📝 需要修改的视图文件清单
以下文件需要检查和修改导入语句:
| 文件路径 | 需要修改的内容 |
|---------|--------------|
| `frontend/src/views/user/login.vue` | 修改导入 localService → apiService |
| `frontend/src/views/user/register.vue` | 修改导入 localService → apiService |
| `frontend/src/views/user/profile.vue` | 修改导入 localService → apiService |
| `frontend/src/views/user/orders.vue` | 修改导入 localService → apiService |
| `frontend/src/views/user/points.vue` | 修改导入 localService → apiService |
| `frontend/src/views/user/recharge.vue` | 修改导入 localService → apiService |
| `frontend/src/views/user/invite.vue` | 修改导入 localService → apiService |
| `frontend/src/views/skill/list.vue` | 修改导入 localService → apiService |
| `frontend/src/views/skill/detail.vue` | 修改导入 localService → apiService |
| `frontend/src/views/order/pay.vue` | 修改导入 localService → apiService |
| `frontend/src/views/order/detail.vue` | 修改导入 localService → apiService |
| `frontend/src/views/home/index.vue` | 修改导入 localService → apiService |
| `frontend/src/views/admin/dashboard.vue` | 修改导入 localService → apiService |
| `frontend/src/views/admin/users.vue` | 修改导入 localService → apiService |
| `frontend/src/views/admin/skills.vue` | 修改导入 localService → apiService |
| `frontend/src/views/admin/orders.vue` | 修改导入 localService → apiService |
**修改示例**
```javascript
// 修改前
import { userService } from '@/service/localService'
// 修改后
import { userService } from '@/service/apiService'
```
---
## ✅ 联调测试计划
### 测试步骤
1. **环境准备**
```bash
# 启动后端服务
cd openclaw-backend/openclaw-backend
mvn spring-boot:run
# 启动前端服务
cd frontend
npm install
npm run dev
```
2. **基础功能测试**
- [ ] 用户注册(含短信验证码)
- [ ] 用户登录
- [ ] 获取用户信息
- [ ] Skill 列表查询
- [ ] Skill 详情查看
3. **核心业务测试**
- [ ] 创建订单
- [ ] 积分支付
- [ ] 充值功能
- [ ] 每日签到
- [ ] 邀请功能
4. **管理后台测试**
- [ ] 管理员登录
- [ ] 用户管理
- [ ] Skill 审核
- [ ] 订单管理
---
## 🎯 实施优先级
### P0 - 必须完成1-2天
- [ ] 安装 axios 依赖
- [ ] 创建 apiService.js
- [ ] 修改 user.js store
- [ ] 修改 skill.js store
- [ ] 修改路由守卫
### P1 - 重要功能2-3天
- [ ] 修改 order.js store
- [ ] 修改 point.js store
- [ ] 修改 admin.js store
- [ ] 修改 main.js
- [ ] 修改主要视图文件
### P2 - 完善和优化3-5天
- [ ] 修改剩余视图文件
- [ ] 添加加载状态
- [ ] 错误处理优化
- [ ] 完整联调测试
---
## ⚠️ 注意事项
1. **数据兼容性**:后端返回的数据结构与前端期望的可能不一致,需要做适配
2. **接口缺失**:部分功能后端可能暂无接口,需要使用模拟数据或等待后端开发
3. **Token 管理**:确保 Token 在 localStorage 中正确存储和清除
4. **错误处理**apiService 已经统一处理了错误,但视图层仍需关注特殊情况
5. **分页**:后端使用 pageNum/pageSize前端需要适配分页逻辑
---
## 📞 技术支持
如有问题,请检查:
1. 后端服务是否正常启动http://localhost:8080
2. 浏览器控制台是否有错误信息
3. Network 面板查看 API 请求和响应
4. 检查 Token 是否正确设置在请求头中
---
**文档结束**