You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

31 KiB

前端真实 API 对接实施计划

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 将 react-shadcn/pc 前端项目中的 mock 数据替换为真实后端 API 调用,优先完成用户管理和个人设置页面的数据对接。

Architecture: 采用渐进式对接策略:1) 优先对接现有后端已支持的 API(用户管理、个人资料)2) 为 Dashboard 创建聚合查询 API 或模拟数据增强 3) 添加 API 错误处理和加载状态 4) 使用 React Query 或 SWR 进行状态管理优化。

Tech Stack: React 19 + TypeScript, fetch API, go-zero backend API, localStorage for auth token


现状分析

Mock 数据使用情况

页面 当前状态 需要对接的 API
DashboardPage 完全 mock 数据 (stats, recentActivity, chart) 需要新增 Dashboard 统计 API
UserManagementPage 部分对接,有 fallback mock 完善现有 API 调用,移除 mock fallback
SettingsPage 完全静态表单,无 API 对接 /profile/me GET/PUT 和 /profile/password
LoginPage 已对接 /login -

后端已有 API 端点

POST   /api/v1/login              - 登录
POST   /api/v1/register           - 注册
POST   /api/v1/refresh            - 刷新 Token

GET    /api/v1/profile/me         - 获取个人资料
PUT    /api/v1/profile/me         - 更新个人资料
POST   /api/v1/profile/password   - 修改密码

GET    /api/v1/users              - 获取用户列表
GET    /api/v1/user/:id           - 获取用户详情
POST   /api/v1/user               - 创建用户
PUT    /api/v1/user/:id           - 更新用户
DELETE /api/v1/user/:id           - 删除用户

需要新增的后端 API

  • GET /api/v1/dashboard/stats - 仪表板统计数据
  • GET /api/v1/dashboard/activities - 最近活动列表

Task 1: 完善 api.ts 添加缺失的 API 方法

Files:

  • Modify: frontend/react-shadcn/pc/src/services/api.ts

Step 1: 添加 Profile 相关 API 方法

// src/services/api.ts

// 在 apiClient 类中添加以下方法

// Profile APIs
async getProfile(): Promise<ApiResponse<Profile>> {
  return this.request<ApiResponse<Profile>>('/profile/me')
}

async updateProfile(data: UpdateProfileRequest): Promise<ApiResponse<Profile>> {
  return this.request<ApiResponse<Profile>>('/profile/me', {
    method: 'PUT',
    body: JSON.stringify(data),
  })
}

async changePassword(data: ChangePasswordRequest): Promise<ApiResponse<void>> {
  return this.request<ApiResponse<void>>('/profile/password', {
    method: 'POST',
    body: JSON.stringify(data),
  })
}

// Dashboard APIs (需要后端实现)
async getDashboardStats(): Promise<ApiResponse<DashboardStats>> {
  return this.request<ApiResponse<DashboardStats>>('/dashboard/stats')
}

async getRecentActivities(limit: number = 10): Promise<ApiResponse<Activity[]>> {
  return this.request<ApiResponse<Activity[]>>(`/dashboard/activities?limit=${limit}`)
}

Step 2: 添加缺失的类型定义

// src/types/index.ts 添加以下类型

export interface DashboardStats {
  totalUsers: number
  activeUsers: number
  systemLoad: number
  dbStatus: '正常' | '异常'
  userGrowth: number[] // 12个月的数据
}

export interface Activity {
  id: number
  user: string
  action: string
  time: string
  status: 'success' | 'error'
}

export interface ChangePasswordRequest {
  oldPassword: string
  newPassword: string
}

export interface UpdateProfileRequest {
  username?: string
  phone?: string
  avatar?: string
  bio?: string
}

Step 3: Commit

git add frontend/react-shadcn/pc/src/services/api.ts frontend/react-shadcn/pc/src/types/index.ts
git commit -m "feat: add profile and dashboard API methods with types"

Task 2: 重构 SettingsPage 对接真实 API

Files:

  • Modify: frontend/react-shadcn/pc/src/pages/SettingsPage.tsx

