完成主页, 作品页,作品编辑页
> 未对接后端接口
Change-Id: I5d62663602656da4940707e00f76bfe09d824c2c
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;