保存本地对routes.ts的修改
Change-Id: I4f4dbd8069893d7363e251130791dc0594be44e1
diff --git a/src/feature/categories/GameCategory.tsx b/src/feature/categories/GameCategory.tsx
new file mode 100644
index 0000000..3f5c78d
--- /dev/null
+++ b/src/feature/categories/GameCategory.tsx
@@ -0,0 +1,173 @@
+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
new file mode 100644
index 0000000..262ebd4
--- /dev/null
+++ b/src/feature/categories/MovieCategory.tsx
@@ -0,0 +1,172 @@
+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
new file mode 100644
index 0000000..f9057ee
--- /dev/null
+++ b/src/feature/categories/MusicCategory.tsx
@@ -0,0 +1,191 @@
+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
new file mode 100644
index 0000000..6c3318a
--- /dev/null
+++ b/src/feature/categories/OtherCategory.tsx
@@ -0,0 +1,178 @@
+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/Home.tsx b/src/feature/home/Home.tsx
index fc2c982..80e44eb 100644
--- a/src/feature/home/Home.tsx
+++ b/src/feature/home/Home.tsx
@@ -1,12 +1,116 @@
+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万' },
+];
+
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 },
+ ];
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>
+
+ {/* 分区 + 排行榜 容器,改为 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)' }}
+ />
+
+ {/* 渐变遮罩层 - 增强文字可读性 */}
+ <div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/40 to-transparent z-10"></div>
+
+ {/* 文字内容 */}
+ <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>
+
+ {/* 描述文本 - 悬停显示 */}
+ <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>
+ </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>
+ );
}
-
-
-export default Home
+export default Home;
\ No newline at end of file
diff --git a/src/feature/work/WorkPage.css b/src/feature/work/WorkPage.css
new file mode 100644
index 0000000..14b815a
--- /dev/null
+++ b/src/feature/work/WorkPage.css
@@ -0,0 +1,155 @@
+/* 作品页面容器 */
+.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
new file mode 100644
index 0000000..3a108a7
--- /dev/null
+++ b/src/feature/work/WorkPage.tsx
@@ -0,0 +1,131 @@
+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