Step 1: 添加状态管理和 API 调用

// src/pages/SettingsPage.tsx

import { useState, useEffect } from 'react'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
import { Input } from '@/components/ui/Input'
import { Button } from '@/components/ui/Button'
import { Settings, Save, Bell, Lock, Palette, Loader2 } from 'lucide-react'
import { apiClient } from '@/services/api'
import type { Profile, UpdateProfileRequest, ChangePasswordRequest } from '@/types'

export function SettingsPage() {
  // 加载状态
  const [isLoading, setIsLoading] = useState(true)
  const [isSaving, setIsSaving] = useState(false)
  const [isChangingPassword, setIsChangingPassword] = useState(false)
  const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)

  // 个人资料状态
  const [profile, setProfile] = useState<Partial<Profile>>({
    username: '',
    email: '',
    phone: '',
    bio: '',
    avatar: '',
  })

  // 密码修改状态
  const [passwordData, setPasswordData] = useState<ChangePasswordRequest>({
    oldPassword: '',
    newPassword: '',
  })
  const [confirmPassword, setConfirmPassword] = useState('')

  // 通知设置状态
  const [notifications, setNotifications] = useState({
    email: true,
    system: true,
  })

  // 加载个人资料
  useEffect(() => {
    loadProfile()
  }, [])

  const loadProfile = async () => {
    try {
      setIsLoading(true)
      const response = await apiClient.getProfile()
      if (response.success && response.data) {
        setProfile(response.data)
      }
    } catch (error) {
      setMessage({ type: 'error', text: '加载个人资料失败' })
    } finally {
      setIsLoading(false)
    }
  }

  // 保存个人资料
  const handleSaveProfile = async () => {
    try {
      setIsSaving(true)
      setMessage(null)

      const updateData: UpdateProfileRequest = {
        username: profile.username,
        phone: profile.phone,
        avatar: profile.avatar,
        bio: profile.bio,
      }

      const response = await apiClient.updateProfile(updateData)
      if (response.success) {
        setMessage({ type: 'success', text: '个人资料保存成功' })
      } else {
        setMessage({ type: 'error', text: response.message || '保存失败' })
      }
    } catch (error) {
      setMessage({ type: 'error', text: '保存个人资料失败' })
    } finally {
      setIsSaving(false)
    }
  }

  // 修改密码
  const handleChangePassword = async () => {
    // 验证密码
    if (passwordData.newPassword !== confirmPassword) {
      setMessage({ type: 'error', text: '新密码与确认密码不一致' })
      return
    }

    if (passwordData.newPassword.length < 6) {
      setMessage({ type: 'error', text: '新密码长度至少6位' })
      return
    }

    try {
      setIsChangingPassword(true)
      setMessage(null)

      const response = await apiClient.changePassword(passwordData)
      if (response.success) {
        setMessage({ type: 'success', text: '密码修改成功' })
        // 清空密码输入
        setPasswordData({ oldPassword: '', newPassword: '' })
        setConfirmPassword('')
      } else {
        setMessage({ type: 'error', text: response.message || '修改失败' })
      }
    } catch (error) {
      setMessage({ type: 'error', text: '修改密码失败,请检查当前密码是否正确' })
    } finally {
      setIsChangingPassword(false)
    }
  }

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-64">
        <Loader2 className="h-8 w-8 animate-spin text-sky-400" />
      </div>
    )
  }

  return (
    <div className="space-y-6 animate-fade-in max-w-2xl">
      {/* 消息提示 */}
      {message && (
        <div className={`p-4 rounded-lg ${message.type === 'success' ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400'}`}>
          {message.text}
        </div>
      )}

      {/* Profile Settings */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <Settings className="h-5 w-5 text-sky-400" />
            个人设置
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <Input
            label="用户名"
            placeholder="请输入用户名"
            value={profile.username || ''}
            onChange={(e) => setProfile({ ...profile, username: e.target.value })}
          />
          <Input
            label="邮箱"
            type="email"
            placeholder="请输入邮箱"
            value={profile.email || ''}
            disabled
          />
          <Input
            label="手机号"
            placeholder="请输入手机号"
            value={profile.phone || ''}
            onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
          />
          <div className="pt-4">
            <Button
              variant="primary"
              onClick={handleSaveProfile}
              disabled={isSaving}
            >
              {isSaving ? (
                <Loader2 className="h-4 w-4 animate-spin" />
              ) : (
                <Save className="h-4 w-4" />
              )}
              保存设置
            </Button>
          </div>
        </CardContent>
      </Card>

      {/* Notification Settings */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <Bell className="h-5 w-5 text-sky-400" />
            通知设置
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="flex items-center justify-between p-4 rounded-lg bg-gray-800/50">
            <div>
              <p className="text-sm font-medium text-white">邮件通知</p>
              <p className="text-xs text-gray-500">接收重要操作邮件通知</p>
            </div>
            <label className="relative inline-flex items-center cursor-pointer">
              <input
                type="checkbox"
                checked={notifications.email}
                onChange={(e) => setNotifications({ ...notifications, email: e.target.checked })}
                className="sr-only peer"
              />
              <div className="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500" />
            </label>
          </div>
          <div className="flex items-center justify-between p-4 rounded-lg bg-gray-800/50">
            <div>
              <p className="text-sm font-medium text-white">系统消息</p>
              <p className="text-xs text-gray-500">接收系统更新消息</p>
            </div>
            <label className="relative inline-flex items-center cursor-pointer">
              <input
                type="checkbox"
                checked={notifications.system}
                onChange={(e) => setNotifications({ ...notifications, system: e.target.checked })}
                className="sr-only peer"
              />
              <div className="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500" />
            </label>
          </div>
        </CardContent>
      </Card>

      {/* Security Settings */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <Lock className="h-5 w-5 text-sky-400" />
            安全设置
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <Input
            label="当前密码"
            type="password"
            placeholder="请输入当前密码"
            value={passwordData.oldPassword}
            onChange={(e) => setPasswordData({ ...passwordData, oldPassword: e.target.value })}
          />
          <Input
            label="新密码"
            type="password"
            placeholder="请输入新密码"
            value={passwordData.newPassword}
            onChange={(e) => setPasswordData({ ...passwordData, newPassword: e.target.value })}
          />
          <Input
            label="确认密码"
            type="password"
            placeholder="请确认新密码"
            value={confirmPassword}
            onChange={(e) => setConfirmPassword(e.target.value)}
          />
          <div className="pt-4">
            <Button
              variant="primary"
              onClick={handleChangePassword}
              disabled={isChangingPassword}
            >
              {isChangingPassword ? (
                <Loader2 className="h-4 w-4 animate-spin" />
              ) : null}
              修改密码
            </Button>
          </div>
        </CardContent>
      </Card>

      {/* Theme Settings */}
      <Card>
        <CardHeader>
          <CardTitle className="flex items-center gap-2">
            <Palette className="h-5 w-5 text-sky-400" />
            外观设置
          </CardTitle>
        </CardHeader>
        <CardContent className="space-y-4">
          <div className="flex items-center justify-between p-4 rounded-lg bg-gray-800/50">
            <div>
              <p className="text-sm font-medium text-white">深色模式</p>
              <p className="text-xs text-gray-500">使用深色主题</p>
            </div>
            <label className="relative inline-flex items-center cursor-pointer">
              <input type="checkbox" defaultChecked className="sr-only peer" />
              <div className="w-11 h-6 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-sky-500" />
            </label>
          </div>
        </CardContent>
      </Card>
    </div>
  )
}

Step 2: Commit

git add frontend/react-shadcn/pc/src/pages/SettingsPage.tsx
git commit -m "feat: integrate profile APIs with loading states and error handling"

Task 3: 重构 DashboardPage 使用真实数据

Files:

  • Create: backend/internal/logic/dashboard/getstatslogic.go (后端新增)
  • Modify: frontend/react-shadcn/pc/src/pages/DashboardPage.tsx

Step 1: 后端新增 Dashboard Stats API

// backend/internal/logic/dashboard/getstatslogic.go

package dashboard

import (
	"context"
	"github.com/youruser/base/internal/svc"
	"github.com/youruser/base/internal/types"
	"github.com/youruser/base/model"
	"github.com/zeromicro/go-zero/core/logx"
)

type GetStatsLogic struct {
	logx.Logger
	ctx    context.Context
	svcCtx *svc.ServiceContext
}

func NewGetStatsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetStatsLogic {
	return &GetStatsLogic{
		Logger: logx.WithContext(ctx),
		ctx:    ctx,
		svcCtx: svcCtx,
	}
}

func (l *GetStatsLogic) GetStats() (resp *types.DashboardStatsResponse, err error) {
	// 获取总用户数
	totalUsers, err := model.CountUsers(l.ctx, l.svcCtx.DB)
	if err != nil {
		return nil, err
	}

	// 获取活跃用户(7天内有登录的)
	activeUsers, err := model.CountActiveUsers(l.ctx, l.svcCtx.DB, 7)
	if err != nil {
		return nil, err
	}

	// 获取最近12个月的用户增长数据
	userGrowth, err := model.GetUserGrowthByMonth(l.ctx, l.svcCtx.DB, 12)
	if err != nil {
		return nil, err
	}

	resp = &types.DashboardStatsResponse{
		TotalUsers:  totalUsers,
		ActiveUsers: activeUsers,
		SystemLoad:  32, // 可以从系统监控获取
		DbStatus:    "正常",
		UserGrowth:  userGrowth,
	}

	return resp, nil
}

Step 2: 添加类型定义到 backend

// backend/internal/types/dashboard.go

package types

type DashboardStatsResponse struct {
	TotalUsers  int64  `json:"totalUsers"`
	ActiveUsers int64  `json:"activeUsers"`
	SystemLoad  int    `json:"systemLoad"`
	DbStatus    string `json:"dbStatus"`
	UserGrowth  []int  `json:"userGrowth"`
}

type Activity struct {
	Id     int    `json:"id"`
	User   string `json:"user"`
	Action string `json:"action"`
	Time   string `json:"time"`
	Status string `json:"status"`
}

type GetActivitiesResponse struct {
	List []Activity `json:"list"`
}

Step 3: 前端 DashboardPage 使用真实数据

// src/pages/DashboardPage.tsx

import { useState, useEffect } from 'react'
import { Users, Zap, Activity, Database, Loader2 } from 'lucide-react'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/Card'
import { apiClient } from '@/services/api'
import type { DashboardStats, Activity } from '@/types'

export function DashboardPage() {
  const [isLoading, setIsLoading] = useState(true)
  const [stats, setStats] = useState<DashboardStats | null>(null)
  const [activities, setActivities] = useState<Activity[]>([])
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    loadDashboardData()
  }, [])

  const loadDashboardData = async () => {
    try {
      setIsLoading(true)
      setError(null)

      // 并行获取统计数据和活动列表
      const [statsResponse, activitiesResponse] = await Promise.all([
        apiClient.getDashboardStats(),
        apiClient.getRecentActivities(5),
      ])

      if (statsResponse.success && statsResponse.data) {
        setStats(statsResponse.data)
      }

      if (activitiesResponse.success && activitiesResponse.data) {
        setActivities(activitiesResponse.data)
      }
    } catch (err) {
      setError('加载数据失败,请稍后重试')
      // 使用默认数据作为 fallback
      setStats({
        totalUsers: 1234,
        activeUsers: 856,
        systemLoad: 32,
        dbStatus: '正常',
        userGrowth: [65, 72, 68, 80, 75, 85, 82, 90, 88, 95, 92, 100],
      })
      setActivities([
        { id: 1, user: 'john@example.com', action: '登录系统', time: '5 分钟前', status: 'success' },
        { id: 2, user: 'jane@example.com', action: '更新资料', time: '15 分钟前', status: 'success' },
        { id: 3, user: 'admin@example.com', action: '创建用户', time: '1 小时前', status: 'success' },
        { id: 4, user: 'bob@example.com', action: '修改密码', time: '2 小时前', status: 'success' },
        { id: 5, user: 'alice@example.com', action: '登录失败', time: '3 小时前', status: 'error' },
      ])
    } finally {
      setIsLoading(false)
    }
  }

  // 格式化数字显示
  const formatNumber = (num: number): string => {
    return num.toLocaleString('zh-CN')
  }

  // 计算增长率(模拟)
  const calculateGrowth = (current: number): string => {
    const growth = ((current * 0.1) / 10).toFixed(0)
    return `+${growth}%`
  }

  const statsConfig = [
    {
      title: '总用户数',
      value: stats ? formatNumber(stats.totalUsers) : '-',
      change: calculateGrowth(stats?.totalUsers || 0),
      icon: Users,
      color: 'from-sky-500 to-blue-600',
    },
    {
      title: '活跃用户',
      value: stats ? formatNumber(stats.activeUsers) : '-',
      change: calculateGrowth(stats?.activeUsers || 0),
      icon: Activity,
      color: 'from-green-500 to-emerald-600',
    },
    {
      title: '系统负载',
      value: stats ? `${stats.systemLoad}%` : '-',
      change: '-5%',
      icon: Zap,
      color: 'from-amber-500 to-orange-600',
    },
    {
      title: '数据库状态',
      value: stats?.dbStatus || '正常',
      change: '稳定',
      icon: Database,
      color: 'from-purple-500 to-violet-600',
    },
  ]

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-96">
        <Loader2 className="h-12 w-12 animate-spin text-sky-400" />
      </div>
    )
  }

  return (
    <div className="space-y-6">
      {error && (
        <div className="p-4 bg-red-500/10 text-red-400 rounded-lg">
          {error} <button onClick={loadDashboardData} className="underline">重试</button>
        </div>
      )}

      {/* Stats Grid */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 animate-fade-in" style={{ animationDelay: '0.1s' }}>
        {statsConfig.map((stat, index) => (
          <Card key={index} className="overflow-hidden hover:border-gray-700 transition-colors">
            <CardContent className="p-6">
              <div className="flex items-start justify-between">
                <div className="space-y-1">
                  <p className="text-sm font-medium text-gray-400 font-body">{stat.title}</p>
                  <p className="text-3xl font-bold text-white font-display">{stat.value}</p>
                  <p className="text-xs text-green-400 font-body">{stat.change}</p>
                </div>
                <div className={`p-3 rounded-lg bg-gradient-to-br ${stat.color}`}>
                  <stat.icon className="h-5 w-5 text-white" />
                </div>
              </div>
            </CardContent>
          </Card>
        ))}
      </div>

      {/* Main Content Grid */}
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
        {/* Chart */}
        <Card className="lg:col-span-2 animate-fade-in" style={{ animationDelay: '0.2s' }}>
          <CardHeader>
            <CardTitle>用户增长趋势</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="h-64 flex items-end justify-between gap-2 px-4">
              {(stats?.userGrowth || [65, 72, 68, 80, 75, 85, 82, 90, 88, 95, 92, 100]).map((height, i) => (
                <div
                  key={i}
                  className="flex-1 max-w-8 bg-gradient-to-t from-sky-600 to-sky-400 rounded-t-sm transition-all duration-300 hover:from-sky-500 hover:to-sky-300 relative group"
                  style={{ height: `${height}%` }}
                >
                  <div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-gray-800 text-xs text-white px-2 py-1 rounded opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap">
                    {height}%
                  </div>
                </div>
              ))}
            </div>
            <div className="flex justify-between mt-4 px-4 text-xs text-gray-500 font-body">
              {['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'].map((month) => (
                <span key={month}>{month}</span>
              ))}
            </div>
          </CardContent>
        </Card>

        {/* Recent Activity */}
        <Card className="animate-fade-in" style={{ animationDelay: '0.3s' }}>
          <CardHeader>
            <CardTitle>最近活动</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="space-y-4">
              {activities.map((activity) => (
                <div
                  key={activity.id}
                  className="flex items-start gap-3 p-3 rounded-lg bg-gray-800/50 hover:bg-gray-800 transition-colors"
                >
                  <div
                    className={`w-2 h-2 rounded-full mt-2 flex-shrink-0 ${
                      activity.status === 'success' ? 'bg-green-500' : 'bg-red-500'
                    }`}
                  />
                  <div className="flex-1 min-w-0">
                    <p className="text-sm text-gray-300 font-body truncate">
                      {activity.user}
                    </p>
                    <p className="text-xs text-gray-500 font-body">{activity.action}</p>
                  </div>
                  <span className="text-xs text-gray-600 font-body whitespace-nowrap">
                    {activity.time}
                  </span>
                </div>
              ))}
            </div>
          </CardContent>
        </Card>
      </div>

      {/* Quick Actions */}
      <Card className="animate-fade-in" style={{ animationDelay: '0.4s' }}>
        <CardHeader>
          <CardTitle>快捷操作</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
            {[
              { label: '添加用户', icon: Users, action: 'users' },
              { label: '系统设置', icon: Zap, action: 'settings' },
              { label: '数据备份', icon: Database, action: 'backup' },
              { label: '查看日志', icon: Activity, action: 'logs' },
            ].map((item, index) => (
              <button
                key={index}
                className="flex flex-col items-center gap-2 p-4 rounded-lg border border-gray-800 hover:border-sky-500/50 hover:bg-gray-800/50 transition-all duration-200 group"
              >
                <item.icon className="h-6 w-6 text-gray-500 group-hover:text-sky-400 transition-colors" />
                <span className="text-sm text-gray-400 group-hover:text-gray-200 font-body transition-colors">
                  {item.label}
                </span>
              </button>
            ))}
          </div>
        </CardContent>
      </Card>
    </div>
  )
}

