feat: 完成管理后台与后端API完整联调

## 主要改进
- 修复前后端数据格式不匹配问题 (ID类型、字段名称等)
- 更新前端TypeScript类型定义匹配后端接口
- 完善API响应拦截器处理后端状态码
- 创建分类管理服务并验证CRUD功能

## 新增功能
- 添加API测试页面 (TestApi.tsx) 用于功能验证
- 更新App路由支持测试页面访问
- 统一API响应格式处理和错误提示

## 技术改进
- 前端类型系统与后端接口完全匹配
- 验证用户认证、分类管理等核心功能正常工作
- 数据库初始化完成,默认管理员账户可正常登录

## 任务进度
- 更新TASK_PROGRESS.md,新增v0.3.0版本记录
- 完成第一阶段核心功能开发,进入第二阶段管理后台完善
- 总完成率提升至19% (5/26任务完成)
This commit is contained in:
xujiang
2025-07-11 09:49:32 +08:00
parent 9c0a373728
commit b26a05f089
6 changed files with 291 additions and 125 deletions

View File

@ -12,6 +12,7 @@ import Categories from './pages/Categories'
import Tags from './pages/Tags'
import Users from './pages/Users'
import Settings from './pages/Settings'
import TestApi from './pages/TestApi'
function App() {
const { isAuthenticated } = useAuthStore()
@ -34,6 +35,7 @@ function App() {
<Route path="/photos/upload" element={<PhotoUpload />} />
<Route path="/categories" element={<Categories />} />
<Route path="/tags" element={<Tags />} />
<Route path="/test-api" element={<TestApi />} />
<Route
path="/users"
element={

165
admin/src/pages/TestApi.tsx Normal file
View File

@ -0,0 +1,165 @@
import React, { useState, useEffect } from 'react'
import { categoryService } from '@/services/categoryService'
import { authService } from '@/services/authService'
import { useAuthStore } from '@/stores/authStore'
import { toast } from 'sonner'
const TestApi: React.FC = () => {
const [categories, setCategories] = useState<any[]>([])
const [loading, setLoading] = useState(false)
const [newCategory, setNewCategory] = useState({ name: '', description: '' })
const { token, login } = useAuthStore()
// 测试登录
const testLogin = async () => {
try {
setLoading(true)
const response = await authService.login({
username: 'admin',
password: 'admin123'
})
console.log('Login response:', response)
if (response.code === 0) {
login(response.data.token, response.data.user)
toast.success('登录成功')
}
} catch (error) {
console.error('Login error:', error)
toast.error('登录失败')
} finally {
setLoading(false)
}
}
// 获取分类列表
const fetchCategories = async () => {
try {
setLoading(true)
const response = await categoryService.getCategories()
console.log('Categories response:', response)
if (response.code === 200) {
setCategories(response.data.categories || [])
toast.success('获取分类成功')
}
} catch (error) {
console.error('Fetch categories error:', error)
toast.error('获取分类失败')
} finally {
setLoading(false)
}
}
// 创建分类
const createCategory = async () => {
if (!newCategory.name) {
toast.error('请输入分类名称')
return
}
try {
setLoading(true)
const response = await categoryService.createCategory(newCategory)
console.log('Create category response:', response)
if (response.code === 200) {
toast.success('创建分类成功')
setNewCategory({ name: '', description: '' })
fetchCategories() // 重新获取分类列表
}
} catch (error) {
console.error('Create category error:', error)
toast.error('创建分类失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
if (token) {
fetchCategories()
}
}, [token])
return (
<div className="p-6 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-6">API </h1>
{/* 登录测试 */}
<div className="mb-6 p-4 border rounded-lg">
<h2 className="text-lg font-semibold mb-2"></h2>
<p className="text-sm text-gray-600 mb-2">Token: {token ? '已登录' : '未登录'}</p>
<button
onClick={testLogin}
disabled={loading}
className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
{loading ? '登录中...' : '测试登录'}
</button>
</div>
{/* 分类管理测试 */}
<div className="mb-6 p-4 border rounded-lg">
<h2 className="text-lg font-semibold mb-4"></h2>
{/* 创建分类 */}
<div className="mb-4">
<h3 className="font-medium mb-2"></h3>
<div className="flex gap-2 mb-2">
<input
type="text"
placeholder="分类名称"
value={newCategory.name}
onChange={(e) => setNewCategory(prev => ({ ...prev, name: e.target.value }))}
className="flex-1 px-3 py-2 border rounded"
/>
<input
type="text"
placeholder="分类描述"
value={newCategory.description}
onChange={(e) => setNewCategory(prev => ({ ...prev, description: e.target.value }))}
className="flex-1 px-3 py-2 border rounded"
/>
<button
onClick={createCategory}
disabled={loading || !token}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
>
</button>
</div>
</div>
{/* 获取分类列表 */}
<div className="mb-4">
<button
onClick={fetchCategories}
disabled={loading || !token}
className="px-4 py-2 bg-indigo-500 text-white rounded hover:bg-indigo-600 disabled:opacity-50"
>
{loading ? '加载中...' : '获取分类列表'}
</button>
</div>
{/* 分类列表 */}
<div>
<h3 className="font-medium mb-2"> ({categories.length})</h3>
<div className="space-y-2">
{categories.map((category) => (
<div key={category.id} className="p-3 bg-gray-50 rounded">
<div className="font-medium">{category.name}</div>
<div className="text-sm text-gray-600">{category.description}</div>
<div className="text-xs text-gray-500">
ID: {category.id} | : {new Date(category.created_at * 1000).toLocaleString()}
</div>
</div>
))}
</div>
</div>
</div>
</div>
)
}
export default TestApi

View File

@ -27,6 +27,16 @@ api.interceptors.request.use(
// 响应拦截器
api.interceptors.response.use(
(response) => {
// 处理后端的响应格式
const data = response.data
// 检查后端的业务状态码
if (data.code && data.code !== 0 && data.code !== 200) {
const message = data.message || '请求失败'
toast.error(message)
return Promise.reject(new Error(message))
}
return response
},
(error) => {

View File

@ -1,36 +1,14 @@
import api from './api'
export interface Category {
id: number
name: string
slug: string
description: string
parentId?: number
sortOrder: number
isActive: boolean
photoCount: number
createdAt: string
updatedAt: string
}
export interface CategoryTree extends Category {
children: CategoryTree[]
}
import { Category, ApiResponse, PaginatedResponse } from '@/types'
export interface CreateCategoryRequest {
name: string
slug: string
description?: string
parentId?: number
}
export interface UpdateCategoryRequest {
name?: string
slug?: string
description?: string
parentId?: number
sortOrder?: number
isActive?: boolean
}
export interface CategoryStats {
@ -41,54 +19,30 @@ export interface CategoryStats {
}
class CategoryService {
async getCategories(parentId?: number): Promise<Category[]> {
const params = parentId ? { parent_id: parentId } : {}
const response = await api.get('/categories', { params })
async getCategories(page: number = 1, size: number = 10): Promise<ApiResponse<PaginatedResponse<Category>>> {
const response = await api.get('/categories', { params: { page, size } })
return response.data
}
async getCategoryTree(): Promise<CategoryTree[]> {
const response = await api.get('/categories/tree')
return response.data
}
async getCategory(id: number): Promise<Category> {
async getCategory(id: number): Promise<ApiResponse<Category>> {
const response = await api.get(`/categories/${id}`)
return response.data
}
async getCategoryBySlug(slug: string): Promise<Category> {
const response = await api.get(`/categories/slug/${slug}`)
return response.data
}
async createCategory(data: CreateCategoryRequest): Promise<Category> {
async createCategory(data: CreateCategoryRequest): Promise<ApiResponse<Category>> {
const response = await api.post('/categories', data)
return response.data
}
async updateCategory(id: number, data: UpdateCategoryRequest): Promise<Category> {
async updateCategory(id: number, data: UpdateCategoryRequest): Promise<ApiResponse<Category>> {
const response = await api.put(`/categories/${id}`, data)
return response.data
}
async deleteCategory(id: number): Promise<void> {
await api.delete(`/categories/${id}`)
}
async reorderCategories(parentId: number | null, categoryIds: number[]): Promise<void> {
await api.post('/categories/reorder', { parentId, categoryIds })
}
async getStats(): Promise<CategoryStats> {
const response = await api.get('/categories/stats')
async deleteCategory(id: number): Promise<ApiResponse<void>> {
const response = await api.delete(`/categories/${id}`)
return response.data
}
async generateSlug(name: string): Promise<string> {
const response = await api.post('/categories/generate-slug', { name })
return response.data.slug
}
}
export const categoryService = new CategoryService()

View File

@ -1,40 +1,32 @@
// 通用类型定义
export interface User {
id: string
id: number
username: string
email: string
role: 'admin' | 'editor' | 'user'
isActive: boolean
createdAt: string
updatedAt: string
avatar: string
status: number // 1:启用 0:禁用
created_at: number
updated_at: number
}
export interface Photo {
id: string
id: number
title: string
description: string
url: string
thumbnailUrl?: string
originalFilename: string
fileSize: number
status: 'draft' | 'published' | 'archived' | 'processing'
categories: Category[]
tags: Tag[]
userId: string
createdAt: string
updatedAt: string
file_path: string
thumbnail_path?: string
user_id: number
category_id: number
created_at: number
updated_at: number
}
export interface Category {
id: string
id: number
name: string
slug: string
description?: string
parentId?: string
isActive: boolean
photoCount: number
createdAt: string
updatedAt: string
created_at: number
updated_at: number
}
export interface Tag {
@ -49,17 +41,18 @@ export interface Tag {
// API 响应类型
export interface ApiResponse<T> {
code: number
message: string
data: T
message?: string
success: boolean
}
export interface PaginatedResponse<T> {
data: T[]
total: number
page: number
limit: number
totalPages: number
size: number
photos?: T[]
categories?: T[]
users?: T[]
}
export interface PhotoListResponse extends PaginatedResponse<Photo> {