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

34 KiB
Raw Blame History

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

修改内容:

{
  "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"
  }
}

执行命令:

cd frontend
npm install

1.2 创建 API 服务层

新建文件: frontend/src/service/apiService.js

文件内容:

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

文件内容:

VITE_API_BASE_URL=http://localhost:8080/api/v1

新建文件: frontend/.env.production

文件内容:

VITE_API_BASE_URL=https://api.openclaw.com/api/v1

1.4 修改 Vite 配置(添加代理)

修改文件: frontend/vite.config.js

当前文件内容(如果不存在则新建):

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

完整替换内容:

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

完整替换内容:

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

完整替换内容:

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

完整替换内容:

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

完整替换内容:

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

修改内容: 替换整个路由守卫部分

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

完整替换内容:

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

修改示例

// 修改前
import { userService } from '@/service/localService'

// 修改后
import { userService } from '@/service/apiService'

联调测试计划

测试步骤

  1. 环境准备

    # 启动后端服务
    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 是否正确设置在请求头中

文档结束