Step 4: Commit

git add backend/internal/logic/dashboard/ backend/internal/types/dashboard.go
git add frontend/react-shadcn/pc/src/pages/DashboardPage.tsx
git commit -m "feat: add dashboard stats API and integrate with frontend"

Task 4: 移除 UserManagementPage 的 mock fallback

Files:

  • Modify: frontend/react-shadcn/pc/src/pages/UserManagementPage.tsx

Step 1: 修改 fetchUsers 移除 mock fallback

// src/pages/UserManagementPage.tsx

// 修改 fetchUsers 函数
const fetchUsers = async () => {
  try {
    setIsLoading(true)
    setError(null) // 添加错误状态
    const response = await apiClient.getUsers({ page: 1, pageSize: 100 })
    if (response.success && response.data) {
      setUsers(response.data.users || [])
    } else {
      setError(response.message || '获取用户列表失败')
      setUsers([])
    }
  } catch (error) {
    console.error('Failed to fetch users:', error)
    setError('获取用户列表失败,请稍后重试')
    setUsers([])
  } finally {
    setIsLoading(false)
  }
}

// 添加错误状态
const [error, setError] = useState<string | null>(null)

Step 2: 在表格区域显示错误信息

{error && (
  <div className="p-4 mb-4 bg-red-500/10 text-red-400 rounded-lg">
    {error}
    <button
      onClick={fetchUsers}
      className="ml-2 underline hover:text-red-300"
    >
      重试
    </button>
  </div>
)}

