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