diff --git a/src/pages/News/EventDetail.tsx b/src/pages/News/EventDetail.tsx index 52a743b..b795b1a 100644 --- a/src/pages/News/EventDetail.tsx +++ b/src/pages/News/EventDetail.tsx @@ -16,6 +16,10 @@ import { Card, Row, Col, + message, + Modal, + Input, + Form, } from 'antd' import { HomeOutlined, @@ -23,34 +27,16 @@ import { CalendarOutlined, ClockCircleOutlined, EnvironmentOutlined, - UserOutlined, - DollarOutlined, TeamOutlined, - PhoneOutlined, - MailOutlined, } from '@ant-design/icons' -import { getEventById } from '@services/api' -import type { Event } from '@/types' +import dayjs from 'dayjs' +import { getEventDetail, registerEvent, cancelEventRegistration } from '@services/eventApi' +import type { EventDetail, EventRegistrationParams } from '@/types' +import { requireAuth } from '@/utils/authGuard' import './EventDetail.css' const { Title, Paragraph, Text } = Typography -const typeLabels: Record = { - exhibition: '展览', - workshop: '工作坊', - performance: '演出', - lecture: '讲座', - festival: '节日', -} - -const typeColors: Record = { - exhibition: 'purple', - workshop: 'blue', - performance: 'red', - lecture: 'green', - festival: 'orange', -} - const statusLabels: Record = { upcoming: '即将开始', ongoing: '进行中', @@ -65,10 +51,13 @@ const statusColors: Record = { cancelled: 'red', } -const EventDetail: React.FC = () => { +const EventDetailPage: React.FC = () => { const { id } = useParams<{ id: string }>() - const [data, setData] = useState(null) + const [data, setData] = useState(null) const [loading, setLoading] = useState(true) + const [registering, setRegistering] = useState(false) + const [showRegisterModal, setShowRegisterModal] = useState(false) + const [form] = Form.useForm() useEffect(() => { if (id) { @@ -79,7 +68,7 @@ const EventDetail: React.FC = () => { const fetchData = async (eventId: string) => { setLoading(true) try { - const result = await getEventById(eventId) + const result = await getEventDetail(eventId) setData(result) } catch (error) { console.error('Failed to fetch event detail:', error) @@ -88,9 +77,66 @@ const EventDetail: React.FC = () => { } } - const handleEnroll = () => { - // TODO: 实现报名逻辑 - console.log('报名活动:', id) + // 报名活动 + const handleRegister = () => { + // 🔥 认证守卫:未登录时提示并跳转到登录页 + if (!requireAuth('活动报名')) { + return + } + setShowRegisterModal(true) + } + + // 提交报名 + const handleSubmitRegister = async () => { + try { + const values = await form.validateFields() + setRegistering(true) + + const params: EventRegistrationParams = { + eventId: Number(id), + phone: values.phone, + remark: values.remark, + } + + await registerEvent(params) + message.success('报名成功!') + setShowRegisterModal(false) + form.resetFields() + + // 刷新数据 + if (id) { + fetchData(id) + } + } catch (error: any) { + if (error.errorFields) { + // 表单验证错误 + return + } + message.error(error.message || '报名失败') + } finally { + setRegistering(false) + } + } + + // 取消报名 + const handleCancelRegistration = async () => { + if (!id) return + + Modal.confirm({ + title: '确认取消报名', + content: '确定要取消报名吗?', + onOk: async () => { + try { + await cancelEventRegistration(Number(id)) + message.success('已取消报名') + + // 刷新数据 + fetchData(id) + } catch (error: any) { + message.error(error.message || '取消报名失败') + } + }, + }) } if (loading) { @@ -109,11 +155,70 @@ const EventDetail: React.FC = () => { ) } - const isEnrollable = - data.status === 'upcoming' && - (!data.capacity || data.enrolled < data.capacity) + // 格式化时间 + const startDate = dayjs(data.startTime) + const endDate = dayjs(data.endTime) + const registrationStartDate = dayjs(data.registrationStart) + const registrationEndDate = dayjs(data.registrationEnd) + const isSameDay = startDate.format('YYYY-MM-DD') === endDate.format('YYYY-MM-DD') - const isFull = data.capacity && data.enrolled >= data.capacity + // 判断是否已满 + const isFull = data.currentParticipants >= data.maxParticipants + + // 渲染报名按钮 + const renderActionButton = () => { + if (data.isRegistered) { + return ( + + ) + } + + if (!data.canRegister) { + return ( + + ) + } + + return ( + + ) + } return (
@@ -135,8 +240,8 @@ const EventDetail: React.FC = () => {
{/* 封面图 */} - {data.cover && ( -
+ {data.coverImage && ( +
@@ -155,9 +260,6 @@ const EventDetail: React.FC = () => {
{/* 头部信息 */}
- - {typeLabels[data.type]} - {data.title} @@ -167,18 +269,16 @@ const EventDetail: React.FC = () => { - {data.startDate} - {data.startDate !== data.endDate && ` ~ ${data.endDate}`} + {startDate.format('YYYY-MM-DD')} + {!isSameDay && ` ~ ${endDate.format('YYYY-MM-DD')}`} + + + + + + {startDate.format('HH:mm')} - {endDate.format('HH:mm')} - {data.startTime && ( - - - - {data.startTime} - {data.endTime} - - - )} {data.location} @@ -193,23 +293,19 @@ const EventDetail: React.FC = () => { - {/* 活动描述 */} -
- 活动介绍 - {data.description} + {/* 摘要 */} +
+ {data.summary}
- {/* 标签 */} - {data.tags && data.tags.length > 0 && ( -
- - 标签: - {data.tags.map((tag) => ( - {tag} - ))} - -
- )} + {/* 活动介绍 - 使用 dangerouslySetInnerHTML 渲染 HTML */} +
+ 活动详情 +
+
@@ -218,124 +314,117 @@ const EventDetail: React.FC = () => {
{/* 报名信息卡片 */} -
- -
- - 活动费用 - -
- {data.isFree ? ( - - 免费 - - ) : ( - - ¥{data.price} - - )} -
-
-
- - -
-
- - 报名人数 - - {data.enrolled} / {data.capacity || '不限'} +
+ + + 报名人数 + + + {data.currentParticipants} / {data.maxParticipants}
- {data.capacity && ( -
-
-
- )} +
+
+
{isFull && ( - + 名额已满 )} {data.status === 'finished' && ( - + 活动已结束 )} {data.status === 'cancelled' && ( - + 活动已取消 )} - + {renderActionButton()}
- - {/* 组织方信息 */} - {data.organizer && ( - - 主办方 - - -
- - - {data.organizer} - -
- {data.contactInfo && (data.contactInfo.phone || data.contactInfo.email) && ( - <> - {data.contactInfo.phone && ( -
- - {data.contactInfo.phone} -
- )} - {data.contactInfo.email && ( -
- - {data.contactInfo.email} -
- )} - - )} -
-
- )} + + + {/* 报名时间 */} +
+ 报名时间 + + {registrationStartDate.format('YYYY-MM-DD HH:mm')} +
+ 至 +
+ {registrationEndDate.format('YYYY-MM-DD HH:mm')} +
+
+
+ + {/* 报名弹窗 */} + { + setShowRegisterModal(false) + form.resetFields() + }} + confirmLoading={registering} + okText="提交报名" + cancelText="取消" + width={480} + > +
+ + + + + + + +
+
) } -export default EventDetail +export default EventDetailPage