Step 3: Commit

git add frontend/react-shadcn/pc/src/pages/UserManagementPage.tsx
git commit -m "refactor: remove mock fallback from user management, add error handling"

Task 5: 更新测试以支持真实 API

Files:

  • Modify: frontend/react-shadcn/pc/tests/config.ts
  • Modify: frontend/react-shadcn/pc/tests/*.test.ts

Step 1: 更新测试配置确保测试用户存在

// tests/config.ts

export const TEST_CONFIG = {
  // ... existing config

  // 测试前确保创建此用户
  ensureTestUser: true,

  // API 超时配置
  apiTimeout: 5000,
}

Step 2: Commit

git add frontend/react-shadcn/pc/tests/config.ts
git commit -m "test: update test config for real API integration"

Task 6: 创建 API 对接验证脚本

Files:

  • Create: frontend/react-shadcn/pc/scripts/verify-api.js

Step 1: 创建验证脚本

// scripts/verify-api.js

/**
 * API 对接验证脚本
 * 运行: node scripts/verify-api.js
 */

const API_BASE_URL = 'http://localhost:8888/api/v1';

const endpoints = [
  { method: 'POST', path: '/login', body: { email: 'admin@example.com', password: 'password123' } },
  { method: 'GET', path: '/profile/me', auth: true },
  { method: 'PUT', path: '/profile/me', auth: true, body: { username: 'admin', phone: '13800138000' } },
  { method: 'POST', path: '/profile/password', auth: true, body: { oldPassword: 'password123', newPassword: 'password123' } },
  { method: 'GET', path: '/users', auth: true },
  { method: 'POST', path: '/user', auth: true, body: { username: 'test_api', email: 'test_api@example.com', password: 'testpass123', phone: '13800138000' } },
];

