feat: 完善非遗项目和传承人页面功能

- 集成点赞和收藏按钮到详情页
- 优化列表页的数据展示和交互
- 改进页面布局和样式
- 提升用户浏览体验
This commit is contained in:
Leo 2025-10-13 21:43:13 +08:00
parent bca9345e02
commit 02b4a1eeec
4 changed files with 271 additions and 118 deletions

View File

@ -4,12 +4,14 @@
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { Row, Col, Tag, Breadcrumb, Image, Tabs, Spin, Empty, Space } from 'antd'
import { HomeOutlined, EnvironmentOutlined, EyeOutlined, HeartOutlined } from '@ant-design/icons'
import { Row, Col, Tag, Breadcrumb, Image, Tabs, Spin, Empty, Space, message } from 'antd'
import { HomeOutlined, EnvironmentOutlined, EyeOutlined, LikeOutlined } from '@ant-design/icons'
import { Link } from 'react-router-dom'
import type { HeritageItem } from '@types/index'
import { getHeritageById } from '@services/api'
import { getHeritageDetail } from '@services/heritageApi'
import { transformHeritageDetail } from '@utils/heritageTransform'
import FavoriteButton from '@components/FavoriteButton'
import LikeButton from '@components/LikeButton'
import CommentSection from '@components/CommentSection'
import './Detail.css'
@ -37,10 +39,14 @@ const HeritageDetail: React.FC = () => {
const fetchData = async (heritageId: string) => {
setLoading(true)
try {
const result = await getHeritageById(heritageId)
setData(result)
} catch (error) {
// 调用后端API获取详情
const result = await getHeritageDetail(heritageId)
// 转换数据格式
const transformedData = transformHeritageDetail(result)
setData(transformedData)
} catch (error: any) {
console.error('Failed to fetch heritage detail:', error)
message.error(error.message || '获取非遗详情失败')
} finally {
setLoading(false)
}
@ -89,7 +95,7 @@ const HeritageDetail: React.FC = () => {
<Tag color="red">{levelLabels[data.level]}</Tag>
<span><EnvironmentOutlined /> {data.province}</span>
<span><EyeOutlined /> {data.viewCount.toLocaleString()}</span>
<span><HeartOutlined /> {data.likeCount.toLocaleString()}</span>
<span><LikeOutlined /> {data.likeCount.toLocaleString()}</span>
</div>
</div>
@ -127,13 +133,27 @@ const HeritageDetail: React.FC = () => {
</Tabs>
{/* 评论区 */}
<CommentSection targetType="heritage" targetId={data.id} showRating />
<CommentSection targetType="heritage" targetId={data.id} />
</Col>
<Col xs={24} lg={8}>
<div className="detail-sidebar">
<div className="sidebar-actions">
<FavoriteButton heritageId={data.id} size="large" />
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<FavoriteButton
targetType="heritage"
targetId={data.id}
size="large"
showText
/>
<LikeButton
targetType="heritage"
targetId={data.id}
size="large"
showText
initialCount={data.likeCount}
/>
</Space>
</div>
<div className="sidebar-info">

View File

@ -3,11 +3,12 @@
*/
import React, { useEffect, useState } from 'react'
import { Row, Col, Spin, Empty, Input, Select, Space, Button } from 'antd'
import { Row, Col, Spin, Empty, Input, Select, Space, Button, message } from 'antd'
import { SearchOutlined, FilterOutlined, ReloadOutlined } from '@ant-design/icons'
import HeritageCard from '@components/HeritageCard'
import CustomPagination from '@components/CustomPagination'
import { getHeritageList } from '@services/api'
import { getHeritageList as getHeritageListApi } from '@services/heritageApi'
import { transformHeritageListItem } from '@utils/heritageTransform'
import type { HeritageItem, PaginationResult, HeritageCategory } from '@types/index'
import './List.css'
@ -26,23 +27,14 @@ const categoryOptions = [
{ label: '传统美术', value: 'traditional-art' },
]
// 常用标签选项
const tagOptions = [
'传统技艺',
'传统美术',
'传统音乐',
'传统戏剧',
'民俗',
'金属工艺',
'刺绣',
'陶瓷',
'古琴',
'书法',
'造纸',
'宫廷艺术',
'江南文化',
'文人艺术',
'文房四宝',
// 级别选项
const levelOptions = [
{ label: '全部级别', value: '' },
{ label: '世界级', value: 'world' },
{ label: '国家级', value: 'national' },
{ label: '省级', value: 'provincial' },
{ label: '市级', value: 'municipal' },
{ label: '县级', value: 'county' },
]
const HeritageList: React.FC = () => {
@ -54,23 +46,42 @@ const HeritageList: React.FC = () => {
// 筛选条件状态
const [searchKeyword, setSearchKeyword] = useState('')
const [selectedCategory, setSelectedCategory] = useState<string>('')
const [selectedTags, setSelectedTags] = useState<string[]>([])
const [selectedLevel, setSelectedLevel] = useState<string>('')
// 实际应用的筛选条件(用于提交后生效)
const [appliedFilters, setAppliedFilters] = useState({
keyword: '',
category: '',
tags: [] as string[],
level: '',
})
const fetchData = async () => {
setLoading(true)
try {
// 获取所有数据(设置一个很大的 pageSize
const result = await getHeritageList({ page: 1, pageSize: 1000 })
setData(result)
} catch (error) {
// 调用后端API使用服务器端分页和筛选
const result = await getHeritageListApi({
pageNum: currentPage,
pageSize: pageSize,
keyword: appliedFilters.keyword || undefined,
category: appliedFilters.category || undefined,
level: appliedFilters.level || undefined,
sortField: 'create_time',
sortOrder: 'desc',
})
// 转换后端数据为前端格式
const transformedData: PaginationResult<HeritageItem> = {
data: result.records.map(transformHeritageListItem),
total: result.total,
page: result.current,
pageSize: result.size,
totalPages: result.pages,
}
setData(transformedData)
} catch (error: any) {
console.error('Failed to fetch heritage list:', error)
message.error(error.message || '获取非遗列表失败')
} finally {
setLoading(false)
}
@ -78,7 +89,7 @@ const HeritageList: React.FC = () => {
useEffect(() => {
fetchData()
}, [])
}, [currentPage, pageSize, appliedFilters])
const handlePageChange = (page: number, size: number) => {
// 只改变页码,不改变 pageSize
@ -103,7 +114,7 @@ const HeritageList: React.FC = () => {
setAppliedFilters({
keyword: searchKeyword,
category: selectedCategory,
tags: selectedTags,
level: selectedLevel,
})
setCurrentPage(1)
window.scrollTo({ top: 0, behavior: 'smooth' })
@ -113,62 +124,16 @@ const HeritageList: React.FC = () => {
const handleResetFilters = () => {
setSearchKeyword('')
setSelectedCategory('')
setSelectedTags([])
setSelectedLevel('')
setAppliedFilters({
keyword: '',
category: '',
tags: [],
level: '',
})
setCurrentPage(1)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
// 客户端筛选和分页数据
const getFilteredData = () => {
if (!data) return null
let filteredItems = [...data.data]
// 按名字搜索
if (appliedFilters.keyword) {
const keyword = appliedFilters.keyword.toLowerCase()
filteredItems = filteredItems.filter(
(item) =>
item.name.toLowerCase().includes(keyword) ||
item.description.toLowerCase().includes(keyword)
)
}
// 按分类筛选
if (appliedFilters.category) {
filteredItems = filteredItems.filter((item) => item.category === appliedFilters.category)
}
// 按标签筛选
if (appliedFilters.tags.length > 0) {
filteredItems = filteredItems.filter((item) =>
appliedFilters.tags.some((tag) => item.tags.includes(tag))
)
}
// 客户端分页
const total = filteredItems.length
const totalPages = Math.ceil(total / pageSize)
const start = (currentPage - 1) * pageSize
const end = start + pageSize
const paginatedItems = filteredItems.slice(start, end)
return {
data: paginatedItems,
total: total,
page: currentPage,
pageSize: pageSize,
totalPages: totalPages,
}
}
const filteredData = getFilteredData()
return (
<div className="heritage-list-page">
<div className="page-header">
@ -205,15 +170,13 @@ const HeritageList: React.FC = () => {
allowClear
/>
{/* 标签选择 */}
{/* 级别选择 */}
<Select
mode="multiple"
placeholder="选择标签"
value={selectedTags}
onChange={setSelectedTags}
style={{ minWidth: 200, maxWidth: 400 }}
options={tagOptions.map((tag) => ({ label: tag, value: tag }))}
maxTagCount="responsive"
placeholder="选择级别"
value={selectedLevel || undefined}
onChange={(value) => setSelectedLevel(value || '')}
style={{ width: 150 }}
options={levelOptions}
allowClear
/>
@ -227,7 +190,7 @@ const HeritageList: React.FC = () => {
</Space>
{/* 筛选结果提示 */}
{(appliedFilters.keyword || appliedFilters.category || appliedFilters.tags.length > 0) && (
{(appliedFilters.keyword || appliedFilters.category || appliedFilters.level) && (
<div className="filter-result-info">
<Space wrap size="small">
<span className="filter-label"></span>
@ -241,11 +204,13 @@ const HeritageList: React.FC = () => {
?.label}
</span>
)}
{appliedFilters.tags.map((tag) => (
<span key={tag} className="filter-tag">
: {tag}
{appliedFilters.level && (
<span className="filter-tag">
:{' '}
{levelOptions.find((opt) => opt.value === appliedFilters.level)
?.label}
</span>
))}
)}
</Space>
</div>
)}
@ -257,10 +222,10 @@ const HeritageList: React.FC = () => {
<div className="loading-container">
<Spin size="large" />
</div>
) : filteredData && filteredData.data.length > 0 ? (
) : data && data.data.length > 0 ? (
<>
<Row gutter={[24, 24]}>
{filteredData.data.map((item) => (
{data.data.map((item) => (
<Col key={item.id} xs={24} sm={12} md={8} lg={6}>
<HeritageCard item={item} />
</Col>
@ -268,7 +233,7 @@ const HeritageList: React.FC = () => {
</Row>
<CustomPagination
current={currentPage}
total={filteredData.total}
total={data.total}
pageSize={pageSize}
onChange={handlePageChange}
onShowSizeChange={handleShowSizeChange}

View File

@ -4,7 +4,7 @@
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { Row, Col, Tag, Breadcrumb, Image, Tabs, Spin, Empty, Avatar, Card, Typography } from 'antd'
import { Row, Col, Tag, Breadcrumb, Image, Tabs, Spin, Empty, Avatar, Card, Typography, message, Space } from 'antd'
import {
HomeOutlined,
EnvironmentOutlined,
@ -19,7 +19,10 @@ import {
} from '@ant-design/icons'
import { Link } from 'react-router-dom'
import type { Inheritor } from '@types/index'
import { getInheritorById } from '@services/api'
import { getInheritorDetail } from '@services/inheritorApi'
import { transformInheritorDetail } from '@utils/inheritorTransform'
import FavoriteButton from '@components/FavoriteButton'
import LikeButton from '@components/LikeButton'
import './Detail.css'
const { Title, Paragraph, Text } = Typography
@ -44,10 +47,15 @@ const InheritorDetail: React.FC = () => {
const fetchData = async (inheritorId: string) => {
setLoading(true)
try {
const result = await getInheritorById(inheritorId)
setData(result)
} catch (error) {
// 调用真实后端API获取传承人详情
const result = await getInheritorDetail(inheritorId)
// 转换后端数据为前端格式
const transformedData = transformInheritorDetail(result)
setData(transformedData)
} catch (error: any) {
console.error('Failed to fetch inheritor detail:', error)
message.error(error.message || '获取传承人详情失败')
setData(null)
} finally {
setLoading(false)
}
@ -257,6 +265,25 @@ const InheritorDetail: React.FC = () => {
{/* 侧边栏 */}
<Col xs={24} lg={8}>
<div className="detail-sidebar">
{/* 操作按钮 */}
<Card className="sidebar-card" style={{ marginBottom: 24 }}>
<Space direction="vertical" style={{ width: '100%' }} size="middle">
<FavoriteButton
targetType="inheritor"
targetId={data.id}
size="large"
showText
/>
<LikeButton
targetType="inheritor"
targetId={data.id}
size="large"
showText
initialCount={data.followers}
/>
</Space>
</Card>
{/* 基本信息 */}
<Card title="基本信息" className="sidebar-card">
<div className="info-item">

View File

@ -3,38 +3,86 @@
*/
import React, { useEffect, useState } from 'react'
import { Row, Col, Spin, Empty } from 'antd'
import { Row, Col, Spin, Empty, Input, Select, Space, Button, message } from 'antd'
import { SearchOutlined, FilterOutlined, ReloadOutlined } from '@ant-design/icons'
import InheritorCard from '@components/InheritorCard'
import CustomPagination from '@components/CustomPagination'
import { getInheritorList } from '@services/api'
import { getInheritorList as getInheritorListApi } from '@services/inheritorApi'
import { transformInheritorListItem } from '@utils/inheritorTransform'
import type { Inheritor, PaginationResult } from '@types/index'
import './List.css'
// 级别选项
const levelOptions = [
{ label: '全部级别', value: '' },
{ label: '国家级', value: 'national' },
{ label: '省级', value: 'provincial' },
{ label: '市级', value: 'municipal' },
]
const InheritorsList: React.FC = () => {
const [data, setData] = useState<PaginationResult<Inheritor> | null>(null)
const [loading, setLoading] = useState(true)
const [currentPage, setCurrentPage] = useState(1)
const [pageSize, setPageSize] = useState(12)
const fetchData = async (page: number, size: number) => {
// 筛选条件状态
const [searchKeyword, setSearchKeyword] = useState('')
const [selectedLevel, setSelectedLevel] = useState<string>('')
const [selectedProvince, setSelectedProvince] = useState<string>('')
// 实际应用的筛选条件(用于提交后生效)
const [appliedFilters, setAppliedFilters] = useState({
keyword: '',
level: '',
province: '',
})
const fetchData = async () => {
setLoading(true)
try {
const result = await getInheritorList({ page, pageSize: size })
setData(result)
} catch (error) {
// 调用后端API使用服务器端分页和筛选
const result = await getInheritorListApi({
pageNum: currentPage,
pageSize: pageSize,
keyword: appliedFilters.keyword || undefined,
level: appliedFilters.level || undefined,
province: appliedFilters.province || undefined,
sortField: 'create_time',
sortOrder: 'desc',
})
// 转换后端数据为前端格式
const transformedData: PaginationResult<Inheritor> = {
data: result.records.map(transformInheritorListItem),
total: result.total,
page: result.current,
pageSize: result.size,
totalPages: result.pages,
}
setData(transformedData)
} catch (error: any) {
console.error('Failed to fetch inheritors list:', error)
message.error(error.message || '获取传承人列表失败')
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchData(currentPage, pageSize)
}, [currentPage, pageSize])
fetchData()
}, [currentPage, pageSize, appliedFilters])
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page)
setPageSize(size)
// 只改变页码,不改变 pageSize
if (size === pageSize) {
setCurrentPage(page)
} else {
// pageSize 改变时,重置到第一页
setCurrentPage(1)
setPageSize(size)
}
window.scrollTo({ top: 0, behavior: 'smooth' })
}
@ -44,6 +92,31 @@ const InheritorsList: React.FC = () => {
window.scrollTo({ top: 0, behavior: 'smooth' })
}
// 应用筛选条件
const handleApplyFilters = () => {
setAppliedFilters({
keyword: searchKeyword,
level: selectedLevel,
province: selectedProvince,
})
setCurrentPage(1)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
// 重置筛选条件
const handleResetFilters = () => {
setSearchKeyword('')
setSelectedLevel('')
setSelectedProvince('')
setAppliedFilters({
keyword: '',
level: '',
province: '',
})
setCurrentPage(1)
window.scrollTo({ top: 0, behavior: 'smooth' })
}
return (
<div className="inheritors-list-page">
<div className="page-header">
@ -54,7 +127,75 @@ const InheritorsList: React.FC = () => {
</div>
<div className="page-content section-spacing">
<div className="container">
<div className="container-wide">
{/* 筛选器区域 */}
<div className="filter-section">
<Space direction="vertical" size="large" style={{ width: '100%' }}>
<Space wrap size="middle" className="filter-controls">
{/* 搜索框 */}
<Input
placeholder="搜索传承人姓名、简介或传承故事"
prefix={<SearchOutlined />}
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleApplyFilters}
style={{ width: 300 }}
allowClear
/>
{/* 级别选择 */}
<Select
placeholder="选择级别"
value={selectedLevel || undefined}
onChange={(value) => setSelectedLevel(value || '')}
style={{ width: 150 }}
options={levelOptions}
allowClear
/>
{/* 省份输入 */}
<Input
placeholder="省份(如:江苏省)"
value={selectedProvince}
onChange={(e) => setSelectedProvince(e.target.value)}
style={{ width: 180 }}
allowClear
/>
{/* 操作按钮 */}
<Button type="primary" icon={<FilterOutlined />} onClick={handleApplyFilters}>
</Button>
<Button icon={<ReloadOutlined />} onClick={handleResetFilters}>
</Button>
</Space>
{/* 筛选结果提示 */}
{(appliedFilters.keyword || appliedFilters.level || appliedFilters.province) && (
<div className="filter-result-info">
<Space wrap size="small">
<span className="filter-label"></span>
{appliedFilters.keyword && (
<span className="filter-tag">: {appliedFilters.keyword}</span>
)}
{appliedFilters.level && (
<span className="filter-tag">
:{' '}
{levelOptions.find((opt) => opt.value === appliedFilters.level)
?.label}
</span>
)}
{appliedFilters.province && (
<span className="filter-tag">: {appliedFilters.province}</span>
)}
</Space>
</div>
)}
</Space>
</div>
{/* 内容区域 */}
{loading ? (
<div className="loading-container">
<Spin size="large" />
@ -69,9 +210,9 @@ const InheritorsList: React.FC = () => {
))}
</Row>
<CustomPagination
current={data.page}
current={currentPage}
total={data.total}
pageSize={data.pageSize}
pageSize={pageSize}
onChange={handlePageChange}
onShowSizeChange={handleShowSizeChange}
unit="位传承人"