feat: 完善 CI/CD 配置并修复代码质量问题
## 修复内容 ### 前端 (Frontend) - 修复 ESLint 错误:未使用变量重命名为下划线前缀 - 修复 TypeScript 类型错误:完善 BackendPhoto 接口定义 - 修复引号转义问题:搜索结果显示优化 - 优化 useEffect 依赖:添加 useCallback 避免无限循环 - 移除未使用的导入和变量 ### 后端 (Backend) - 修复 go vet 错误:测试文件中的字段名称不匹配 - 修复数组访问错误:使用正确的结构体字段路径 - 统一代码格式:go fmt 自动格式化 ### 管理后台 (Admin) - 创建缺失的 ESLint 配置文件 - 修复 React 导入缺失问题 - 确保 TypeScript 编译通过 ## CI/CD 改进 - 验证了前端、后端、管理后台的完整构建流程 - 所有 lint 检查、类型检查、测试均通过 - 为自动化部署做好准备 ## 技术细节 - 前端:修复 5+ ESLint 错误,完善类型定义 - 后端:修复 3+ go vet 错误,通过所有测试 - 管理后台:创建 ESLint 配置,修复导入问题 - 所有模块均可正常构建和运行
This commit is contained in:
31
admin/.eslintrc.json
Normal file
31
admin/.eslintrc.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true
|
||||
},
|
||||
"extends": [
|
||||
"eslint:recommended"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"dist",
|
||||
"node_modules",
|
||||
"*.config.*"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
|
||||
],
|
||||
"no-console": "warn"
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
@ -247,7 +247,7 @@ func (suite *IntegrationTestSuite) testCategoryManagement() {
|
||||
err := json.Unmarshal(resp.Body, &createResp)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
newCategoryID := createResp.Data.ID
|
||||
newCategoryID := createResp.Data.Id
|
||||
|
||||
// 测试获取分类列表
|
||||
resp = suite.makeRequest("GET", "/api/v1/categories", nil, suite.authToken)
|
||||
@ -257,7 +257,7 @@ func (suite *IntegrationTestSuite) testCategoryManagement() {
|
||||
err = json.Unmarshal(resp.Body, &listResp)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.GreaterOrEqual(len(listResp.Data), 2)
|
||||
suite.GreaterOrEqual(len(listResp.Data.Categories), 2)
|
||||
|
||||
// 测试更新分类
|
||||
updateData := map[string]interface{}{
|
||||
@ -276,7 +276,7 @@ func (suite *IntegrationTestSuite) testCategoryManagement() {
|
||||
// testPhotoManagement 照片管理测试
|
||||
func (suite *IntegrationTestSuite) testPhotoManagement() {
|
||||
// 测试创建照片记录(简化版,不包含实际文件上传)
|
||||
photoData := map[string]interface{}{
|
||||
_ = map[string]interface{}{
|
||||
"title": "测试照片",
|
||||
"description": "这是一个测试照片",
|
||||
"file_path": "/uploads/test.jpg",
|
||||
@ -306,7 +306,7 @@ func (suite *IntegrationTestSuite) testPhotoManagement() {
|
||||
err = json.Unmarshal(resp.Body, &listResp)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
suite.GreaterOrEqual(len(listResp.Data), 1)
|
||||
suite.GreaterOrEqual(len(listResp.Data.Photos), 1)
|
||||
|
||||
// 测试获取照片详情
|
||||
resp = suite.makeRequest("GET", fmt.Sprintf("/api/v1/photos/%d", suite.photoID), nil, suite.authToken)
|
||||
|
||||
@ -87,7 +87,7 @@ func (tc *TestContext) Login(t *testing.T) {
|
||||
err = json.Unmarshal(respBody, &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
tc.authToken = resp.Token
|
||||
tc.authToken = resp.Data.Token
|
||||
}
|
||||
|
||||
// PostJSON 发送 POST JSON 请求
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { Badge } from './ui/badge'
|
||||
import { Button } from './ui/button'
|
||||
import { Alert, AlertDescription } from './ui/alert'
|
||||
@ -21,7 +21,7 @@ export function ApiStatus() {
|
||||
}
|
||||
}, [useRealApi])
|
||||
|
||||
const checkApiStatus = async () => {
|
||||
const checkApiStatus = useCallback(async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
if (useRealApi) {
|
||||
@ -38,14 +38,14 @@ export function ApiStatus() {
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [useRealApi])
|
||||
|
||||
useEffect(() => {
|
||||
checkApiStatus()
|
||||
// 每30秒检查一次API状态
|
||||
const interval = setInterval(checkApiStatus, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [useRealApi])
|
||||
}, [useRealApi, checkApiStatus])
|
||||
|
||||
const toggleApiMode = () => {
|
||||
const newMode = !useRealApi
|
||||
|
||||
@ -33,7 +33,7 @@ interface CategoryPageProps {
|
||||
}
|
||||
|
||||
export function CategoryPage({ photos, onCategorySelect, onPhotosView }: CategoryPageProps) {
|
||||
const { data: dynamicCategories = [] } = useCategories()
|
||||
const { data: _dynamicCategories = [] } = useCategories()
|
||||
const [searchQuery, setSearchQuery] = useState("")
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
|
||||
@ -273,7 +273,7 @@ export function CategoryPage({ photos, onCategorySelect, onPhotosView }: Categor
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex -space-x-2">
|
||||
{getCategoryPreviewImages(category.photos).slice(0, 3).map((photo, idx) => (
|
||||
{getCategoryPreviewImages(category.photos).slice(0, 3).map((photo, _idx) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="w-12 h-12 rounded-lg border-2 border-white overflow-hidden"
|
||||
|
||||
@ -35,7 +35,7 @@ export function FilterBar({
|
||||
const [showAdvanced, setShowAdvanced] = useState(false)
|
||||
|
||||
// 静态分类作为备选
|
||||
const staticCategories = [
|
||||
const _staticCategories = [
|
||||
{ id: "all", name: "全部作品" },
|
||||
{ id: "urban", name: "城市风光" },
|
||||
{ id: "nature", name: "自然风景" },
|
||||
@ -191,7 +191,7 @@ export function FilterBar({
|
||||
|
||||
{searchText.trim() && (
|
||||
<Badge variant="outline" className="gap-1">
|
||||
搜索: "{searchText.trim()}"
|
||||
搜索: “{searchText.trim()}”
|
||||
<X
|
||||
className="h-3 w-3 cursor-pointer"
|
||||
onClick={handleClearSearch}
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
Tag,
|
||||
TrendingUp,
|
||||
Hash,
|
||||
Filter,
|
||||
ArrowRight,
|
||||
Camera,
|
||||
Sparkles
|
||||
@ -95,7 +94,7 @@ export function TagCloud({ photos, onTagSelect, onPhotosView }: TagCloudProps) {
|
||||
|
||||
// 过滤和排序标签
|
||||
const filteredTags = useMemo(() => {
|
||||
let filtered = tagStats.filter(tag =>
|
||||
const filtered = tagStats.filter(tag =>
|
||||
tag.count >= minCount &&
|
||||
(searchQuery.trim() === '' || tag.name.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
)
|
||||
@ -237,7 +236,7 @@ export function TagCloud({ photos, onTagSelect, onPhotosView }: TagCloudProps) {
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as any)}
|
||||
onChange={(e) => setSortBy(e.target.value as 'popularity' | 'alphabetical' | 'recent')}
|
||||
className="px-3 py-2 border border-gray-300 rounded-md text-sm"
|
||||
>
|
||||
<option value="popularity">按热度</option>
|
||||
@ -324,7 +323,7 @@ export function TagCloud({ photos, onTagSelect, onPhotosView }: TagCloudProps) {
|
||||
|
||||
{/* 最近照片预览 */}
|
||||
<div className="flex -space-x-2 mt-3">
|
||||
{tag.recentPhotos.slice(0, 3).map((photo, idx) => (
|
||||
{tag.recentPhotos.slice(0, 3).map((photo, _idx) => (
|
||||
<div
|
||||
key={photo.id}
|
||||
className="w-8 h-8 rounded-full border-2 border-white overflow-hidden"
|
||||
|
||||
@ -8,7 +8,7 @@ class CategoryService {
|
||||
// 获取所有分类
|
||||
async getAllCategories(): Promise<Category[]> {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
|
||||
const response: any = await api.get('/categories?page=1&page_size=100')
|
||||
const response: { categories: Category[] } = await api.get('/categories?page=1&page_size=100')
|
||||
return response?.categories || []
|
||||
} else {
|
||||
// Mock API 返回字符串数组,需要转换
|
||||
|
||||
@ -58,27 +58,54 @@ export const queryKeys = {
|
||||
categories: ['categories'] as const,
|
||||
}
|
||||
|
||||
// 后端照片数据结构
|
||||
interface BackendPhoto {
|
||||
id: number
|
||||
title?: string
|
||||
description?: string
|
||||
src?: string
|
||||
url?: string
|
||||
image_path?: string
|
||||
file_path?: string
|
||||
thumbnail_path?: string
|
||||
category?: string
|
||||
category_id?: number
|
||||
user_id?: number
|
||||
tags?: string[]
|
||||
date?: string
|
||||
created_at?: number
|
||||
updated_at?: number
|
||||
exif?: {
|
||||
camera?: string
|
||||
lens?: string
|
||||
settings?: string
|
||||
location?: string
|
||||
}
|
||||
}
|
||||
|
||||
// 数据转换工具
|
||||
const transformPhoto = async (backendPhoto: any): Promise<Photo> => {
|
||||
const transformPhoto = async (backendPhoto: BackendPhoto): Promise<Photo> => {
|
||||
// 如果使用Mock API,直接返回
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
|
||||
return {
|
||||
...backendPhoto,
|
||||
id: backendPhoto.id,
|
||||
title: backendPhoto.title || '无标题',
|
||||
description: backendPhoto.description || '',
|
||||
src: backendPhoto.src || '/placeholder.jpg',
|
||||
category: backendPhoto.category || 'general',
|
||||
tags: backendPhoto.tags || [],
|
||||
date: backendPhoto.date || new Date().toISOString().split('T')[0],
|
||||
exif: backendPhoto.exif || {
|
||||
camera: '未知',
|
||||
lens: '未知',
|
||||
settings: '未知',
|
||||
location: '未知'
|
||||
exif: {
|
||||
camera: backendPhoto.exif?.camera || '未知',
|
||||
lens: backendPhoto.exif?.lens || '未知',
|
||||
settings: backendPhoto.exif?.settings || '未知',
|
||||
location: backendPhoto.exif?.location || '未知'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取分类名称
|
||||
const categoryName = await categoryService.getCategoryName(backendPhoto.category_id)
|
||||
const categoryName = await categoryService.getCategoryName(backendPhoto.category_id || 1)
|
||||
|
||||
// 转换后端API数据格式
|
||||
return {
|
||||
@ -88,7 +115,7 @@ const transformPhoto = async (backendPhoto: any): Promise<Photo> => {
|
||||
src: backendPhoto.file_path ? `http://localhost:8080${backendPhoto.file_path}` : '/placeholder.jpg',
|
||||
category: categoryName,
|
||||
tags: [], // 后端暂无标签系统,使用空数组
|
||||
date: new Date(backendPhoto.created_at * 1000).toISOString().split('T')[0],
|
||||
date: new Date((backendPhoto.created_at || Date.now() / 1000) * 1000).toISOString().split('T')[0],
|
||||
exif: {
|
||||
camera: '未知',
|
||||
lens: '未知',
|
||||
@ -105,11 +132,11 @@ const transformPhoto = async (backendPhoto: any): Promise<Photo> => {
|
||||
}
|
||||
}
|
||||
|
||||
const transformCategory = (backendCategory: any): string => {
|
||||
const _transformCategory = (backendCategory: Category | string): string => {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API !== 'true') {
|
||||
return backendCategory
|
||||
return typeof backendCategory === 'string' ? backendCategory : backendCategory.name
|
||||
}
|
||||
return backendCategory.name
|
||||
return typeof backendCategory === 'string' ? backendCategory : backendCategory.name
|
||||
}
|
||||
|
||||
// 获取所有照片
|
||||
@ -119,13 +146,13 @@ export const usePhotos = () => {
|
||||
queryFn: async (): Promise<Photo[]> => {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
|
||||
// 使用真实API,带分页参数
|
||||
const response: any = await api.get('/photos?page=1&page_size=100')
|
||||
const response: { photos: BackendPhoto[] } = await api.get('/photos?page=1&page_size=100')
|
||||
const photos = response?.photos || []
|
||||
// 并发处理所有照片的转换
|
||||
return Promise.all(photos.map(transformPhoto))
|
||||
} else {
|
||||
// 使用Mock API
|
||||
const photos: any[] = await api.get('/photos')
|
||||
const photos: BackendPhoto[] = await api.get('/photos')
|
||||
return Promise.all(photos.map(transformPhoto))
|
||||
}
|
||||
},
|
||||
@ -139,7 +166,7 @@ export const usePhotosPaginated = (page: number = 1, pageSize: number = 12) => {
|
||||
queryKey: [...queryKeys.photos, 'paginated', page, pageSize],
|
||||
queryFn: async (): Promise<{ photos: Photo[], total: number, hasMore: boolean }> => {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
|
||||
const response: any = await api.get(`/photos?page=${page}&page_size=${pageSize}`)
|
||||
const response: { photos: BackendPhoto[], total: number } = await api.get(`/photos?page=${page}&page_size=${pageSize}`)
|
||||
const photos = response?.photos || []
|
||||
const total = response?.total || 0
|
||||
const transformedPhotos = await Promise.all(photos.map(transformPhoto))
|
||||
@ -150,7 +177,7 @@ export const usePhotosPaginated = (page: number = 1, pageSize: number = 12) => {
|
||||
}
|
||||
} else {
|
||||
// 使用Mock API - 模拟分页
|
||||
const allPhotos: any[] = await api.get('/photos')
|
||||
const allPhotos: BackendPhoto[] = await api.get('/photos')
|
||||
const startIndex = (page - 1) * pageSize
|
||||
const endIndex = startIndex + pageSize
|
||||
const paginatedPhotos = allPhotos.slice(startIndex, endIndex)
|
||||
@ -167,17 +194,17 @@ export const usePhotosPaginated = (page: number = 1, pageSize: number = 12) => {
|
||||
}
|
||||
|
||||
// 无限滚动照片查询
|
||||
export const useInfinitePhotos = (pageSize: number = 12) => {
|
||||
export const useInfinitePhotos = (_pageSize: number = 12) => {
|
||||
return useQuery({
|
||||
queryKey: [...queryKeys.photos, 'infinite'],
|
||||
queryFn: async (): Promise<Photo[]> => {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
|
||||
// 获取所有照片用于前端分页
|
||||
const response: any = await api.get('/photos?page=1&page_size=200')
|
||||
const response: { photos: BackendPhoto[] } = await api.get('/photos?page=1&page_size=200')
|
||||
const photos = response?.photos || []
|
||||
return Promise.all(photos.map(transformPhoto))
|
||||
} else {
|
||||
const photos: any[] = await api.get('/photos')
|
||||
const photos: BackendPhoto[] = await api.get('/photos')
|
||||
return Promise.all(photos.map(transformPhoto))
|
||||
}
|
||||
},
|
||||
@ -191,7 +218,7 @@ export const usePhoto = (id: number) => {
|
||||
queryKey: queryKeys.photo(id),
|
||||
queryFn: async (): Promise<Photo> => {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
|
||||
const response = await api.get(`/photos/${id}`)
|
||||
const response: BackendPhoto = await api.get(`/photos/${id}`)
|
||||
return await transformPhoto(response)
|
||||
} else {
|
||||
return api.get(`/photos/${id}`)
|
||||
@ -207,7 +234,7 @@ export const useCategories = () => {
|
||||
queryKey: queryKeys.categories,
|
||||
queryFn: async (): Promise<string[]> => {
|
||||
if (process.env.NEXT_PUBLIC_USE_REAL_API === 'true') {
|
||||
const response: any = await api.get('/categories?page=1&page_size=100')
|
||||
const response: { categories: Category[] } = await api.get('/categories?page=1&page_size=100')
|
||||
const categories = response?.categories || []
|
||||
return categories.map((cat: Category) => cat.name)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user