async function verifyEndpoint(endpoint, token) {
  const url = `${API_BASE_URL}${endpoint.path}`;
  const headers = {
    'Content-Type': 'application/json',
  };

  if (endpoint.auth && token) {
    headers['Authorization'] = `Bearer ${token}`;
  }

  try {
    const response = await fetch(url, {
      method: endpoint.method,
      headers,
      body: endpoint.body ? JSON.stringify(endpoint.body) : undefined,
    });

    const data = await response.json();
    return {
      success: response.ok,
      status: response.status,
      data,
    };
  } catch (error) {
    return {
      success: false,
      error: error.message,
    };
  }
}

async function main() {
  console.log('🔍 API 对接验证\n');

  let token = null;

  for (const endpoint of endpoints) {
    console.log(`${endpoint.method} ${endpoint.path}`);
    const result = await verifyEndpoint(endpoint, token);

    if (result.success) {
      console.log('  ✅ 通过');
      // 保存登录 token
      if (endpoint.path === '/login' && result.data.token) {
        token = result.data.token;
        console.log('  📝 Token 已获取');
      }
    } else {
      console.log('  ❌ 失败');
      console.log(`     状态: ${result.status || 'N/A'}`);
      console.log(`     错误: ${result.error || result.data?.message || 'Unknown'}`);
    }
    console.log('');
  }
}

