完成主页, 作品页,作品编辑页
> 未对接后端接口
Change-Id: I5d62663602656da4940707e00f76bfe09d824c2c
diff --git a/src/api/User/UserApi.ts b/src/api/User/UserApi.ts
index 56152fc..cd7aa55 100644
--- a/src/api/User/UserApi.ts
+++ b/src/api/User/UserApi.ts
@@ -1,12 +1,17 @@
import type { AxiosResponse } from "axios";
import axios from "axios";
-import type { UserInfo } from "./type";
+import type { UserDetailInfo, UserInfo } from "./type";
import type { CommonResponse } from "../type";
class UserAPi {
static getMe() :Promise<AxiosResponse<CommonResponse<UserInfo>>> {
return axios.get('/api/user/me');
}
+
+ static getMeDetail(userid:string) :Promise<AxiosResponse<CommonResponse<UserDetailInfo>>> {
+ return axios.get(`/api/user/${userid}`);
+ }
}
+
export default UserAPi;
\ No newline at end of file
diff --git a/src/api/User/type.ts b/src/api/User/type.ts
index a7a1503..1410aa8 100644
--- a/src/api/User/type.ts
+++ b/src/api/User/type.ts
@@ -6,5 +6,5 @@
export interface UserDetailInfo {
userid: string,
username: string,
- // ...
+ email: string,
}
\ No newline at end of file
diff --git a/src/api/categoryApi.ts b/src/api/categoryApi.ts
deleted file mode 100644
index 816ac71..0000000
--- a/src/api/categoryApi.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import axios from 'axios';
-import type { CategoryDTO, Category, WorkResponse } from './otherType';
-
-const API_BASE_URL = '/api';
-
-class CategoryAPI {
- /**
- * 获取全部分区(树形结构)
- * @returns 返回分区的树形结构数据
- */
- static getCategoryTree(): Promise<CategoryDTO[]> {
- return axios.get<CategoryDTO[]>(`${API_BASE_URL}/categories`)
- .then(response => response.data);
- }
-
- /**
- * 获取单个分区详情
- * @param id 分区ID
- * @returns 返回单个分区的详细信息
- */
- static getCategory(id: number): Promise<Category> {
- return axios.get<Category>(`${API_BASE_URL}/categories/${id}`)
- .then(response => response.data);
- }
-
- /**
- * 创建新分区
- * @param category 分区数据(不包含ID)
- * @returns 返回创建成功的消息
- */
- static createCategory(category: Omit<Category, 'id'>): Promise<string> {
- return axios.post<string>(`${API_BASE_URL}/categories`, category)
- .then(response => response.data);
- }
-
- /**
- * 更新分区信息
- * @param id 分区ID
- * @param category 更新后的分区数据
- * @returns 返回更新成功的消息
- */
- static updateCategory(id: number, category: Partial<Category>): Promise<string> {
- return axios.put<string>(`${API_BASE_URL}/categories/${id}`, category)
- .then(response => response.data);
- }
-
- /**
- * 删除分区
- * @param id 分区ID
- * @returns 返回删除成功的消息
- */
- static deleteCategory(id: number): Promise<string> {
- return axios.delete<string>(`${API_BASE_URL}/categories/${id}`)
- .then(response => response.data);
- }
-
- /**
- * 获取分区下的作品列表(带分页)
- * @param categoryId 分区ID
- * @param params 分页和排序参数
- * @returns 返回作品列表和分页信息
- */
- static getWorksByCategory(
- categoryId: number,
- params?: {
- page?: number;
- size?: number;
- sort?: string;
- }
- ): Promise<WorkResponse> {
- return axios.get<WorkResponse>(`${API_BASE_URL}/works`, {
- params: {
- categoryId,
- ...params
- }
- }).then(response => response.data);
- }
-
- /**
- * 获取子分区列表
- * @param parentId 父分区ID
- * @returns 返回子分区列表
- */
- static getSubCategories(parentId: number): Promise<CategoryDTO[]> {
- return axios.get<CategoryDTO[]>(`${API_BASE_URL}/categories/${parentId}/children`)
- .then(response => response.data);
- }
-}
-
-export default CategoryAPI;
\ No newline at end of file
diff --git a/src/api/categoryTypes.ts b/src/api/categoryTypes.ts
deleted file mode 100644
index 4a46034..0000000
--- a/src/api/categoryTypes.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { ReactNode } from "react";
-
-// 基础分类接口
-export interface CategoryDTO {
- id: number;
- name: string;
- description?: string;
- parentId?: number; // 移除了 null 类型,只保留 number | undefined
- children?: CategoryDTO[];
- workCount?: number;
-}
-
-
-// 基础资源接口
-interface BaseResource {
- id: number;
- title: string;
- categoryId: number;
- categoryName: string;
- uploadDate: string;
- uploader: string;
- size: string;
- seeders: number;
- leechers: number;
- downloadCount: number;
- hash: string;
-}
-
-// 音乐资源
-export interface MusicResource extends BaseResource {
- category: ReactNode;
- artist: string;
- quality: 'MP3 128k' | 'MP3 320k' | 'FLAC' | 'APE' | 'Hi-Res' | 'DSD';
- genre: string[];
-}
-
-// 影视资源
-export interface MovieResource extends BaseResource {
- director: string;
- actors: string[];
- resolution: '1080p' | '720p' | '4K' | '8K';
- duration: string;
-}
-
-// 游戏资源
-export interface GameResource extends BaseResource {
- developer: string;
- publisher: string;
- platform: 'PC' | 'PS' | 'Xbox' | 'Switch';
- releaseDate: string;
-}
-
-// 其他资源
-export interface OtherResource extends BaseResource {
- description: string;
- fileType: string;
-}
-
-// 分类DTO
-export interface CategoryDTO {
- id: number;
- name: string;
- description?: string;
- parentId?: number;
- children?: CategoryDTO[];
- workCount?: number;
-}
\ No newline at end of file
diff --git a/src/api/commentApi.ts b/src/api/commentApi.ts
deleted file mode 100644
index 360e659..0000000
--- a/src/api/commentApi.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import axios from 'axios';
-import type { Comment, CommentFormData, CommentReply, ReplyFormData } from './otherType';
-
-const API_BASE_URL = '/api';
-
-class CommentAPI {
- /**
- * 添加评论
- */
- static addComment(data: CommentFormData): Promise<Comment> {
- return axios.post<Comment>(`${API_BASE_URL}/comments`, data)
- .then(response => response.data);
- }
-
- /**
- * 删除评论
- */
- static deleteComment(commentId: number): Promise<void> {
- return axios.delete(`${API_BASE_URL}/comments/${commentId}`);
- }
-
- /**
- * 更新评论
- */
- static updateComment(commentId: number, data: CommentFormData): Promise<Comment> {
- return axios.put<Comment>(`${API_BASE_URL}/comments/${commentId}`, data)
- .then(response => response.data);
- }
-
- /**
- * 获取单个评论
- */
- static getCommentById(commentId: number): Promise<Comment> {
- return axios.get<Comment>(`${API_BASE_URL}/comments/${commentId}`)
- .then(response => response.data);
- }
-
- /**
- * 获取帖子下的所有评论
- */
- static getCommentsByPostId(postId: number): Promise<Comment[]> {
- return axios.get<Comment[]>(`${API_BASE_URL}/comments/post/${postId}`)
- .then(response => response.data);
- }
-
- /**
- * 添加回复
- */
- static addReply(commentId: number, data: ReplyFormData): Promise<CommentReply> {
- return axios.post<CommentReply>(`${API_BASE_URL}/comments/${commentId}/replies`, data)
- .then(response => response.data);
- }
-
- /**
- * 获取评论的所有回复
- */
- static getReplies(commentId: number): Promise<CommentReply[]> {
- return axios.get<CommentReply[]>(`${API_BASE_URL}/comments/${commentId}/replies`)
- .then(response => response.data);
- }
-
- /**
- * 点赞评论
- */
- static likeComment(commentId: number): Promise<void> {
- return axios.post(`${API_BASE_URL}/comments/${commentId}/like`);
- }
-
- /**
- * 举报评论
- */
- static reportComment(commentId: number, reason: string): Promise<void> {
- return axios.post(`${API_BASE_URL}/comments/${commentId}/report`, { reason });
- }
-}
-
-export default CommentAPI;
\ No newline at end of file
diff --git a/src/api/interceptors.ts b/src/api/interceptors.ts
index 3c0863b..04bdcb3 100644
--- a/src/api/interceptors.ts
+++ b/src/api/interceptors.ts
@@ -26,7 +26,7 @@
code,
message: msg,
data,
- success: code === 0, // 根据 code 判断请求是否成功
+ ...response.data
},
};
},
diff --git a/src/api/otherType.ts b/src/api/otherType.ts
deleted file mode 100644
index 60523d0..0000000
--- a/src/api/otherType.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-// 认证相关类型
-export interface LoginRequest {
- email: string;
- password: string;
-}
-
-export interface LoginResponse {
- user: string;
- token: string;
- refreshToken: string;
-}
-
-export interface CommonResponse<T= null> {
- code: number;
- message: string;
- data: T;
-}
-
-// 作品相关类型
-export interface Work {
- size: string;
- data(data: any): unknown;
- artist: string;
- quality: string;
- genre: string[];
- id: number;
- title: string;
- author: string;
- authorId: number;
- views: number;
- likes: number;
- categoryId: number;
- categoryName: string;
- description: string;
- content: string;
- coverUrl: string;
- createTime: string;
- attachments: Attachment[];
- bugCount: number;
- discussionCount: number;
-}
-
-export interface WorkResponse {
- content: Work[];
- totalElements: number;
- totalPages: number;
-}
-
-export interface WorkFormData {
- title: string;
- categoryId: number;
- description: string;
- content: string;
- coverFile?: File;
- attachments?: File[];
-}
-
-// Bug反馈相关类型
-export interface BugReport {
- id: number;
- title: string;
- description: string;
- severity: 'low' | 'medium' | 'high';
- status: 'open' | 'resolved' | 'wontfix';
- reporter: string;
- reporterId: number;
- workId: number;
- createTime: string;
-}
-
-// 讨论区相关类型
-export interface Discussion {
- id: number;
- title: string;
- content: string;
- author: string;
- authorId: number;
- workId: number;
- isPinned: boolean;
- commentCount: number;
- createTime: string;
- lastUpdateTime: string;
-}
-
-export interface DiscussionFormData {
- workId: number;
- title: string;
- content: string;
-}
-
-// 评论相关类型
-export interface Comment {
- id: number;
- author: string;
- avatar: string;
- content: string;
- likes: number;
- replyCount: number;
- createTime: string;
- replies?: CommentReply[];
-}
-
-export interface CommentFormData {
- postId: number; // 可以是作品ID或讨论区ID
- content: string;
- parentId?: number; // 用于回复评论
-}
-
-export interface CommentReply {
- id: number;
- author: string;
- avatar: string;
- content: string;
- likes: number;
- createTime: string;
-}
-
-export interface ReplyFormData {
- content: string;
-}
-
-// 分区相关类型
-export interface Category {
- id: number;
- name: string;
- description?: string;
- parentId?: number | null;
- coverUrl?: string;
- createdAt?: string;
- updatedAt?: string;
-}
-
-export interface CategoryDTO extends Category {
- children?: CategoryDTO[];
- workCount?: number;
-}
-
-// 用户相关类型
-export interface User {
- id: number;
- username: string;
- email: string;
- avatar?: string;
- role: 'user' | 'admin';
- createdAt: string;
- worksCount: number;
- completedWorks: number;
-}
-
-// 通用类型
-export interface Attachment {
- id: number;
- name: string;
- url: string;
- size: string;
-}
-
-// API响应通用类型
-export interface ApiResponse<T> {
- success: boolean;
- data?: T;
- message?: string;
- error?: string;
-}
\ No newline at end of file
diff --git a/src/api/workApi.ts b/src/api/workApi.ts
deleted file mode 100644
index d624821..0000000
--- a/src/api/workApi.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import axios from 'axios';
-import type { Work, WorkResponse, WorkFormData, BugReport, Discussion } from './otherType';
-
-const API_BASE_URL = '/api';
-
-class WorkAPI {
- /**
- * 获取作品列表(支持分页、分类筛选和搜索)
- */
- static getWorks(params?: {
- categoryId?: number;
- page?: number;
- size?: number;
- search?: string;
- }): Promise<WorkResponse> {
- return axios.get<WorkResponse>(`${API_BASE_URL}/works`, { params })
- .then(response => response.data);
- }
-
- /**
- * 获取单个作品详情
- */
- static getWorkById(id: number): Promise<Work> {
- return axios.get<Work>(`${API_BASE_URL}/works/${id}`)
- .then(response => response.data);
- }
-
- /**
- * 创建新作品
- */
- static createWork(data: WorkFormData): Promise<Work> {
- return axios.post<Work>(`${API_BASE_URL}/works`, data)
- .then(response => response.data);
- }
-
- /**
- * 更新作品信息
- */
- static updateWork(id: number, data: WorkFormData): Promise<Work> {
- return axios.put<Work>(`${API_BASE_URL}/works/${id}`, data)
- .then(response => response.data);
- }
-
- /**
- * 删除作品
- */
- static deleteWork(id: number): Promise<void> {
- return axios.delete(`${API_BASE_URL}/works/${id}`);
- }
-
- /**
- * 点赞作品
- */
- static likeWork(id: number): Promise<void> {
- return axios.post(`${API_BASE_URL}/works/${id}/like`);
- }
-
- /**
- * 根据作者查询作品
- */
- static getWorksByAuthor(author: string): Promise<Work[]> {
- return axios.get<Work[]>(`${API_BASE_URL}/works/byAuthor`, { params: { author } })
- .then(response => response.data);
- }
-
- /**
- * 获取作品Bug反馈列表
- */
- static getBugReports(workId: number): Promise<BugReport[]> {
- return axios.get<BugReport[]>(`${API_BASE_URL}/works/${workId}/bugs`)
- .then(response => response.data);
- }
-
- /**
- * 提交Bug反馈
- */
- static reportBug(workId: number, data: {
- title: string;
- description: string;
- severity: 'low' | 'medium' | 'high';
- }): Promise<void> {
- return axios.post(`${API_BASE_URL}/works/${workId}/bugs`, data);
- }
-
- /**
- * 获取作品讨论区列表
- */
- static getDiscussions(workId: number): Promise<Discussion[]> {
- return axios.get<Discussion[]>(`${API_BASE_URL}/works/${workId}/discussions`)
- .then(response => response.data);
- }
-
- /**
- * 创建新讨论
- */
- static createDiscussion(data: {
- workId: number;
- title: string;
- content: string;
- }): Promise<void> {
- return axios.post(`${API_BASE_URL}/discussions`, data);
- }
-}
-
-export default WorkAPI;
\ No newline at end of file
diff --git a/src/assets/categories/film.jpg b/src/assets/categories/film.jpg
deleted file mode 100644
index 3d05b4d..0000000
--- a/src/assets/categories/film.jpg
+++ /dev/null
Binary files differ
diff --git a/src/assets/categories/game.jpg b/src/assets/categories/game.jpg
deleted file mode 100644
index 15571fe..0000000
--- a/src/assets/categories/game.jpg
+++ /dev/null
Binary files differ
diff --git a/src/assets/categories/music.jpg b/src/assets/categories/music.jpg
deleted file mode 100644
index 47dda2c..0000000
--- a/src/assets/categories/music.jpg
+++ /dev/null
Binary files differ
diff --git a/src/assets/categories/other.jpg b/src/assets/categories/other.jpg
deleted file mode 100644
index b7385c0..0000000
--- a/src/assets/categories/other.jpg
+++ /dev/null
Binary files differ
diff --git a/src/components/BugReportSection.tsx b/src/components/BugReportSection.tsx
deleted file mode 100644
index a512fc6..0000000
--- a/src/components/BugReportSection.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { List, Button, Form, Input, Select, message, Typography, Tag, Divider, Pagination } from 'antd';
-import { BugOutlined } from '@ant-design/icons';
-import WorkAPI from '../api/workApi';
-import type { BugReport } from '../api/otherType';
-
-const { Text } = Typography;
-const { Option } = Select;
-const { TextArea } = Input;
-
-interface BugReportForm {
- title: string;
- description: string;
- severity: 'low' | 'medium' | 'high';
-}
-
-const BugReportSection: React.FC<{ workId: number }> = ({ workId }) => {
- const [bugs, setBugs] = useState<BugReport[]>([]);
- const [loading, setLoading] = useState(false);
- const [form] = Form.useForm();
- const [currentPage, setCurrentPage] = useState(1);
- const [pageSize, setPageSize] = useState(5); // 每页显示5条
-
- // 加载Bug反馈列表
- useEffect(() => {
- const loadBugs = async () => {
- try {
- setLoading(true);
- const bugList = await WorkAPI.getBugReports(workId);
- setBugs(bugList);
- } catch (error) {
- message.error('加载Bug反馈失败');
- } finally {
- setLoading(false);
- }
- };
-
- loadBugs();
- }, [workId]);
-
- // 获取当前页的Bug数据
- const getCurrentPageBugs = () => {
- const startIndex = (currentPage - 1) * pageSize;
- const endIndex = startIndex + pageSize;
- return bugs.slice(startIndex, endIndex);
- };
-
- // 提交Bug反馈
- const handleSubmit = async (values: BugReportForm) => {
- try {
- await WorkAPI.reportBug(workId, values);
- message.success('Bug反馈提交成功');
- form.resetFields();
- // 重新加载Bug列表
- const bugList = await WorkAPI.getBugReports(workId);
- setBugs(bugList);
- // 重置到第一页
- setCurrentPage(1);
- } catch (error) {
- message.error('Bug反馈提交失败');
- }
- };
-
- const getSeverityColor = (severity: string) => {
- switch (severity) {
- case 'high': return 'red';
- case 'medium': return 'orange';
- default: return 'green';
- }
- };
-
- return (
- <div className="bug-report-section">
- <Divider orientation="left">
- <BugOutlined /> Bug反馈
- </Divider>
-
- <Form form={form} onFinish={handleSubmit} layout="vertical">
- <Form.Item
- name="title"
- label="Bug标题"
- rules={[{ required: true, message: '请输入Bug标题' }]}
- >
- <Input placeholder="简要描述Bug" />
- </Form.Item>
-
- <Form.Item
- name="severity"
- label="严重程度"
- initialValue="medium"
- rules={[{ required: true }]}
- >
- <Select>
- <Option value="low">低</Option>
- <Option value="medium">中</Option>
- <Option value="high">高</Option>
- </Select>
- </Form.Item>
-
- <Form.Item
- name="description"
- label="详细描述"
- rules={[{ required: true, message: '请详细描述Bug' }]}
- >
- <TextArea rows={4} placeholder="请详细描述Bug出现的步骤、现象和预期结果" />
- </Form.Item>
-
- <Form.Item>
- <Button type="primary" htmlType="submit">
- 提交Bug
- </Button>
- </Form.Item>
- </Form>
-
- <Divider orientation="left">Bug列表</Divider>
-
- <List
- dataSource={getCurrentPageBugs()}
- loading={loading}
- renderItem={bug => (
- <List.Item>
- <div className="bug-item">
- <div className="bug-header">
- <Text strong>{bug.title}</Text>
- <Tag color={getSeverityColor(bug.severity)}>
- {bug.severity === 'high' ? '严重' : bug.severity === 'medium' ? '中等' : '轻微'}
- </Tag>
- </div>
- <Text type="secondary">报告人: {bug.reporter} | 时间: {new Date(bug.createTime).toLocaleString()}</Text>
- <div className="bug-content">
- <Text>{bug.description}</Text>
- </div>
- <Tag color={bug.status === 'open' ? 'red' : bug.status === 'resolved' ? 'green' : 'gray'}>
- {bug.status === 'open' ? '未解决' : bug.status === 'resolved' ? '已解决' : '不予解决'}
- </Tag>
- </div>
- </List.Item>
- )}
- />
-
- <div style={{ textAlign: 'right', marginTop: 16 }}>
- <Pagination
- current={currentPage}
- pageSize={pageSize}
- total={bugs.length}
- onChange={(page, size) => {
- setCurrentPage(page);
- setPageSize(size);
- }}
- showSizeChanger
- showQuickJumper
- showTotal={total => `共 ${total} 条`}
- />
- </div>
- </div>
- );
-};
-
-export default BugReportSection;
\ No newline at end of file
diff --git a/src/components/CustomComment.css b/src/components/CustomComment.css
deleted file mode 100644
index 8f9a012..0000000
--- a/src/components/CustomComment.css
+++ /dev/null
@@ -1,54 +0,0 @@
-/* src/components/CustomComment.css */
-.custom-comment {
- margin-bottom: 16px;
- padding: 16px 0;
- border-bottom: 1px solid #f0f0f0;
-}
-
-.custom-comment:last-child {
- border-bottom: none;
-}
-
-.custom-comment-inner {
- display: flex;
-}
-
-.custom-comment-avatar {
- margin-right: 12px;
-}
-
-.custom-comment-content {
- flex: 1;
-}
-
-.custom-comment-header {
- margin-bottom: 8px;
- display: flex;
- align-items: center;
-}
-
-.custom-comment-author {
- font-weight: 500;
- color: rgba(0, 0, 0, 0.85);
-}
-
-.custom-comment-datetime {
- margin-left: 12px;
- font-size: 12px;
- color: rgba(0, 0, 0, 0.45);
-}
-
-.custom-comment-text {
- margin-bottom: 8px;
- color: rgba(0, 0, 0, 0.85);
- white-space: pre-line;
-}
-
-.custom-comment-actions {
- margin-top: 8px;
-}
-
-.custom-comment-action {
- padding: 0 4px;
- font-size: 12px;
-}
\ No newline at end of file
diff --git a/src/components/DiscussionSection.tsx b/src/components/DiscussionSection.tsx
deleted file mode 100644
index 7fd39c4..0000000
--- a/src/components/DiscussionSection.tsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { List, Button, Form, Input, message, Avatar, Typography, Modal, Pagination } from 'antd';
-import { LikeOutlined, MessageOutlined } from '@ant-design/icons';
-import CommentAPI from '../api/commentApi';
-import type { Comment } from '../api/otherType';
-
-const { Text } = Typography;
-const { TextArea } = Input;
-
-interface DiscussionSectionProps {
- workId: number;
-}
-
-const DiscussionSection: React.FC<DiscussionSectionProps> = ({ workId }) => {
- const [comments, setComments] = useState<Comment[]>([]);
- const [loading, setLoading] = useState(false);
- const [modalVisible, setModalVisible] = useState(false);
- const [form] = Form.useForm();
- const [currentPage, setCurrentPage] = useState(1);
- const [pageSize, setPageSize] = useState(5); // 每页显示5条
-
- // 加载评论
- useEffect(() => {
- const loadComments = async () => {
- try {
- setLoading(true);
- const commentList = await CommentAPI.getCommentsByPostId(workId);
- setComments(commentList);
- } catch (error) {
- message.error('加载评论失败');
- } finally {
- setLoading(false);
- }
- };
-
- loadComments();
- }, [workId]);
-
- // 获取当前页的评论数据
- const getCurrentPageComments = () => {
- const startIndex = (currentPage - 1) * pageSize;
- const endIndex = startIndex + pageSize;
- return comments.slice(startIndex, endIndex);
- };
-
- // 提交新评论
- const handleSubmit = async (values: { title: string; content: string }) => {
- try {
- await CommentAPI.addComment({
- postId: workId,
- content: values.content
- });
- message.success('评论添加成功');
- form.resetFields();
- setModalVisible(false);
- // 重新加载评论
- const commentList = await CommentAPI.getCommentsByPostId(workId);
- setComments(commentList);
- // 重置到第一页
- setCurrentPage(1);
- } catch (error) {
- message.error('评论添加失败');
- }
- };
-
- // 点赞评论
- const handleLikeComment = async (commentId: number) => {
- try {
- await CommentAPI.likeComment(commentId);
- setComments(prev => prev.map(comment =>
- comment.id === commentId
- ? { ...comment, likes: (comment.likes || 0) + 1 }
- : comment
- ));
- } catch (error) {
- message.error('点赞失败');
- }
- };
-
- return (
- <div className="discussion-section">
- <Button
- type="primary"
- icon={<MessageOutlined />}
- onClick={() => setModalVisible(true)}
- style={{ marginBottom: 16 }}
- >
- 新建讨论
- </Button>
-
- <List
- dataSource={getCurrentPageComments()}
- loading={loading}
- renderItem={comment => (
- <List.Item className="comment-item">
- <div className="comment-content">
- <div className="comment-header">
- <Avatar size="small">
- {comment.author?.charAt(0) || '?'}
- </Avatar>
- <Text strong>{comment.author || '匿名用户'}</Text>
- <Text type="secondary">
- {comment.createTime ? new Date(comment.createTime).toLocaleString() : '未知时间'}
- </Text>
- </div>
- <div className="comment-body">
- <Text>{comment.content}</Text>
- </div>
- <div className="comment-actions">
- <Button
- type="text"
- icon={<LikeOutlined />}
- onClick={() => handleLikeComment(comment.id)}
- >
- {comment.likes || 0}
- </Button>
- </div>
- </div>
- </List.Item>
- )}
- />
-
- <div style={{ textAlign: 'right', marginTop: 16 }}>
- <Pagination
- current={currentPage}
- pageSize={pageSize}
- total={comments.length}
- onChange={(page, size) => {
- setCurrentPage(page);
- setPageSize(size);
- }}
- showSizeChanger
- showQuickJumper
- showTotal={total => `共 ${total} 条`}
- />
- </div>
-
- <Modal
- title={
- <span>
- <MessageOutlined style={{ marginRight: 8 }} />
- 新建讨论
- </span>
- }
- visible={modalVisible}
- onCancel={() => setModalVisible(false)}
- footer={[
- <Button key="cancel" onClick={() => setModalVisible(false)}>
- 取消
- </Button>,
- <Button
- key="submit"
- type="primary"
- onClick={() => {
- form.validateFields()
- .then(values => handleSubmit(values))
- .catch(() => message.error('请填写完整信息'));
- }}
- >
- 创建讨论
- </Button>,
- ]}
- >
- <Form form={form} layout="vertical">
- <Form.Item
- name="title"
- label="讨论主题"
- rules={[{ required: true, message: '请输入讨论主题' }]}
- >
- <Input placeholder="输入讨论主题" />
- </Form.Item>
-
- <Form.Item
- name="content"
- label="讨论内容"
- rules={[{ required: true, message: '请输入讨论内容' }]}
- >
- <TextArea
- rows={6}
- placeholder="详细描述你想讨论的内容..."
- />
- </Form.Item>
- </Form>
- </Modal>
- </div>
- );
-};
-
-
-export default DiscussionSection;
\ No newline at end of file
diff --git a/src/feature/categories/GameCategory.tsx b/src/feature/categories/GameCategory.tsx
deleted file mode 100644
index 3f5c78d..0000000
--- a/src/feature/categories/GameCategory.tsx
+++ /dev/null
@@ -1,173 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Card, List, message, Spin, Input, Tag, Typography} from 'antd';
-import { SearchOutlined, PlayCircleOutlined } from '@ant-design/icons';
-import CategoryAPI from '../../api/categoryApi';
-import type { GameResource } from '../../api/categoryTypes';
-import { useNavigate } from 'react-router';
-
-const { Text } = Typography;
-const { Search } = Input;
-
-const GameCategory: React.FC = () => {
- const [games, setGames] = useState<GameResource[]>([]);
- const [filteredGames, setFilteredGames] = useState<GameResource[]>([]);
- const [isLoading, setIsLoading] = useState(true);
- const [searchQuery, setSearchQuery] = useState('');
-
- // 加载数据
-useEffect(() => {
- const loadGames = async () => {
- try {
- const response = await CategoryAPI.getWorksByCategory(3);
-
- // 类型转换或断言
- const gameResources = response.content.map((work: any) => ({
- ...work,
- developer: (work as any).developer || '未知开发商',
- publisher: (work as any).publisher || '未知发行商',
- platform: (work as any).platform || 'PC',
- releaseDate: (work as any).releaseDate || '未知日期'
- })) as unknown as GameResource[];
-
- setGames(gameResources);
- setFilteredGames(gameResources);
- } catch (error) {
- message.error('加载游戏资源失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- loadGames();
-}, []);
-
- // 筛选逻辑
- useEffect(() => {
- if (!searchQuery) {
- setFilteredGames(games);
- return;
- }
-
- const filtered = games.filter(game =>
- game.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- game.developer.toLowerCase().includes(searchQuery.toLowerCase())
- );
-
- setFilteredGames(filtered);
- }, [searchQuery, games]);
-
-
- // // 在加载数据部分添加虚拟数据
- // useEffect(() => {
- // const loadGames = async () => {
- // try {
- // // 模拟API请求
- // const mockGames: GameResource[] = [
- // {
- // id: 5,
- // title: '赛博朋克2077',
- // developer: 'CD Projekt Red',
- // publisher: 'CD Projekt',
- // categoryId: 3,
- // categoryName: '角色扮演',
- // platform: 'PC',
- // releaseDate: '2020-12-10',
- // seeders: 156,
- // leechers: 34,
- // uploadDate: '2023-06-01',
- // uploader: 'gameLover',
- // downloadCount: 2876,
- // hash: 'm3n4o5p6q7r8',
- // size: '123Mb'
- // },
- // {
- // id: 6,
- // title: '艾尔登法环',
- // developer: 'FromSoftware',
- // publisher: '万代南梦宫',
- // categoryId: 3,
- // categoryName: '动作冒险',
- // releaseDate: '2022-02-25',
- // seeders: 201,
- // leechers: 45,
- // uploadDate: '2023-05-20',
- // uploader: 'hardcoreGamer',
- // downloadCount: 3542,
- // hash: 's9t0u1v2w3x4',
- // platform: 'PC',
- // size: '234Mb'
- // }
- // ];
-
- // setGames(mockGames);
- // setFilteredGames(mockGames);
- // } catch (error) {
- // message.error('加载游戏资源失败');
- // } finally {
- // setIsLoading(false);
- // }
- // };
-
- // loadGames();
- // }, []);
-
- return (
- <div className="game-category-container">
- <div className="category-header">
- <h1><PlayCircleOutlined /> 游戏资源分区</h1>
- <p>PC、主机游戏资源分享</p>
- </div>
-
- <Search
- placeholder="搜索游戏名称或开发商"
- allowClear
- enterButton={<SearchOutlined />}
- value={searchQuery}
- onChange={e => setSearchQuery(e.target.value)}
- className="search-bar"
- />
-
- {isLoading ? (
- <Spin tip="加载游戏资源..." size="large" />
- ) : (
- <List
- grid={{ gutter: 16, column: 1 }}
- dataSource={filteredGames}
- renderItem={game => (
- <List.Item>
- <GameCard game={game} />
- </List.Item>
- )}
- />
- )}
- </div>
- );
-};
-
-const GameCard: React.FC<{ game: GameResource }> = ({ game }) => {
- const navigate = useNavigate();
-
- const handleClick = () => {
- navigate(`/works/${game.id}`);
- };
-
- return (
- <Card className="game-card" onClick={handleClick} hoverable>
- <div className="card-content">
- <div className="card-header">
- <Text strong className="resource-title">{game.title}</Text>
- <Text type="secondary" className="resource-developer">开发商: {game.developer}</Text>
- </div>
-
- <div className="card-meta">
- <Tag color="blue">{game.platform}</Tag>
- <Tag color="geekblue">{game.categoryName}</Tag>
- <Tag color="green">{game.seeders} 做种</Tag>
- <Tag color="orange">{game.leechers} 下载</Tag>
- </div>
- </div>
- </Card>
- );
-};
-
-export default GameCategory;
\ No newline at end of file
diff --git a/src/feature/categories/MovieCategory.tsx b/src/feature/categories/MovieCategory.tsx
deleted file mode 100644
index 262ebd4..0000000
--- a/src/feature/categories/MovieCategory.tsx
+++ /dev/null
@@ -1,172 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Card, List, message, Spin, Input, Tag, Typography} from 'antd';
-import { SearchOutlined, VideoCameraOutlined } from '@ant-design/icons';
-import CategoryAPI from '../../api/categoryApi';
-import type { MovieResource } from '../../api/categoryTypes';
-import { useNavigate } from 'react-router';
-
-const { Text } = Typography;
-const { Search } = Input;
-
-const MovieCategory: React.FC = () => {
- const [movies, setMovies] = useState<MovieResource[]>([]);
- const [filteredMovies, setFilteredMovies] = useState<MovieResource[]>([]);
- const [isLoading, setIsLoading] = useState(true);
- const [searchQuery, setSearchQuery] = useState('');
-
- // 加载数据
-useEffect(() => {
- const loadMovies = async () => {
- try {
- const response = await CategoryAPI.getWorksByCategory(2);
- // 添加类型断言或转换
- const movies = response.content.map((work: any) => ({
- ...work,
- director: (work as any).director || '未知导演',
- actors: (work as any).actors || [],
- resolution: (work as any).resolution || '1080p',
- duration: (work as any).duration || '0分钟'
- })) as unknown as MovieResource[];
-
- setMovies(movies);
- setFilteredMovies(movies);
- } catch (error) {
- message.error('加载影视资源失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- loadMovies();
-}, []);
-
- // 筛选逻辑
- useEffect(() => {
- if (!searchQuery) {
- setFilteredMovies(movies);
- return;
- }
-
- const filtered = movies.filter(movie =>
- movie.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- movie.director.toLowerCase().includes(searchQuery.toLowerCase())
- );
-
- setFilteredMovies(filtered);
- }, [searchQuery, movies]);
-
-
-// // 在加载数据部分添加虚拟数据
-// useEffect(() => {
-// const loadMovies = async () => {
-// try {
-// // 模拟API请求
-// const mockMovies: MovieResource[] = [
-// {
-// id: 3,
-// title: '盗梦空间',
-// director: '克里斯托弗·诺兰',
-// categoryId: 2,
-// categoryName: '科幻电影',
-// resolution: '1080p',
-// duration: '148分钟',
-// actors: ['莱昂纳多·迪卡普里奥', '约瑟夫·高登-莱维特'],
-// seeders: 215,
-// leechers: 42,
-// uploadDate: '2023-06-10',
-// uploader: 'movieFan',
-// downloadCount: 3256,
-// hash: 'a1b2c3d4e5f6',
-// size: '234Mb'
-// },
-// {
-// id: 4,
-// title: '肖申克的救赎',
-// director: '弗兰克·德拉邦特',
-// categoryId: 2,
-// categoryName: '剧情片',
-// resolution: '4K',
-// duration: '142分钟',
-// actors: ['蒂姆·罗宾斯', '摩根·弗里曼'],
-// seeders: 189,
-// leechers: 28,
-// uploadDate: '2023-05-15',
-// uploader: 'classicMovie',
-// downloadCount: 4123,
-// hash: 'g7h8i9j0k1l2',
-// size: '123Mb'
-// }
-// ];
-
-// setMovies(mockMovies);
-// setFilteredMovies(mockMovies);
-// } catch (error) {
-// message.error('加载影视资源失败');
-// } finally {
-// setIsLoading(false);
-// }
-// };
-
-// loadMovies();
-// }, []);
-
- return (
- <div className="movie-category-container">
- <div className="category-header">
- <h1><VideoCameraOutlined /> 影视资源分区</h1>
- <p>高清电影、电视剧、纪录片资源分享</p>
- </div>
-
- <Search
- placeholder="搜索电影或导演"
- allowClear
- enterButton={<SearchOutlined />}
- value={searchQuery}
- onChange={e => setSearchQuery(e.target.value)}
- className="search-bar"
- />
-
- {isLoading ? (
- <Spin tip="加载影视资源..." size="large" />
- ) : (
- <List
- grid={{ gutter: 16, column: 1 }}
- dataSource={filteredMovies}
- renderItem={movie => (
- <List.Item>
- <MovieCard movie={movie} />
- </List.Item>
- )}
- />
- )}
- </div>
- );
-};
-
-const MovieCard: React.FC<{ movie: MovieResource }> = ({ movie }) => {
- const navigate = useNavigate();
-
- const handleClick = () => {
- navigate(`/works/${movie.id}`);
- };
-
- return (
- <Card className="movie-card" onClick={handleClick} hoverable>
- <div className="card-content">
- <div className="card-header">
- <Text strong className="resource-title">{movie.title}</Text>
- <Text type="secondary" className="resource-director">导演: {movie.director}</Text>
- </div>
-
- <div className="card-meta">
- <Tag color="blue">{movie.resolution}</Tag>
- <Tag color="geekblue">{movie.categoryName}</Tag>
- <Tag color="green">{movie.seeders} 做种</Tag>
- <Tag color="orange">{movie.leechers} 下载</Tag>
- </div>
- </div>
- </Card>
- );
-};
-
-export default MovieCategory;
\ No newline at end of file
diff --git a/src/feature/categories/MusicCategory.tsx b/src/feature/categories/MusicCategory.tsx
deleted file mode 100644
index f9057ee..0000000
--- a/src/feature/categories/MusicCategory.tsx
+++ /dev/null
@@ -1,191 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import {
- Card, Button, List, Tag, Typography,
- message, Spin,
- Input} from 'antd';
-import {
- SearchOutlined
-} from '@ant-design/icons';
-import CategoryAPI from '../../api/categoryApi';
-import type { CategoryDTO, MusicResource } from '../../api/categoryTypes';
-import { useNavigate } from 'react-router';
-
-const { Search } = Input;
-const { Text } = Typography;
-
-const MusicCategory: React.FC = () => {
- const [categories, setCategories] = useState<CategoryDTO[]>([]);
- const [resources, setResources] = useState<MusicResource[]>([]);
- const [filteredResources, setFilteredResources] = useState<MusicResource[]>([]);
- const [isLoading, setIsLoading] = useState(true);
- const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
-
- // 筛选状态
- const [searchQuery, setSearchQuery] = useState('');
- const [qualityFilter, setQualityFilter] = useState<string>('all');
-
- // 加载分类数据
- useEffect(() => {
- const loadData = async () => {
- try {
- const [categoryData, resourceData] = await Promise.all([
- CategoryAPI.getCategoryTree(),
- CategoryAPI.getWorksByCategory(1)
- ]);
-
- // 确保 parentId 不包含 null
- const normalizedCategories = categoryData.map(cat => ({
- ...cat,
- parentId: cat.parentId === null ? undefined : cat.parentId
- })) as CategoryDTO[];
-
- // 音乐资源类型转换
- const musicResources = resourceData.content.map(work => ({
- ...work,
- artist: work.artist || '未知艺术家',
- quality: work.quality || 'MP3 320k',
- genre: work.genre || ['未知流派'],
- categoryName: work.categoryName || '未知分类',
- size: work.size || '0MB'
- })) as unknown as MusicResource[];
-
- setCategories(normalizedCategories);
- setResources(musicResources);
- setFilteredResources(musicResources);
- } catch (error) {
- message.error('加载数据失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- loadData();
- }, []);
-
- // 筛选逻辑
- useEffect(() => {
- let filtered = resources;
-
- // 搜索筛选
- if (searchQuery) {
- filtered = filtered.filter(resource =>
- resource.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- resource.artist.toLowerCase().includes(searchQuery.toLowerCase())
- );
- }
-
- // 音质筛选
- if (qualityFilter !== 'all') {
- filtered = filtered.filter(resource => resource.quality === qualityFilter);
- }
-
- // 分类筛选
- if (selectedCategory) {
- filtered = filtered.filter(resource => resource.categoryId === selectedCategory);
- }
-
- setFilteredResources(filtered);
- }, [searchQuery, qualityFilter, selectedCategory, resources]);
-
- // 分类选择处理
- const handleCategorySelect = (categoryId: number) => {
- setSelectedCategory(prev => prev === categoryId ? null : categoryId);
- };
-
- return (
- <div className="music-category-container">
- {/* 头部区域 */}
- <div className="category-header">
- <h1>音乐资源分区</h1>
- <p>高质量音乐资源共享,保持分享率,共建良好PT环境</p>
- </div>
-
- {/* 搜索和筛选 */}
- <div className="filter-section">
- <Search
- placeholder="搜索音乐名称或艺术家"
- allowClear
- enterButton={<SearchOutlined />}
- value={searchQuery}
- onChange={e => setSearchQuery(e.target.value)}
- className="search-bar"
- />
-
- <div className="quality-filter">
- <span>音质筛选:</span>
- <Button.Group>
- {['all', 'FLAC', 'Hi-Res', 'MP3 320k'].map(quality => (
- <Button
- key={quality}
- type={qualityFilter === quality ? 'primary' : 'default'}
- onClick={() => setQualityFilter(quality)}
- >
- {quality === 'all' ? '全部' : quality}
- </Button>
- ))}
- </Button.Group>
- </div>
- </div>
-
- {/* 分类导航 */}
- <div className="category-navigation">
- {categories.map(category => (
- <Tag
- key={category.id}
- color={selectedCategory === category.id ? 'blue' : undefined}
- onClick={() => handleCategorySelect(category.id)}
- className="category-tag"
- >
- {category.name} ({category.workCount || 0})
- </Tag>
- ))}
- </div>
-
- {/* 内容展示 */}
- {isLoading ? (
- <Spin tip="加载中..." size="large" />
- ) : (
- <div className="resource-list">
- <List
- grid={{ gutter: 16, column: 1 }}
- dataSource={filteredResources}
- renderItem={resource => (
- <List.Item>
- <ResourceCard resource={resource} />
- </List.Item>
- )}
- />
- </div>
- )}
- </div>
- );
-};
-
-// 资源卡片组件
-const ResourceCard: React.FC<{ resource: MusicResource }> = ({ resource }) => {
- const navigate = useNavigate();
-
- const handleClick = () => {
- navigate(`/works/${resource.id}`);
- };
-
- return (
- <Card className="resource-card" onClick={handleClick} hoverable>
- <div className="card-content">
- <div className="card-header">
- <Text strong className="resource-title">{resource.title}</Text>
- <Text type="secondary" className="resource-artist">{resource.artist}</Text>
- </div>
-
- <div className="card-meta">
- <Tag color="blue">{resource.quality}</Tag>
- <Tag color="geekblue">{resource.categoryName}</Tag>
- <Tag color="green">{resource.seeders} 做种</Tag>
- <Tag color="orange">{resource.leechers} 下载</Tag>
- </div>
- </div>
- </Card>
- );
-};
-
-export default MusicCategory;
\ No newline at end of file
diff --git a/src/feature/categories/OtherCategory.tsx b/src/feature/categories/OtherCategory.tsx
deleted file mode 100644
index 6c3318a..0000000
--- a/src/feature/categories/OtherCategory.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { Card, List, message, Spin, Input, Tag, Typography} from 'antd';
-import { SearchOutlined, AppstoreOutlined } from '@ant-design/icons';
-import CategoryAPI from '../../api/categoryApi';
-import type { MovieResource, OtherResource } from '../../api/categoryTypes';
-import { useNavigate } from 'react-router';
-
-const { Text } = Typography;
-const { Search } = Input;
-
-const OtherCategory: React.FC = () => {
- const [resources, setResources] = useState<OtherResource[]>([]);
- const [filteredResources, setFilteredResources] = useState<OtherResource[]>([]);
- const [isLoading, setIsLoading] = useState(true);
- const [searchQuery, setSearchQuery] = useState('');
-
- // 加载数据
-useEffect(() => {
- const loadResources = async () => {
- try {
- const response = await CategoryAPI.getWorksByCategory(4);
-
- // 其他资源类型转换
- const otherResources = response.content.map(work => ({
- ...work,
- description: (work as any).description || '暂无描述',
- fileType: (work as any).fileType || '未知类型'
- })) as unknown as OtherResource[];
-
- setResources(otherResources);
- setFilteredResources(otherResources);
- } catch (error) {
- message.error('加载资源失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- loadResources();
-}, []);
-
- // 筛选逻辑
- useEffect(() => {
- if (!searchQuery) {
- setFilteredResources(resources);
- return;
- }
-
- const filtered = resources.filter(resource =>
- resource.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
- resource.description.toLowerCase().includes(searchQuery.toLowerCase())
- );
-
- setFilteredResources(filtered);
- }, [searchQuery, resources]);
-
- // 在加载数据部分添加虚拟数据
- useEffect(() => {
- const loadMovies = async () => {
- try {
- // 模拟API请求
- const mockMovies: MovieResource[] = [
- {
- id: 3,
- title: '盗梦空间',
- director: '克里斯托弗·诺兰',
- categoryId: 2,
- categoryName: '科幻电影',
- resolution: '1080p',
- duration: '148分钟',
- actors: ['莱昂纳多·迪卡普里奥', '约瑟夫·高登-莱维特'],
- seeders: 215,
- leechers: 42,
- uploadDate: '2023-06-10',
- uploader: 'movieFan',
- downloadCount: 3256,
- hash: 'a1b2c3d4e5f6',
- size: '123Mb'
- },
- {
- id: 4,
- title: '肖申克的救赎',
- director: '弗兰克·德拉邦特',
- categoryId: 2,
- categoryName: '剧情片',
- resolution: '4K',
- duration: '142分钟',
- actors: ['蒂姆·罗宾斯', '摩根·弗里曼'],
- seeders: 189,
- leechers: 28,
- uploadDate: '2023-05-15',
- uploader: 'classicMovie',
- downloadCount: 4123,
- hash: 'g7h8i9j0k1l2',
- size: '123Mb'
- }
- ];
-
- setMovies(mockMovies);
- setFilteredMovies(mockMovies);
- } catch (error) {
- message.error('加载影视资源失败');
- } finally {
- setIsLoading(false);
- }
- };
-
- loadMovies();
- }, []);
-
- return (
- <div className="other-category-container">
- <div className="category-header">
- <h1><AppstoreOutlined /> 其他资源分区</h1>
- <p>软件、电子书等其他类型资源</p>
- </div>
-
- <Search
- placeholder="搜索资源名称或描述"
- allowClear
- enterButton={<SearchOutlined />}
- value={searchQuery}
- onChange={e => setSearchQuery(e.target.value)}
- className="search-bar"
- />
-
- {isLoading ? (
- <Spin tip="加载资源..." size="large" />
- ) : (
- <List
- grid={{ gutter: 16, column: 1 }}
- dataSource={filteredResources}
- renderItem={resource => (
- <List.Item>
- <OtherResourceCard resource={resource} />
- </List.Item>
- )}
- />
- )}
- </div>
- );
-};
-
-const OtherResourceCard: React.FC<{ resource: OtherResource }> = ({ resource }) => {
- const navigate = useNavigate();
-
- const handleClick = () => {
- navigate(`/works/${resource.id}`);
- };
-
- return (
- <Card className="other-resource-card" onClick={handleClick} hoverable>
- <div className="card-content">
- <div className="card-header">
- <Text strong className="resource-title">{resource.title}</Text>
- <Text type="secondary" className="resource-type">类型: {resource.fileType}</Text>
- </div>
-
- <div className="card-meta">
- <Tag color="geekblue">{resource.categoryName}</Tag>
- <Tag color="green">{resource.seeders} 做种</Tag>
- <Tag color="orange">{resource.leechers} 下载</Tag>
- </div>
- </div>
- </Card>
- );
-};
-
-export default OtherCategory;
-
-function setMovies(_mockMovies: MovieResource[]) {
- throw new Error('Function not implemented.');
-}
-
-
-function setFilteredMovies(_mockMovies: MovieResource[]) {
- throw new Error('Function not implemented.');
-}
diff --git a/src/feature/home/Category.tsx b/src/feature/home/Category.tsx
new file mode 100644
index 0000000..2e0ab6f
--- /dev/null
+++ b/src/feature/home/Category.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { Row, Col, Typography, Divider } from 'antd';
+import type { CategoryProps } from './types';
+import WorkCard from './WorkCard';
+
+const { Title } = Typography;
+
+const Category: React.FC<CategoryProps> = ({ section, artworks }) => {
+ // 如果没有作品,不渲染该分区
+ if (artworks.length === 0) {
+ return null;
+ }
+
+ return (
+ <div style={{ marginBottom: '48px' }}>
+ <Title level={2} style={{ marginBottom: '24px', color: '#1890ff' }}>
+ {section.name}
+ </Title>
+ <Divider style={{ margin: '16px 0 24px 0' }} />
+
+ <Row gutter={[16, 16]}>
+ {artworks.map((artwork) => (
+ <Col
+ key={artwork.id}
+ xs={24}
+ sm={12}
+ md={8}
+ lg={6}
+ xl={6}
+ >
+ <WorkCard artwork={artwork} />
+ </Col>
+ ))}
+ </Row>
+ </div>
+ );
+};
+
+export default Category;
\ No newline at end of file
diff --git a/src/feature/home/Home.tsx b/src/feature/home/Home.tsx
index 80e44eb..953af8a 100644
--- a/src/feature/home/Home.tsx
+++ b/src/feature/home/Home.tsx
@@ -1,116 +1,223 @@
-import { NavLink } from 'react-router';
-import filmImg from '../../assets/categories/film.jpg';
-import musicImg from '../../assets/categories/music.jpg';
-import gameImg from '../../assets/categories/game.jpg';
-import otherImg from '../../assets/categories/other.jpg';
-// 假设你有排行榜数据,这里模拟一下,实际根据接口等替换
-const filmRankList = [
- { id: 1, name: '影视作品1', view: '1.2万' },
- { id: 2, name: '影视作品2', view: '1.1万' },
- { id: 3, name: '影视作品3', view: '1.0万' },
-];
-const musicRankList = [
- { id: 1, name: '音乐作品1', view: '1.5万' },
- { id: 2, name: '音乐作品2', view: '1.3万' },
- { id: 3, name: '音乐作品3', view: '1.2万' },
-];
-const gameRankList = [
- { id: 1, name: '游戏作品1', view: '2.0万' },
- { id: 2, name: '游戏作品2', view: '1.8万' },
- { id: 3, name: '游戏作品3', view: '1.6万' },
-];
-const otherRankList = [
- { id: 1, name: '其他作品1', view: '0.8万' },
- { id: 2, name: '其他作品2', view: '0.7万' },
- { id: 3, name: '其他作品3', view: '0.6万' },
-];
+import React, { useEffect, useMemo } from 'react';
+import { useDispatch } from 'react-redux';
+import { Input, Spin, Alert } from 'antd';
+import { SearchOutlined } from '@ant-design/icons';
+import type { Section, Artwork } from './types';
+import { initializeArtworks, selectFilteredArtworks, selectSearchTerm, selectWorkListError, selectWorkListLoading, setSearchTerm } from './workListSlice';
+import { useAppSelector } from '../../store/hooks';
+import { selectSections, selectCategoryLoading, selectCategoryError, initializeSections } from './categorySlice';
+import Category from './Category';
-function Home() {
- const categories = [
- { id: 'film', name: '影视', image: filmImg, rankList: filmRankList },
- { id: 'music', name: '音乐', image: musicImg, rankList: musicRankList },
- { id: 'game', name: '游戏', image: gameImg, rankList: gameRankList },
- { id: 'other', name: '其他', image: otherImg, rankList: otherRankList },
- ];
+const Home: React.FC = () => {
+ const dispatch = useDispatch();
- return (
- <div className="max-w-7xl mx-auto px-4 py-16 flex flex-col items-center">
- {/* 页面标题 */}
- <h1 className="text-[clamp(2rem,5vw,3.5rem)] font-bold text-center mb-12 text-gray-800 tracking-tight">
- 欢迎访问创意协作平台
- </h1>
+ // 从Redux store获取状态
+ const sections = useAppSelector(selectSections);
+ const filteredArtworks = useAppSelector(selectFilteredArtworks);
+ const searchTerm = useAppSelector(selectSearchTerm);
+ const workListLoading = useAppSelector(selectWorkListLoading);
+ const workListError = useAppSelector(selectWorkListError);
+ const categoryLoading = useAppSelector(selectCategoryLoading);
+ const categoryError = useAppSelector(selectCategoryError);
- {/* 分区 + 排行榜 容器,改为 flex 布局 */}
- <div className="w-full flex max-w-6xl mx-auto">
- {/* 分区卡片容器 - 响应式网格布局 */}
- <div className="grid grid-cols-4 gap-6 w-3/4">
- {categories.map((category) => (
- <div
- key={category.id}
- className="group relative overflow-hidden rounded-xl shadow-lg transition-all duration-300 hover:-translate-y-2 hover:shadow-xl"
- >
- <NavLink to={`/categories/${category.id}`} className="block">
- {/* 图片容器 */}
- <div className="aspect-[4/3] relative overflow-hidden">
- {/* 背景图片 - 带模糊和亮度调整 */}
- <img
- src={category.image}
- alt={category.name}
- className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
- style={{ filter: 'blur(1px) brightness(0.85)' }}
- />
+ // 综合加载状态和错误状态
+ const loading = workListLoading || categoryLoading;
+ const error = workListError || categoryError;
- {/* 渐变遮罩层 - 增强文字可读性 */}
- <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent z-10"></div>
+ // 示例数据
+ const sampleSections: Section[] = [
+ {
+ id: 1,
+ childrenid: [1, 2, 3],
+ name: "数字艺术"
+ },
+ {
+ id: 2,
+ childrenid: [4, 5],
+ name: "传统绘画"
+ }
+ ];
- {/* 文字内容 */}
- <div className="absolute inset-0 flex flex-col items-center justify-center p-4 z-20">
- {/* 分类标题 - 艺术字体 */}
- <h2 className="text-[clamp(1.8rem,4vw,2.5rem)] font-bold text-white text-center mb-2" style={{
- fontFamily: '"Playfair Display", serif',
- textShadow: '0 4px 12px rgba(0,0,0,0.8)',
- transform: 'translateY(10px)',
- transition: 'transform 0.3s ease-out',
- letterSpacing: '2px',
- borderBottom: '2px solid rgba(255,255,255,0.6)',
- paddingBottom: '4px'
- }}>
- {category.name}
- </h2>
+ const sampleArtworks: Artwork[] = [
+ {
+ id: 1,
+ title: "未来城市概念设计",
+ author: "视觉设计师张三",
+ views: 1247,
+ category: {
+ id: 1,
+ name: "数字艺术",
+ children: ["概念设计", "建筑设计"]
+ },
+ description: "一个关于2050年智慧城市的概念设计作品,融合了可持续发展、人工智能和绿色科技的理念",
+ createTime: "2024-01-15T10:30:00Z",
+ cover: "https://picsum.photos/300/200?random=1"
+ },
+ {
+ id: 2,
+ title: "移动应用界面设计套件",
+ author: "UI设计师李四",
+ views: 856,
+ category: {
+ id: 2,
+ name: "界面设计",
+ children: ["UI/UX设计", "移动端设计"]
+ },
+ description: "一套完整的移动端UI设计规范和组件库,包含100+个精美界面和500+个设计组件",
+ createTime: "2024-02-20T14:15:00Z",
+ cover: "https://picsum.photos/300/200?random=2"
+ },
+ {
+ id: 3,
+ title: "React组件库开发指南",
+ author: "刘松林",
+ views: 432,
+ category: {
+ id: 3,
+ name: "程序开发",
+ children: ["前端开发", "React技术"]
+ },
+ description: "一套完整的企业级React组件库开发教程和源码,包含从设计到发布的完整流程",
+ createTime: "2024-03-10T09:45:00Z",
+ cover: "https://picsum.photos/300/200?random=3"
+ },
+ {
+ id: 4,
+ title: "机械战士3D模型",
+ author: "3D艺术家王五",
+ views: 789,
+ category: {
+ id: 1,
+ name: "数字艺术",
+ children: ["3D建模", "科幻设计"]
+ },
+ description: "一个高精度的科幻机械战士3D模型,包含完整的材质贴图和动画骨骼系统",
+ createTime: "2024-01-25T16:20:00Z",
+ cover: "https://picsum.photos/300/200?random=4"
+ },
+ {
+ id: 5,
+ title: "城市夜景摄影集",
+ author: "摄影师赵六",
+ views: 1123,
+ category: {
+ id: 4,
+ name: "摄影艺术",
+ children: ["摄影作品", "城市风光"]
+ },
+ description: "一组精美的城市夜景摄影作品,捕捉了都市夜晚的璀璨光影",
+ createTime: "2024-02-14T11:30:00Z",
+ cover: "https://picsum.photos/300/200?random=5"
+ },
+ {
+ id: 6,
+ title: "奇幻世界插画系列",
+ author: "插画师孙七",
+ views: 945,
+ category: {
+ id: 5,
+ name: "插画艺术",
+ children: ["插画艺术", "奇幻风格"]
+ },
+ description: "一套充满想象力的奇幻题材插画作品,包含角色设计、场景概念图和完整插图",
+ createTime: "2024-03-05T13:20:00Z",
+ cover: "https://picsum.photos/300/200?random=6"
+ }
+ ];
- {/* 描述文本 - 悬停显示 */}
- <p className="text-indigo-100 text-center opacity-0 transform translate-y-4 transition-all duration-300 group-hover:opacity-100 group-hover:translate-y-0 mt-2" style={{
- fontFamily: '"Merriweather", serif',
- textShadow: '0 2px 6px rgba(0,0,0,0.6)'
- }}>
- 探索{category.name}创意项目
- </p>
- </div>
+ // 初始化数据
+ useEffect(() => {
+ dispatch(initializeArtworks(sampleArtworks));
+ dispatch(initializeSections(sampleSections));
+ }, [dispatch]);
+
+ // 处理搜索输入
+ const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+ dispatch(setSearchTerm(e.target.value));
+ };
+
+ // 根据分区ID获取对应的作品
+ const getArtworksBySection = useMemo(() => {
+ return (childrenIds: number[]): Artwork[] => {
+ return filteredArtworks.filter(artwork => childrenIds.includes(artwork.id));
+ };
+ }, [filteredArtworks]);
+
+ // 渲染加载状态
+ if (loading) {
+ return (
+ <div style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ height: '100vh'
+ }}>
+ <Spin size="large" tip="加载中..." />
+ </div>
+ );
+ }
+
+ // 渲染错误状态
+ if (error) {
+ return (
+ <div style={{ padding: '24px' }}>
+ <Alert
+ message="加载失败"
+ description={error}
+ type="error"
+ showIcon
+ />
+ </div>
+ );
+ }
+
+ return (
+ <div style={{
+ backgroundColor: '#f5f5f5',
+ minHeight: '100vh',
+ padding: '24px'
+ }}>
+ <div style={{ maxWidth: 1200, margin: '0 auto' }}>
+ {/* 搜索栏 */}
+ <div style={{ marginBottom: '32px' }}>
+ <Input
+ size="large"
+ placeholder="搜索作品标题、作者、分类..."
+ prefix={<SearchOutlined />}
+ value={searchTerm}
+ onChange={handleSearchChange}
+ style={{ maxWidth: 600, width: '100%' }}
+ allowClear
+ />
</div>
- </NavLink>
- </div>
- ))}
- </div>
- {/* 排行榜区域 - 右侧 */}
- <div className="w-1/4 pl-6">
- {categories.map((category) => (
- <div key={category.id} className="mb-8">
- <h3 className="text-xl font-bold text-gray-800 mb-4">{category.name}排行榜</h3>
- <ul>
- {category.rankList.map((item) => (
- <li key={item.id} className="flex justify-between items-center mb-2">
- <span>{item.name}</span>
- <span className="text-gray-600">{item.view}</span>
- </li>
- ))}
- </ul>
- </div>
- ))}
- </div>
- </div>
- </div>
- );
-}
+ {/* 分区展示 */}
+ {sections.map((section) => {
+ const sectionArtworks = getArtworksBySection(section.childrenid);
-export default Home;
\ No newline at end of file
+ return (
+ <Category
+ key={section.id}
+ section={section}
+ artworks={sectionArtworks}
+ />
+ );
+ })}
+
+ {/* 无搜索结果提示 */}
+ {searchTerm && filteredArtworks.length === 0 && (
+ <div style={{
+ textAlign: 'center',
+ padding: '48px 0',
+ color: '#999'
+ }}>
+ <p style={{ fontSize: 16 }}>未找到相关作品</p>
+ <p>尝试使用其他关键词搜索</p>
+ </div>
+ )}
+ </div>
+ </div>
+ );
+};
+
+export default Home;
diff --git a/src/feature/home/Tmp.tsx b/src/feature/home/Tmp.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/feature/home/Tmp.tsx
diff --git a/src/feature/home/WorkCard.tsx b/src/feature/home/WorkCard.tsx
new file mode 100644
index 0000000..1d3ac22
--- /dev/null
+++ b/src/feature/home/WorkCard.tsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { Card, Typography, Space } from 'antd';
+import { EyeOutlined, UserOutlined } from '@ant-design/icons';
+import type { WorkCardProps } from './types';
+import { useNavigate } from 'react-router';
+
+const { Text, Title } = Typography;
+const { Meta } = Card;
+
+const WorkCard: React.FC<WorkCardProps> = ({ artwork }) => {
+ const handleImageError = (e: React.SyntheticEvent<HTMLImageElement, Event>) => {
+ const target = e.target as HTMLImageElement;
+ target.src = 'https://via.placeholder.com/300x200?text=No+Image';
+ };
+ const nav = useNavigate()
+ const handleClick = () => {
+ nav(`/work/${artwork.id}`)
+ }
+
+ const formatViews = (views: number): string => {
+ return views.toLocaleString();
+ };
+
+ return (
+ <Card
+ hoverable
+ style={{ marginBottom: 16 }}
+ cover={
+ <img
+ alt={artwork.title}
+ src={artwork.cover}
+ style={{
+ height: 200,
+ objectFit: 'cover',
+ borderRadius: '8px 8px 0 0'
+ }}
+ onError={handleImageError}
+ />
+ }
+ actions={[
+ <Space key="views">
+ <EyeOutlined />
+ <Text type="secondary">{formatViews(artwork.views)}</Text>
+ </Space>,
+ <Space key="author">
+ <UserOutlined />
+ <Text type="secondary">{artwork.author}</Text>
+ </Space>,
+ ]}
+ onClick={handleClick}
+ >
+ <Meta
+ title={
+ <div style={{ marginBottom: 8 }}>
+ <Title level={4} style={{ margin: 0, fontSize: 16 }}>
+ {artwork.title}
+ </Title>
+ </div>
+ }
+ description={
+ <Space direction="vertical" size="small" style={{ width: '100%' }}>
+ <Text type="secondary" style={{ fontSize: 12 }}>
+ 分类: {artwork.category.name}
+ </Text>
+ {artwork.category.children.length > 0 && (
+ <Space wrap size={[4, 4]}>
+ {artwork.category.children.map((child, index) => (
+ <Text
+ key={index}
+ code
+ style={{
+ fontSize: 11,
+ padding: '2px 6px',
+ backgroundColor: '#f5f5f5',
+ borderRadius: 4
+ }}
+ >
+ {child}
+ </Text>
+ ))}
+ </Space>
+ )}
+ </Space>
+ }
+ />
+ </Card>
+ );
+};
+
+export default WorkCard;
\ No newline at end of file
diff --git a/src/feature/home/categorySlice.ts b/src/feature/home/categorySlice.ts
new file mode 100644
index 0000000..31482cc
--- /dev/null
+++ b/src/feature/home/categorySlice.ts
@@ -0,0 +1,167 @@
+import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
+import type { Section } from './types';
+
+// 分区状态类型
+export interface CategoryState {
+ sections: Section[];
+ loading: boolean;
+ error: string | null;
+ selectedCategoryId: number | null;
+}
+
+// 初始状态
+const initialState: CategoryState = {
+ sections: [],
+ loading: false,
+ error: null,
+ selectedCategoryId: null,
+};
+
+// 创建categorySlice
+const categorySlice = createSlice({
+ name: 'category',
+ initialState,
+ reducers: {
+ // 设置加载状态
+ setLoading: (state, action: PayloadAction<boolean>) => {
+ state.loading = action.payload;
+ if (action.payload) {
+ state.error = null;
+ }
+ },
+
+ // 设置错误信息
+ setError: (state, action: PayloadAction<string>) => {
+ state.error = action.payload;
+ state.loading = false;
+ },
+
+ // 设置分区列表
+ setSections: (state, action: PayloadAction<Section[]>) => {
+ state.sections = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+
+ // 添加单个分区
+ addSection: (state, action: PayloadAction<Section>) => {
+ state.sections.push(action.payload);
+ },
+
+ // 更新分区
+ updateSection: (state, action: PayloadAction<Section>) => {
+ const index = state.sections.findIndex(section => section.id === action.payload.id);
+ if (index !== -1) {
+ state.sections[index] = action.payload;
+ }
+ },
+
+ // 删除分区
+ removeSection: (state, action: PayloadAction<number>) => {
+ state.sections = state.sections.filter(section => section.id !== action.payload);
+ // 如果删除的是当前选中的分区,清空选中状态
+ if (state.selectedCategoryId === action.payload) {
+ state.selectedCategoryId = null;
+ }
+ },
+
+ // 设置选中的分区ID
+ setSelectedCategoryId: (state, action: PayloadAction<number | null>) => {
+ state.selectedCategoryId = action.payload;
+ },
+
+ // 向分区添加作品ID
+ addArtworkToSection: (state, action: PayloadAction<{ sectionId: number; artworkId: number }>) => {
+ const { sectionId, artworkId } = action.payload;
+ const section = state.sections.find(s => s.id === sectionId);
+ if (section && !section.childrenid.includes(artworkId)) {
+ section.childrenid.push(artworkId);
+ }
+ },
+
+ // 从分区移除作品ID
+ removeArtworkFromSection: (state, action: PayloadAction<{ sectionId: number; artworkId: number }>) => {
+ const { sectionId, artworkId } = action.payload;
+ const section = state.sections.find(s => s.id === sectionId);
+ if (section) {
+ section.childrenid = section.childrenid.filter(id => id !== artworkId);
+ }
+ },
+
+ // 重新排序分区
+ reorderSections: (state, action: PayloadAction<Section[]>) => {
+ state.sections = action.payload;
+ },
+
+ // 重置分区状态
+ resetCategoryState: () => {
+ return initialState;
+ },
+
+ // 初始化分区数据
+ initializeSections: (state, action: PayloadAction<Section[]>) => {
+ state.sections = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+ },
+});
+
+// 导出actions
+export const {
+ setLoading,
+ setError,
+ setSections,
+ addSection,
+ updateSection,
+ removeSection,
+ setSelectedCategoryId,
+ addArtworkToSection,
+ removeArtworkFromSection,
+ reorderSections,
+ resetCategoryState,
+ initializeSections,
+} = categorySlice.actions;
+
+// 选择器函数
+export const selectSections = (state: { category: CategoryState }): Section[] =>
+ state.category.sections;
+
+export const selectCategoryLoading = (state: { category: CategoryState }): boolean =>
+ state.category.loading;
+
+export const selectCategoryError = (state: { category: CategoryState }): string | null =>
+ state.category.error;
+
+export const selectSelectedCategoryId = (state: { category: CategoryState }): number | null =>
+ state.category.selectedCategoryId;
+
+// 根据ID获取分区
+export const selectSectionById = (sectionId: number) => (state: { category: CategoryState }) => {
+ return state.category.sections.find(section => section.id === sectionId);
+};
+
+// 获取分区数量
+export const selectSectionCount = (state: { category: CategoryState }) => state.category.sections.length;
+
+// 获取包含特定作品的分区
+export const selectSectionsByArtworkId = (artworkId: number) => (state: { category: CategoryState }) => {
+ return state.category.sections.filter(section => section.childrenid.includes(artworkId));
+};
+
+// 获取选中的分区
+export const selectSelectedSection = (state: { category: CategoryState }) => {
+ const { sections, selectedCategoryId } = state.category;
+ if (selectedCategoryId === null) return null;
+ return sections.find(section => section.id === selectedCategoryId) || null;
+};
+
+// 获取分区名称映射
+export const selectSectionNameMap = (state: { category: CategoryState }) => {
+ return state.category.sections.reduce((acc, section) => {
+ acc[section.id] = section.name;
+ return acc;
+ }, {} as Record<number, string>);
+};
+
+export default categorySlice.reducer;
\ No newline at end of file
diff --git a/src/feature/home/types.ts b/src/feature/home/types.ts
new file mode 100644
index 0000000..90db0de
--- /dev/null
+++ b/src/feature/home/types.ts
@@ -0,0 +1,52 @@
+// 类型定义文件
+export interface Category {
+ id: number;
+ name: string;
+ children: string[];
+}
+
+export interface Artwork {
+ id: number;
+ title: string;
+ author: string;
+ views: number;
+ category: Category;
+ description: string;
+ createTime: string;
+ cover: string;
+}
+
+export interface Section {
+ id: number;
+ childrenid: number[];
+ name: string;
+}
+
+export interface ArtworkDisplayProps {
+ sections: Section[];
+ artworks: Artwork[];
+}
+
+export interface WorkCardProps {
+ artwork: Artwork;
+}
+
+export interface CategoryProps {
+ section: Section;
+ artworks: Artwork[];
+}
+
+// Redux state 类型
+export interface WorkListState {
+ artworks: Artwork[];
+ searchTerm: string;
+ loading: boolean;
+ error: string | null;
+}
+
+export interface CategoryState {
+ sections: Section[];
+ loading: boolean;
+ error: string | null;
+ selectedCategoryId: number | null;
+}
\ No newline at end of file
diff --git a/src/feature/home/workListSlice.ts b/src/feature/home/workListSlice.ts
new file mode 100644
index 0000000..ee0d52e
--- /dev/null
+++ b/src/feature/home/workListSlice.ts
@@ -0,0 +1,131 @@
+import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
+import type{ WorkListState, Artwork, Section } from './types';
+
+// 初始状态
+const initialState: WorkListState = {
+ artworks: [],
+ searchTerm: '',
+ loading: false,
+ error: null,
+};
+
+// 创建slice
+const workListSlice = createSlice({
+ name: 'workList',
+ initialState,
+ reducers: {
+ // 设置加载状态
+ setLoading: (state, action: PayloadAction<boolean>) => {
+ state.loading = action.payload;
+ if (action.payload) {
+ state.error = null;
+ }
+ },
+
+ // 设置错误信息
+ setError: (state, action: PayloadAction<string>) => {
+ state.error = action.payload;
+ state.loading = false;
+ },
+
+ // 设置作品列表
+ setArtworks: (state, action: PayloadAction<Artwork[]>) => {
+ state.artworks = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+
+ // 添加单个作品
+ addArtwork: (state, action: PayloadAction<Artwork>) => {
+ state.artworks.push(action.payload);
+ },
+
+ // 更新作品
+ updateArtwork: (state, action: PayloadAction<Artwork>) => {
+ const index = state.artworks.findIndex(artwork => artwork.id === action.payload.id);
+ if (index !== -1) {
+ state.artworks[index] = action.payload;
+ }
+ },
+
+ // 删除作品
+ removeArtwork: (state, action: PayloadAction<number>) => {
+ state.artworks = state.artworks.filter(artwork => artwork.id !== action.payload);
+ },
+
+ // 设置搜索词
+ setSearchTerm: (state, action: PayloadAction<string>) => {
+ state.searchTerm = action.payload;
+ },
+
+ // 清空搜索
+ clearSearch: (state) => {
+ state.searchTerm = '';
+ },
+
+ // 重置状态
+ resetState: () => {
+ return initialState;
+ },
+
+ // 初始化数据(用于设置示例数据)
+ initializeArtworks: (state, action: PayloadAction<Artwork[]>) => {
+ state.artworks = action.payload;
+ state.loading = false;
+ state.error = null;
+ },
+ },
+});
+
+// 导出actions
+export const {
+ setLoading,
+ setError,
+ setArtworks,
+ addArtwork,
+ updateArtwork,
+ removeArtwork,
+ setSearchTerm,
+ clearSearch,
+ resetState,
+ initializeArtworks,
+} = workListSlice.actions;
+
+// 选择器函数
+export const selectArtworks = (state: { workList: WorkListState }) => state.workList.artworks;
+export const selectSearchTerm = (state: { workList: WorkListState }) => state.workList.searchTerm;
+export const selectWorkListLoading = (state: { workList: WorkListState }) => state.workList.loading;
+export const selectWorkListError = (state: { workList: WorkListState }) => state.workList.error;
+
+// 过滤后的作品选择器
+export const selectFilteredArtworks = (state: { workList: WorkListState }) => {
+ const { artworks, searchTerm } = state.workList;
+
+ if (!searchTerm.trim()) {
+ return artworks;
+ }
+
+ return artworks.filter(artwork =>
+ artwork.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ artwork.author.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ artwork.category.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ artwork.category.children.some(child =>
+ child.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ );
+};
+
+// 根据分区获取作品的选择器
+export const selectArtworksBySection = (sectionId: number) => (state: {
+ workList: WorkListState;
+ category: { sections: Section[] }
+}) => {
+ const section = state.category.sections.find(s => s.id === sectionId);
+ const filteredArtworks = selectFilteredArtworks(state);
+
+ if (!section) return [];
+
+ return filteredArtworks.filter(artwork => section.childrenid.includes(artwork.id));
+};
+
+export default workListSlice.reducer;
\ No newline at end of file
diff --git a/src/feature/user/UserHome.tsx b/src/feature/user/UserHome.tsx
index 5f925dc..cae4f6d 100644
--- a/src/feature/user/UserHome.tsx
+++ b/src/feature/user/UserHome.tsx
@@ -3,7 +3,7 @@
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useAppSelector, useAppDispatch } from '../../store/hooks';
-import { getUserInfo } from './userSlice';
+import { getUserDetail, getUserInfo } from './userSlice';
const { Title, Text, Paragraph } = Typography;
@@ -56,17 +56,19 @@
// 检查token并获取用户信息
useEffect(() => {
const initializeUser = async () => {
- const token = localStorage.getItem('token');
- if (!token) {
- // 如果没有token,重定向到登录页
- window.location.href = '/login';
- return;
+ if (userState.userid) {
+ await dispatch(getUserDetail(userState.userid));
+
}
// 如果用户信息为空或状态为idle,重新获取
if (!userState.username || userState.status === 'idle') {
try {
- await dispatch(getUserInfo()).unwrap();
+ await dispatch(getUserInfo()).then(
+ () => {
+ dispatch(getUserDetail(userState.userid));
+ }
+ )
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取用户信息失败,可能token过期,重定向到登录页
@@ -75,6 +77,7 @@
return;
}
}
+
setPageLoading(false);
};
@@ -139,11 +142,11 @@
// 如果页面正在初始化,显示加载状态
if (pageLoading || userState.status === 'loading') {
return (
- <div style={{
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
- minHeight: '400px'
+ <div style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ minHeight: '400px'
}}>
<Spin size="large" tip="加载用户信息中..." />
</div>
@@ -153,19 +156,19 @@
// 如果获取用户信息失败
if (userState.status === 'failed' || !userState.username) {
return (
- <div style={{
- display: 'flex',
- justifyContent: 'center',
- alignItems: 'center',
+ <div style={{
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
minHeight: '400px',
flexDirection: 'column'
}}>
- <Empty
+ <Empty
description="无法获取用户信息"
style={{ marginBottom: 16 }}
/>
- <Button
- type="primary"
+ <Button
+ type="primary"
onClick={() => dispatch(getUserInfo())}
>
重新加载
@@ -174,21 +177,24 @@
);
}
+
+
+
return (
<div className="user-home-container" style={{ padding: '0 24px' }}>
{/* 用户信息横栏 */}
- <Card
- style={{
- marginBottom: 24,
+ <Card
+ style={{
+ marginBottom: 24,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none'
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Space size="large" align="center">
- <Avatar
- size={80}
- icon={<UserOutlined />}
+ <Avatar
+ size={80}
+ icon={<UserOutlined />}
style={{ backgroundColor: '#fff', color: '#667eea' }}
/>
<div>
@@ -216,12 +222,12 @@
<Row gutter={24}>
{/* 左侧:作品展示栏 */}
<Col span={16}>
- <Card
+ <Card
title={
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span>我的作品</span>
- <Button
- type="primary"
+ <Button
+ type="primary"
icon={<PlusOutlined />}
onClick={() => {
// 这里后续添加跳转到发布作品页面的逻辑
@@ -239,7 +245,7 @@
<Spin size="large" tip="加载作品中..." />
</div>
) : userWorks.length === 0 ? (
- <Empty
+ <Empty
description="暂无作品,快去发布第一个作品吧!"
style={{ padding: '50px 0' }}
/>
@@ -247,7 +253,7 @@
<Row gutter={[16, 16]}>
{userWorks.map((work) => (
<Col span={12} key={work.id}>
- <Card
+ <Card
hoverable
style={{ height: '100%' }}
actions={[
@@ -261,27 +267,27 @@
>
<Card.Meta
title={
- <div style={{
- overflow: 'hidden',
- textOverflow: 'ellipsis',
- whiteSpace: 'nowrap'
+ <div style={{
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ whiteSpace: 'nowrap'
}}>
{work.title}
</div>
}
description={
<div>
- <Paragraph
- ellipsis={{ rows: 2 }}
+ <Paragraph
+ ellipsis={{ rows: 2 }}
style={{ marginBottom: 8, minHeight: '40px' }}
>
{work.description || '暂无描述'}
</Paragraph>
- <div style={{
- display: 'flex',
- justifyContent: 'space-between',
- fontSize: '12px',
- color: '#999'
+ <div style={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ fontSize: '12px',
+ color: '#999'
}}>
<span>
<CalendarOutlined /> {formatDate(work.createTime)}
@@ -301,7 +307,7 @@
{/* 右侧:通知栏 */}
<Col span={8}>
- <Card
+ <Card
title={
<div style={{ display: 'flex', alignItems: 'center' }}>
<BellOutlined style={{ marginRight: 8 }} />
@@ -314,8 +320,8 @@
<div>
{mockNotifications.map((notification, index) => (
<div key={notification.id}>
- <div
- style={{
+ <div
+ style={{
padding: '12px',
backgroundColor: notification.unread ? '#f6ffed' : 'transparent',
borderRadius: '4px',
@@ -323,7 +329,7 @@
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
- <div style={{
+ <div style={{
fontWeight: notification.unread ? 'bold' : 'normal',
marginBottom: '4px',
display: 'flex',
@@ -331,15 +337,15 @@
}}>
{notification.title}
{notification.unread && (
- <Badge
- color="red"
+ <Badge
+ color="red"
style={{ marginLeft: '8px' }}
/>
)}
</div>
- <div style={{
- color: '#666',
- fontSize: '14px',
+ <div style={{
+ color: '#666',
+ fontSize: '14px',
lineHeight: '1.4',
marginBottom: '8px'
}}>
@@ -354,7 +360,7 @@
{index < mockNotifications.length - 1 && <Divider style={{ margin: '0' }} />}
</div>
))}
-
+
{/* 查看更多按钮 */}
<div style={{ textAlign: 'center', marginTop: '16px' }}>
<Button type="link">查看全部通知</Button>
diff --git a/src/feature/user/UserLayout.tsx b/src/feature/user/UserLayout.tsx
index cb88dde..e3613ec 100644
--- a/src/feature/user/UserLayout.tsx
+++ b/src/feature/user/UserLayout.tsx
@@ -5,13 +5,13 @@
function UserLayout() {
return (
-
- <Content style={{ margin: '24px 16px 0' }}>
- <div style={{ padding: 24, minHeight: 360 }}>
- <Outlet /> {/* 这里会渲染子路由对应的组件 */}
- </div>
- </Content>
-
+
+ <Content style={{ margin: '24px 16px 0' }}>
+ <div style={{ padding: 24, minHeight: 360 }}>
+ <Outlet />
+ </div>
+ </Content>
+
);
}
diff --git a/src/feature/user/userSlice.ts b/src/feature/user/userSlice.ts
index c3f7e44..64daa4e 100644
--- a/src/feature/user/userSlice.ts
+++ b/src/feature/user/userSlice.ts
@@ -1,6 +1,6 @@
// src/store/userSlice.ts
import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit';
-import type { UserInfo } from '../../api/User/type';
+import type { UserDetailInfo, UserInfo } from '../../api/User/type';
import UserAPi from '../../api/User/UserApi';
// 定义用户信息的类型
@@ -39,6 +39,23 @@
}
);
+export const getUserDetail = createAsyncThunk<
+ UserDetailInfo,
+ string,
+ {rejectValue:string}
+>(
+ 'user/getUserDetail',
+ async (id, { rejectWithValue }) => {
+ if(!id) return rejectWithValue("未获取到用户信息");
+ const response = await UserAPi.getMeDetail(id);
+ if (response.data.code == 0) {
+ return response.data.data;
+ } else {
+ return rejectWithValue(response.data.message);
+ }
+ }
+);
+
// 创建 userSlice
const userSlice = createSlice({
name: 'user',
@@ -62,7 +79,21 @@
.addCase(getUserInfo.rejected, (state, action) => {
state.status = 'failed';
state.error = action.error.message ?? 'Unknown error';
- });
+ })
+ // getUserDetailFromState
+ .addCase(getUserDetail.pending, (state) => {
+ state.status = 'loading';
+ state.error = null;
+ })
+ .addCase(getUserDetail.fulfilled, (state, action: PayloadAction<UserDetailInfo>) => {
+ state.status = 'succeeded';
+ state.username = action.payload.username;
+ state.email = action.payload.email || '';
+ })
+ .addCase(getUserDetail.rejected, (state, action) => {
+ state.status = 'failed';
+ state.error = action.payload || action.error.message || 'Unknown error';
+ })
},
});
diff --git a/src/feature/work/Work.tsx b/src/feature/work/Work.tsx
index 56939d1..d9b08f0 100644
--- a/src/feature/work/Work.tsx
+++ b/src/feature/work/Work.tsx
@@ -100,8 +100,6 @@
}
}, [work_id, dispatch]);
- // ==================== EditWork 集成功能 ====================
-
// 更新作品信息
const handleUpdateArtwork = useCallback(async (updates: Partial<ArtworkData>): Promise<void> => {
if (!work_id || !currentArtwork) return;
@@ -157,17 +155,6 @@
}
}, [work_id, dispatch, comments.current, comments.pageSize]);
- // 兼容旧的编辑处理器
- const handleEditArtwork = useCallback((): void => {
- if (isAuthor) {
- setShowEditControls(true);
- message.info('请使用上方的编辑控件来修改作品信息');
- } else {
- message.warning('您没有编辑此作品的权限');
- }
- }, [isAuthor]);
-
- // ==================== 渲染逻辑 ====================
// 加载状态
if (loading.artwork) {
@@ -275,7 +262,6 @@
currentPage={safeComments.current}
pageSize={safeComments.pageSize}
isAuthor={isAuthor}
- onEditArtwork={handleEditArtwork}
/>
</Flex>
</div>
diff --git a/src/feature/work/WorkComponents.tsx b/src/feature/work/WorkComponents.tsx
index 64ef37a..79f976a 100644
--- a/src/feature/work/WorkComponents.tsx
+++ b/src/feature/work/WorkComponents.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { Card, Typography, Tag, Flex, Table, Collapse, List, Spin, Alert, Button, Input, Form, message } from 'antd';
-import { EditOutlined, SendOutlined } from '@ant-design/icons';
+import { SendOutlined } from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import type { ColumnsType } from 'antd/es/table';
import type { PaginationConfig } from 'antd/es/pagination';
@@ -59,7 +59,7 @@
description: string;
isAuthor?: boolean;
onEdit?: () => void;
-}> = ({ name, author, category, description, isAuthor = false, onEdit }) => (
+}> = ({ name, author, category, description }) => (
<Card style={{ marginBottom: 20 }}>
<Flex justify="space-between" align="flex-start">
<div style={{ flex: 1 }}>
@@ -74,11 +74,6 @@
<ReactMarkdown>{description}</ReactMarkdown>
</div>
</div>
- {isAuthor && (
- <Button type="primary" icon={<EditOutlined />} onClick={onEdit} style={{ marginLeft: 16 }}>
- 编辑作品
- </Button>
- )}
</Flex>
</Card>
);
diff --git a/src/feature/work/WorkPage.css b/src/feature/work/WorkPage.css
deleted file mode 100644
index 14b815a..0000000
--- a/src/feature/work/WorkPage.css
+++ /dev/null
@@ -1,155 +0,0 @@
-/* 作品页面容器 */
-.work-page-container {
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
-}
-
-/* 返回按钮 */
-.back-button {
- margin-bottom: 20px;
-}
-
-/* 作品头部 */
-.work-header {
- margin-bottom: 20px;
-}
-
-.work-header .ant-typography {
- margin-bottom: 8px;
-}
-
-.work-meta {
- margin: 16px 0;
-}
-
-.like-button {
- margin-top: 10px;
-}
-
-/* 内容区域 */
-.work-content {
- padding: 20px;
- background: #f9f9f9;
- border-radius: 4px;
-}
-
-/* Bug反馈区域 */
-.bug-report-section {
- margin-top: 20px;
-}
-
-.bug-item {
- width: 100%;
- padding: 15px;
- border: 1px solid #f0f0f0;
- border-radius: 4px;
- margin-bottom: 10px;
-}
-
-.bug-header {
- display: flex;
- justify-content: space-between;
- margin-bottom: 8px;
-}
-
-.bug-content {
- margin: 10px 0;
-}
-
-/* 讨论区 */
-.discussion-section {
- margin-top: 20px;
-}
-
-.discussion-container {
- display: flex;
- gap: 20px;
-}
-
-.discussion-list {
- width: 300px;
- border-right: 1px solid #f0f0f0;
- padding-right: 20px;
-}
-
-.discussion-detail {
- flex: 1;
-}
-
-.discussion-item {
- padding: 15px;
- cursor: pointer;
- border-radius: 4px;
- margin-bottom: 10px;
- transition: all 0.3s;
-}
-
-.discussion-item:hover {
- background: #f5f5f5;
-}
-
-.discussion-item.active {
- background: #e6f7ff;
- border-left: 3px solid #1890ff;
-}
-
-.discussion-detail-content {
- padding: 20px;
- background: #f9f9f9;
- border-radius: 4px;
- margin-bottom: 20px;
-}
-
-.discussion-title {
- font-size: 18px;
- display: block;
- margin-bottom: 10px;
-}
-
-.discussion-meta {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 15px;
-}
-
-.discussion-body {
- margin-bottom: 20px;
-}
-
-.comment-item {
- padding: 15px 0;
- border-bottom: 1px solid #f0f0f0;
-}
-
-.comment-header {
- display: flex;
- align-items: center;
- gap: 10px;
- margin-bottom: 10px;
-}
-
-.comment-actions {
- margin-top: 10px;
-}
-
-.new-discussion-btn {
- margin-bottom: 20px;
-}
-
-/* 响应式调整 */
-@media (max-width: 768px) {
- .discussion-container {
- flex-direction: column;
- }
-
- .discussion-list {
- width: 100%;
- border-right: none;
- padding-right: 0;
- border-bottom: 1px solid #f0f0f0;
- padding-bottom: 20px;
- margin-bottom: 20px;
- }
-}
\ No newline at end of file
diff --git a/src/feature/work/WorkPage.tsx b/src/feature/work/WorkPage.tsx
deleted file mode 100644
index 3a108a7..0000000
--- a/src/feature/work/WorkPage.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import React, { useState, useEffect } from 'react';
-import { useParams, useNavigate } from 'react-router';
-import { Button, Tabs, message, Spin, Tag, Typography, Space, Divider } from 'antd';
-import { ArrowLeftOutlined, LikeOutlined, BugOutlined, CommentOutlined } from '@ant-design/icons';
-import WorkAPI from '../../api/workApi';
-import BugReportSection from '../../components/BugReportSection';
-import DiscussionSection from '../../components/DiscussionSection';
-import type { Work } from '../../api/otherType';
-
-const { Title, Text, Paragraph } = Typography;
-const { TabPane } = Tabs;
-
-const WorkPage: React.FC = () => {
- const { id } = useParams<{ id: string }>();
- const navigate = useNavigate();
- const [work, setWork] = useState<Work | null>(null);
- const [loading, setLoading] = useState(true);
- const [activeTab, setActiveTab] = useState('details');
-
- // 加载作品数据
- useEffect(() => {
- const loadWork = async () => {
- try {
- const workData = await WorkAPI.getWorkById(Number(id));
- setWork(workData);
- } catch (error) {
- message.error('加载作品失败');
- navigate('/');
- } finally {
- setLoading(false);
- }
- };
-
- loadWork();
- }, [id, navigate]);
-
- // 点赞处理
- const handleLike = async () => {
- try {
- await WorkAPI.likeWork(Number(id));
- setWork(prev => prev ? { ...prev, likes: prev.likes + 1 } : null);
- message.success('点赞成功');
- } catch (error) {
- message.error('点赞失败');
- }
- };
-
- if (loading) {
- return <Spin size="large" className="center-spinner" />;
- }
-
- if (!work) {
- return <div>作品不存在</div>;
- }
-
- return (
- <div className="work-page-container">
- <Button
- type="text"
- icon={<ArrowLeftOutlined />}
- onClick={() => navigate(-1)}
- className="back-button"
- >
- 返回
- </Button>
-
- <div className="work-header">
- <Title level={2}>{work.title}</Title>
- <Text type="secondary">作者: {work.author}</Text>
-
- <div className="work-meta">
- <Space size="middle">
- <Tag color="blue">{work.categoryName}</Tag>
- <Text>浏览: {work.views}</Text>
- <Text>点赞: {work.likes}</Text>
- <Text>上传时间: {new Date(work.createTime).toLocaleDateString()}</Text>
- </Space>
- </div>
-
- <Button
- type="primary"
- icon={<LikeOutlined />}
- onClick={handleLike}
- className="like-button"
- >
- 点赞
- </Button>
- </div>
-
- <Divider />
-
- <Tabs activeKey={activeTab} onChange={setActiveTab}>
- <TabPane tab="作品详情" key="details">
- <div className="work-content">
- <Title level={4}>作品描述</Title>
- <Paragraph>{work.description}</Paragraph>
-
- <Title level={4}>作品内容</Title>
- <div className="content-container">
- {work.content}
- </div>
- </div>
- </TabPane>
-
- <TabPane
- tab={
- <span>
- <BugOutlined /> Bug反馈
- </span>
- }
- key="bugs"
- >
- <BugReportSection workId={work.id} />
- </TabPane>
-
- <TabPane
- tab={
- <span>
- <CommentOutlined /> 交流区
- </span>
- }
- key="discussions"
- >
- <DiscussionSection workId={work.id} />
- </TabPane>
- </Tabs>
- </div>
- );
-};
-
-export default WorkPage;
\ No newline at end of file
diff --git a/src/feature/work/mockData.ts b/src/feature/work/mockData.ts
index 8470e61..9caaed7 100644
--- a/src/feature/work/mockData.ts
+++ b/src/feature/work/mockData.ts
@@ -127,8 +127,8 @@
// 作品数据集合
export const mockArtworks: Record<string, ArtworkData> = {
- '12345': {
- artworkId: '12345',
+ '1': {
+ artworkId: '1',
artworkCover: 'https://picsum.photos/300/400?random=1',
author: '视觉设计师张三',
authorId: 'author_zhangsan_001',
@@ -189,31 +189,16 @@
{ username: '创意总监', userId: 'user_director_104' },
],
usersSeedingHistory: [
- {
- username: '资深下载者', uploadTotal: '156.8GB',
- userId: ''
- },
- {
- username: '设计素材库', uploadTotal: '89.2GB',
- userId: ''
- },
- {
- username: '创意工作室', uploadTotal: '67.5GB',
- userId: ''
- },
- {
- username: '学院资源组', uploadTotal: '45.3GB',
- userId: ''
- },
- {
- username: '独立设计师', uploadTotal: '23.7GB',
- userId: ''
- },
+ { username: '资深下载者', uploadTotal: '156.8GB', userId: 'hist_user_001' },
+ { username: '设计素材库', uploadTotal: '89.2GB', userId: 'hist_user_002' },
+ { username: '创意工作室', uploadTotal: '67.5GB', userId: 'hist_user_003' },
+ { username: '学院资源组', uploadTotal: '45.3GB', userId: 'hist_user_004' },
+ { username: '独立设计师', uploadTotal: '23.7GB', userId: 'hist_user_005' },
],
},
- '23456': {
- artworkId: '23456',
+ '2': {
+ artworkId: '2',
artworkCover: 'https://picsum.photos/300/400?random=2',
author: 'UI设计师李四',
authorId: 'author_lisi_002',
@@ -256,26 +241,17 @@
{ username: '产品经理小王', userId: 'user_pm_wang_202' },
],
usersSeedingHistory: [
- {
- username: 'UI设计公司', uploadTotal: '78.9GB',
- userId: ''
- },
- {
- username: '设计师联盟', uploadTotal: '45.6GB',
- userId: ''
- },
- {
- username: '学习小组', uploadTotal: '23.4GB',
- userId: ''
- },
+ { username: 'UI设计公司', uploadTotal: '78.9GB', userId: 'hist_ui_001' },
+ { username: '设计师联盟', uploadTotal: '45.6GB', userId: 'hist_ui_002' },
+ { username: '学习小组', uploadTotal: '23.4GB', userId: 'hist_ui_003' },
],
},
- '67890': {
- artworkId: '67890',
+ '3': {
+ artworkId: '3',
artworkCover: 'https://picsum.photos/300/400?random=6',
author: '刘松林',
- authorId: '2', // 用户ID为2
+ authorId: '2',
artworkName: 'React组件库开发指南',
artworkCategory: '前端开发',
comments: [],
@@ -313,33 +289,176 @@
{ username: 'React爱好者', userId: 'user_react_fan_602' },
],
usersSeedingHistory: [
+ { username: '大厂前端团队', uploadTotal: '567.8GB', userId: 'hist_dev_001' },
+ { username: '开源社区', uploadTotal: '234.5GB', userId: 'hist_dev_002' },
+ { username: '技术培训机构', uploadTotal: '189.7GB', userId: 'hist_dev_003' },
+ ],
+ },
+
+ '4': {
+ artworkId: '4',
+ artworkCover: 'https://picsum.photos/300/400?random=7',
+ author: '3D艺术家王五',
+ authorId: 'author_wangwu_004',
+ artworkName: '机械战士3D模型',
+ artworkCategory: '3D建模',
+ comments: [],
+ artworkDescription: `# 机械战士3D模型
+
+一个高精度的科幻机械战士3D模型,包含完整的材质贴图和动画骨骼系统。
+
+## 模型特点
+
+### 🤖 设计风格
+- 未来科幻风格
+- 硬表面建模技术
+- PBR材质工作流
+- 模块化装备系统
+
+### 📐 技术规格
+- **面数**: 25,000 三角面
+- **贴图分辨率**: 4K PBR贴图组
+- **骨骼系统**: 完整人形骨骼
+- **动画**: 10个基础动作
+
+适用于游戏开发、影视制作和3D打印。`,
+ versionList: [
{
- username: '大厂前端团队', uploadTotal: '567.8GB',
- userId: ''
+ version: '1.0',
+ seedFile: 'magnet:?xt=urn:btih:mech_warrior_v1_0&dn=机械战士v1.0.zip',
+ versionDescription: `## 基础版本 v1.0
+
+### 核心内容
+- 🎯 **高精度3D模型** (.fbx, .obj)
+- 🎨 **4K PBR贴图组**
+- 🦴 **完整骨骼系统**
+- 🎬 **基础动画文件**`,
},
+ ],
+ usersSeedingCurrently: [
+ { username: '游戏开发者小陈', userId: 'user_gamedev_301' },
+ { username: '3D建模师', userId: 'user_3dmodeler_302' },
+ ],
+ usersSeedingHistory: [
+ { username: '游戏工作室', uploadTotal: '234.5GB', userId: 'hist_3d_001' },
+ { username: '影视特效团队', uploadTotal: '178.3GB', userId: 'hist_3d_002' },
+ { username: '独立开发者', uploadTotal: '98.7GB', userId: 'hist_3d_003' },
+ ],
+ },
+
+ '5': {
+ artworkId: '5',
+ artworkCover: 'https://picsum.photos/300/400?random=8',
+ author: '摄影师赵六',
+ authorId: 'author_zhaoliu_005',
+ artworkName: '城市夜景摄影集',
+ artworkCategory: '摄影作品',
+ comments: [],
+ artworkDescription: `# 城市夜景摄影集
+
+一组精美的城市夜景摄影作品,捕捉了都市夜晚的璀璨光影。
+
+## 作品特色
+
+### 📸 拍摄技法
+- 长曝光技术
+- HDR合成处理
+- 光轨艺术表现
+- 城市建筑几何美学
+
+### 🎨 后期处理
+- **RAW格式**: 无损原始文件
+- **精修版本**: Lightroom + Photoshop
+- **色彩分级**: 电影级调色
+- **分辨率**: 6000x4000像素
+
+包含20张高分辨率摄影作品,适合商业使用和艺术收藏。`,
+ versionList: [
{
- username: '开源社区', uploadTotal: '234.5GB',
- userId: ''
+ version: '1.0',
+ seedFile: 'magnet:?xt=urn:btih:night_city_photos_v1_0&dn=城市夜景v1.0.zip',
+ versionDescription: `## 完整版本 v1.0
+
+### 包含内容
+- 📷 **RAW原始文件** (20张)
+- 🎨 **精修JPG版本** (高分辨率)
+- 📋 **拍摄参数记录**
+- 📍 **拍摄地点信息**`,
},
+ ],
+ usersSeedingCurrently: [
+ { username: '摄影爱好者小林', userId: 'user_photo_401' },
+ { username: '设计师小美', userId: 'user_designer_402' },
+ ],
+ usersSeedingHistory: [
+ { username: '摄影工作室', uploadTotal: '445.8GB', userId: 'hist_photo_001' },
+ { username: '商业摄影师', uploadTotal: '367.2GB', userId: 'hist_photo_002' },
+ { username: '摄影学院', uploadTotal: '289.1GB', userId: 'hist_photo_003' },
+ ],
+ },
+
+ '6': {
+ artworkId: '6',
+ artworkCover: 'https://picsum.photos/300/400?random=9',
+ author: '插画师孙七',
+ authorId: 'author_sunqi_006',
+ artworkName: '奇幻世界插画系列',
+ artworkCategory: '插画艺术',
+ comments: [],
+ artworkDescription: `# 奇幻世界插画系列
+
+一套充满想象力的奇幻题材插画作品,包含角色设计、场景概念图和完整插图。
+
+## 创作风格
+
+### 🎨 艺术特色
+- 欧美奇幻风格
+- 数字绘画技法
+- 丰富色彩层次
+- 细腻光影表现
+
+### 📚 作品内容
+- **角色设计**: 15个原创角色
+- **场景概念**: 8个奇幻场景
+- **完整插图**: 12张精美插画
+- **线稿资源**: 黑白线稿版本
+
+适合游戏美术、小说封面、卡牌设计等多种用途。`,
+ versionList: [
{
- username: '技术培训机构', uploadTotal: '189.7GB',
- userId: ''
+ version: '1.0',
+ seedFile: 'magnet:?xt=urn:btih:fantasy_art_v1_0&dn=奇幻插画v1.0.zip',
+ versionDescription: `## 标准版本 v1.0
+
+### 核心内容
+- 🎨 **高分辨率插画** (35张)
+- ✏️ **线稿资源包**
+- 🎯 **PSD分层文件**
+- 📖 **创作过程记录**`,
},
],
+ usersSeedingCurrently: [
+ { username: '插画学习者', userId: 'user_illustrator_501' },
+ { username: '游戏美术师', userId: 'user_gameart_502' },
+ ],
+ usersSeedingHistory: [
+ { username: '插画师联盟', uploadTotal: '378.9GB', userId: 'hist_art_001' },
+ { username: '游戏美术团队', uploadTotal: '256.4GB', userId: 'hist_art_002' },
+ { username: '艺术学院', uploadTotal: '189.6GB', userId: 'hist_art_003' },
+ ],
},
};
// 获取指定作品的评论数据
export const getCommentsForArtwork = (artworkId: string): Comment[] => {
- // 为不同作品生成不同的评论
const commentVariations: Record<string, Comment[]> = {
- '12345': baseComments,
- '23456': baseComments.slice(0, 5).map(comment => ({
+ '1': baseComments,
+ '2': baseComments.slice(0, 5).map(comment => ({
...comment,
id: `ui_${comment.id}`,
content: comment.content.replace('作品', 'UI套件').replace('设计', '界面设计'),
})),
- '67890': [
+ '3': [
{
id: 'dev_comment_1',
content: '这个组件库的设计思路很棒!TypeScript类型定义特别完善。',
@@ -366,6 +485,21 @@
child: [],
},
],
+ '4': baseComments.slice(0, 4).map(comment => ({
+ ...comment,
+ id: `3d_${comment.id}`,
+ content: comment.content.replace('作品', '3D模型').replace('设计', '建模'),
+ })),
+ '5': baseComments.slice(0, 6).map(comment => ({
+ ...comment,
+ id: `photo_${comment.id}`,
+ content: comment.content.replace('作品', '摄影作品').replace('设计思路', '拍摄技法'),
+ })),
+ '6': baseComments.slice(0, 5).map(comment => ({
+ ...comment,
+ id: `art_${comment.id}`,
+ content: comment.content.replace('作品', '插画').replace('设计', '绘画'),
+ })),
};
return commentVariations[artworkId] || baseComments;
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index d380bde..eeebbe0 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -12,17 +12,13 @@
import CreateWork from "../feature/work/CreateWork";
import UserHome from "../feature/user/UserHome";
+// import Tmp from "../feature/home/tmp";
// 创建受保护的组件
const ProtectedHome = withProtect(Home);
const ProtectedWork = withProtect(Work);
const ProtectedCreatWork = withProtect(CreateWork)
-import FilmCategory from '../feature/categories/MovieCategory';
-import MusicCategory from '../feature/categories/MusicCategory';
-import GameCategory from '../feature/categories/GameCategory';
-import OtherCategory from '../feature/categories/OtherCategory';
-import WorkPage from '../feature/work/WorkPage';
-
+const ProtectedUserHome = withProtect(UserHome)
export default createBrowserRouter([
{
path: "/",
@@ -32,14 +28,9 @@
path: "/",
Component: ProtectedHome,
},
- {path: 'categories/film', Component: FilmCategory,},
- {path: 'categories/music', Component: MusicCategory,},
- {path: 'categories/game', Component: GameCategory,},
- {path: 'categories/other', Component: OtherCategory,},
- {path: '/works/:id', Component: WorkPage, },
{
path: "user",
- Component: UserHome,
+ Component: ProtectedUserHome,
},
{
Component: AuthLayout,
@@ -56,7 +47,11 @@
{
path:"/work/creat",
Component: ProtectedCreatWork
- }
+ },
+ // {
+ // path:"/tmp",
+ // Component: Tmp
+ // }
],
},
]);
\ No newline at end of file
diff --git a/src/store/store.ts b/src/store/store.ts
index cb5b975..b543f75 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -2,12 +2,15 @@
import authReducer from "../feature/auth/authSlice"
import userReducer from "../feature/user/userSlice"
import workReducer from "../feature/work/workSlice"
-
+import workListReducer from '../feature/home/workListSlice';
+import categoryReducer from '../feature/home/categorySlice';
export const store = configureStore({
reducer: {
auth: authReducer,
user: userReducer,
work: workReducer,
+ workList: workListReducer,
+ category: categoryReducer,
},
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(),
})
diff --git a/src/test/category/GameCategory.test.tsx b/src/test/category/GameCategory.test.tsx
deleted file mode 100644
index ad42353..0000000
--- a/src/test/category/GameCategory.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import GameCategory from '../../feature/categories/GameCategory';
-import { Provider } from 'react-redux';
-import { store } from '../../store/store';
-import { MemoryRouter } from 'react-router';
-
-describe('GameCategory Component', () => {
- it('renders game category page with search and game cards', () => {
- render(
- <MemoryRouter>
- <Provider store={store}>
- <GameCategory />
- </Provider>
- </MemoryRouter>
- );
-
- // 验证标题和描述
- expect(screen.getByText('游戏资源分区')).toBeInTheDocument();
- expect(screen.getByText('PC、主机游戏资源分享')).toBeInTheDocument();
-
- // 验证搜索框
- const searchInput = screen.getByPlaceholderText('搜索游戏名称或开发商');
- expect(searchInput).toBeInTheDocument();
-
- // 验证游戏卡片
- expect(screen.getByText('赛博朋克2077')).toBeInTheDocument();
- expect(screen.getByText('艾尔登法环')).toBeInTheDocument();
-
- // 验证开发商信息
- expect(screen.getByText('开发商: CD Projekt Red')).toBeInTheDocument();
- expect(screen.getByText('开发商: FromSoftware')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/test/category/MovieCategory.test.tsx b/src/test/category/MovieCategory.test.tsx
deleted file mode 100644
index be1ed01..0000000
--- a/src/test/category/MovieCategory.test.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import MovieCategory from '../../feature/categories/MovieCategory';
-import { Provider } from 'react-redux';
-import { store } from '../../store/store';
-import { MemoryRouter } from 'react-router';
-
-describe('MovieCategory Component', () => {
- it('renders movie category page with search and movie cards', () => {
- render(
- <MemoryRouter>
- <Provider store={store}>
- <MovieCategory />
- </Provider>
- </MemoryRouter>
- );
-
- // 验证标题和描述
- expect(screen.getByText('影视资源分区')).toBeInTheDocument();
- expect(screen.getByText('高清电影、电视剧、纪录片资源分享')).toBeInTheDocument();
-
- // 验证搜索框
- const searchInput = screen.getByPlaceholderText('搜索电影或导演');
- expect(searchInput).toBeInTheDocument();
-
- // 验证电影卡片
- expect(screen.getByText('盗梦空间')).toBeInTheDocument();
- expect(screen.getByText('肖申克的救赎')).toBeInTheDocument();
-
- // 验证导演信息
- expect(screen.getByText('导演: 克里斯托弗·诺兰')).toBeInTheDocument();
- expect(screen.getByText('导演: 弗兰克·德拉邦特')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/test/category/MusicCategory.test.tsx b/src/test/category/MusicCategory.test.tsx
deleted file mode 100644
index 4de1bdf..0000000
--- a/src/test/category/MusicCategory.test.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import MusicCategory from '../../feature/categories/MusicCategory';
-import { Provider } from 'react-redux';
-import { store } from '../../store/store';
-import { MemoryRouter } from 'react-router';
-
-describe('MusicCategory Component', () => {
- it('renders music category page with search, filters and music cards', () => {
- render(
- <MemoryRouter>
- <Provider store={store}>
- <MusicCategory />
- </Provider>
- </MemoryRouter>
- );
-
- // 验证标题和描述
- expect(screen.getByText('音乐资源分区')).toBeInTheDocument();
- expect(screen.getByText('高质量音乐资源共享,保持分享率,共建良好PT环境')).toBeInTheDocument();
-
- // 验证搜索框
- const searchInput = screen.getByPlaceholderText('搜索音乐名称或艺术家');
- expect(searchInput).toBeInTheDocument();
-
- // 验证音质筛选
- expect(screen.getByText('音质筛选:')).toBeInTheDocument();
- expect(screen.getByText('全部')).toBeInTheDocument();
- expect(screen.getByText('FLAC')).toBeInTheDocument();
-
- // 验证分类标签
- expect(screen.getByText('欧美流行 (15)')).toBeInTheDocument();
- expect(screen.getByText('华语流行 (23)')).toBeInTheDocument();
-
- // 验证音乐卡片
- expect(screen.getByText('Blinding Lights')).toBeInTheDocument();
- expect(screen.getByText('Bohemian Rhapsody')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/test/category/OtherCategory.test.tsx b/src/test/category/OtherCategory.test.tsx
deleted file mode 100644
index dd1e793..0000000
--- a/src/test/category/OtherCategory.test.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import OtherCategory from '../../feature/categories/OtherCategory';
-import { Provider } from 'react-redux';
-import { store } from '../../store/store';
-import { MemoryRouter } from 'react-router';
-
-describe('OtherCategory Component', () => {
- it('renders other resources category page with search and resource cards', () => {
- render(
- <MemoryRouter>
- <Provider store={store}>
- <OtherCategory />
- </Provider>
- </MemoryRouter>
- );
-
- // 验证标题和描述
- expect(screen.getByText('其他资源分区')).toBeInTheDocument();
- expect(screen.getByText('软件、电子书等其他类型资源')).toBeInTheDocument();
-
- // 验证搜索框
- const searchInput = screen.getByPlaceholderText('搜索资源名称或描述');
- expect(searchInput).toBeInTheDocument();
-
- // 验证资源卡片
- expect(screen.getByText('盗梦空间')).toBeInTheDocument();
- expect(screen.getByText('肖申克的救赎')).toBeInTheDocument();
-
- // 验证类型信息
- expect(screen.getByText('类型: 未知类型')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/test/work/WorkPage.test.tsx b/src/test/work/WorkPage.test.tsx
deleted file mode 100644
index 4dc5036..0000000
--- a/src/test/work/WorkPage.test.tsx
+++ /dev/null
@@ -1,268 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import WorkPage from '../../feature/work/WorkPage';
-import { Provider } from 'react-redux';
-import { store } from '../../store/store';
-import { MemoryRouter, Route, Routes, useNavigate } from 'react-router';
-import { act } from 'react-dom/test-utils';
-import WorkAPI from '../../api/workApi';
-import type { Work } from '../../api/otherType';
-import '@testing-library/jest-dom';
-
-// 模拟整个WorkAPI类
-jest.mock('../../api/workApi', () => {
- return {
- __esModule: true,
- default: {
- getWorkById: jest.fn(),
- likeWork: jest.fn(),
- // 添加其他可能用到的方法
- getBugReports: jest.fn(),
- getDiscussions: jest.fn()
- }
- }
-});
-
-// 模拟子组件
-jest.mock('../../components/BugReportSection', () => () => (
- <div data-testid="bug-report-mock">BugReportSection Mock</div>
-));
-
-jest.mock('../../components/DiscussionSection', () => () => (
- <div data-testid="discussion-mock">DiscussionSection Mock</div>
-));
-
-// 模拟react-router-dom的useNavigate
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useNavigate: () => jest.fn()
-}));
-
-describe('WorkPage Component', () => {
- const mockWork: Work = {
- id: 1,
- title: '测试作品',
- author: '测试作者',
- categoryName: '测试分类',
- views: 100,
- likes: 50,
- createTime: '2023-01-01T00:00:00Z',
- description: '测试描述',
- content: '测试内容',
- size: '',
- data: function (): unknown {
- throw new Error('Function not implemented.');
- },
- artist: '',
- quality: '',
- genre: [],
- authorId: 0,
- categoryId: 0,
- coverUrl: '',
- attachments: [],
- bugCount: 0,
- discussionCount: 0
- };
-
- beforeEach(() => {
- // 重置所有模拟
- jest.clearAllMocks();
-
- // 设置默认模拟实现
- (WorkAPI.getWorkById as jest.Mock).mockResolvedValue(mockWork);
- (WorkAPI.likeWork as jest.Mock).mockResolvedValue({});
- });
-
- it('renders loading spinner initially', async () => {
- // 延迟API响应以测试加载状态
- let resolvePromise: (value: Work) => void;
- (WorkAPI.getWorkById as jest.Mock).mockImplementation(
- () => new Promise<Work>((resolve) => {
- resolvePromise = resolve;
- })
- );
-
- render(
- <MemoryRouter initialEntries={['/works/1']}>
- <Provider store={store}>
- <Routes>
- <Route path="/works/:id" element={<WorkPage />} />
- </Routes>
- </Provider>
- </MemoryRouter>
- );
-
- // 验证加载状态
- expect(screen.getByRole('status')).toBeInTheDocument();
- expect(screen.getByText(/加载/)).toBeInTheDocument();
-
- // 完成API请求
- await act(async () => {
- resolvePromise(mockWork);
- });
-
- // 验证加载完成后内容
- expect(await screen.findByText('测试作品')).toBeInTheDocument();
- });
-
- it('renders work details after loading', async () => {
- render(
- <MemoryRouter initialEntries={['/works/1']}>
- <Provider store={store}>
- <Routes>
- <Route path="/works/:id" element={<WorkPage />} />
- </Routes>
- </Provider>
- </MemoryRouter>
- );
-
- // 等待数据加载完成
- expect(await screen.findByText('测试作品')).toBeInTheDocument();
-
- // 验证头部信息
- expect(screen.getByText('作者: 测试作者')).toBeInTheDocument();
- expect(screen.getByText('测试分类')).toHaveClass('ant-tag');
- expect(screen.getByText('浏览: 100')).toBeInTheDocument();
- expect(screen.getByText('点赞: 50')).toBeInTheDocument();
- expect(screen.getByText(/2023/)).toBeInTheDocument();
-
- // 验证作品内容
- expect(screen.getByRole('heading', { level: 4, name: '作品描述' })).toBeInTheDocument();
- expect(screen.getByText('测试描述')).toBeInTheDocument();
- expect(screen.getByRole('heading', { level: 4, name: '作品内容' })).toBeInTheDocument();
- expect(screen.getByText('测试内容')).toBeInTheDocument();
-
- // 验证标签页
- expect(screen.getByRole('tab', { name: '作品详情' })).toBeInTheDocument();
- expect(screen.getByRole('tab', { name: /Bug反馈/ })).toBeInTheDocument();
- expect(screen.getByRole('tab', { name: /交流区/ })).toBeInTheDocument();
- });
-
- it('handles back button click', async () => {
- const mockNavigate = jest.fn();
- (useNavigate as jest.Mock).mockReturnValue(mockNavigate);
-
- render(
- <MemoryRouter initialEntries={['/works/1']}>
- <Provider store={store}>
- <Routes>
- <Route path="/works/:id" element={<WorkPage />} />
- </Routes>
- </Provider>
- </MemoryRouter>
- );
-
- // 等待数据加载完成
- await screen.findByText('测试作品');
-
- // 点击返回按钮
- const backButton = screen.getByRole('button', { name: '返回' });
- await act(async () => {
- backButton.click();
- });
-
- // 验证导航被调用
- expect(mockNavigate).toHaveBeenCalledWith(-1);
- });
-
- it('handles like button click', async () => {
- render(
- <MemoryRouter initialEntries={['/works/1']}>
- <Provider store={store}>
- <Routes>
- <Route path="/works/:id" element={<WorkPage />} />
- </Routes>
- </Provider>
- </MemoryRouter>
- );
-
- // 等待数据加载完成
- await screen.findByText('测试作品');
-
- // 点击点赞按钮
- const likeButton = screen.getByRole('button', { name: '点赞' });
- await act(async () => {
- likeButton.click();
- });
-
- // 验证API调用
- expect(WorkAPI.likeWork).toHaveBeenCalledTimes(1);
- expect(WorkAPI.likeWork).toHaveBeenCalledWith(1);
-
- // 验证UI反馈
- expect(await screen.findByText('点赞成功')).toBeInTheDocument();
- });
-
- it('shows error message when work loading fails', async () => {
- const errorMessage = '加载失败';
- (WorkAPI.getWorkById as jest.Mock).mockRejectedValue(new Error(errorMessage));
-
- const mockNavigate = jest.fn();
- (useNavigate as jest.Mock).mockReturnValue(mockNavigate);
-
- render(
- <MemoryRouter initialEntries={['/works/1']}>
- <Provider store={store}>
- <Routes>
- <Route path="/works/:id" element={<WorkPage />} />
- </Routes>
- </Provider>
- </MemoryRouter>
- );
-
- // 验证错误消息
- expect(await screen.findByText('作品不存在')).toBeInTheDocument();
-
- // 验证导航到首页
- expect(mockNavigate).toHaveBeenCalledWith('/');
- });
-
- it('switches between tabs correctly', async () => {
- render(
- <MemoryRouter initialEntries={['/works/1']}>
- <Provider store={store}>
- <Routes>
- <Route path="/works/:id" element={<WorkPage />} />
- </Routes>
- </Provider>
- </MemoryRouter>
- );
-
- // 等待数据加载完成
- await screen.findByText('测试作品');
-
- // 初始显示作品详情
- expect(screen.getByText('测试描述')).toBeInTheDocument();
- expect(screen.queryByTestId('bug-report-mock')).not.toBeInTheDocument();
- expect(screen.queryByTestId('discussion-mock')).not.toBeInTheDocument();
-
- // 点击Bug反馈标签
- const bugTab = screen.getByRole('tab', { name: /Bug反馈/ });
- await act(async () => {
- bugTab.click();
- });
-
- // 验证Bug反馈内容显示
- expect(screen.queryByText('测试描述')).not.toBeInTheDocument();
- expect(screen.getByTestId('bug-report-mock')).toBeInTheDocument();
-
- // 点击交流区标签
- const discussionTab = screen.getByRole('tab', { name: /交流区/ });
- await act(async () => {
- discussionTab.click();
- });
-
- // 验证交流区内容显示
- expect(screen.queryByTestId('bug-report-mock')).not.toBeInTheDocument();
- expect(screen.getByTestId('discussion-mock')).toBeInTheDocument();
-
- // 切换回作品详情
- const detailsTab = screen.getByRole('tab', { name: '作品详情' });
- await act(async () => {
- detailsTab.click();
- });
-
- // 验证作品详情再次显示
- expect(screen.getByText('测试描述')).toBeInTheDocument();
- expect(screen.queryByTestId('discussion-mock')).not.toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
index 941aa98..bd06358 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -8,9 +8,8 @@
server: {
proxy: {
'/api': {
- target: 'http://localhost:8080/',
+ target: 'http://localhost:8080',
changeOrigin: true,
-
},
},
},