1351 lines
34 KiB
Markdown
1351 lines
34 KiB
Markdown
# 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 是否正确设置在请求头中
|
||
|
||
---
|
||
|
||
**文档结束**
|