main();

Step 2: Commit

git add frontend/react-shadcn/pc/scripts/verify-api.js
git commit -m "feat: add API verification script"

Task 7: 更新文档

Files:

  • Modify: frontend/react-shadcn/pc/README.md
  • Modify: CLAUDE.md

Step 1: 添加 API 对接说明到 README

## API 对接状态

| 功能 | 状态 | 端点 |
|------|------|------|
| 登录 | ✅ 已对接 | POST /api/v1/login |
| 注册 | ✅ 已对接 | POST /api/v1/register |
| 用户列表 | ✅ 已对接 | GET /api/v1/users |
| 创建用户 | ✅ 已对接 | POST /api/v1/user |
| 更新用户 | ✅ 已对接 | PUT /api/v1/user/:id |
| 删除用户 | ✅ 已对接 | DELETE /api/v1/user/:id |
| 个人资料 | ✅ 已对接 | GET/PUT /api/v1/profile/me |
| 修改密码 | ✅ 已对接 | POST /api/v1/profile/password |
| 仪表板统计 | ✅ 已对接 | GET /api/v1/dashboard/stats |
| 最近活动 | ⚠️ 模拟数据 | GET /api/v1/dashboard/activities |

## 验证 API 对接

```bash
node scripts/verify-api.js

**Step 2: Commit**

```bash
git add frontend/react-shadcn/pc/README.md CLAUDE.md
git commit -m "docs: update API integration status and verification guide"

执行选项

Plan complete and saved to docs/plans/2026-02-13-frontend-api-integration.md. Two execution options:

1. Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration

2. Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints

Which approach?