feat: 为活动报名添加认证守卫

- 在活动报名功能中集成认证守卫
- 替换原有的手动跳转逻辑为统一的认证守卫
- 未登录用户点击报名时显示提示并跳转到登录页
- 移除未使用的imports和dependencies
This commit is contained in:
Leo 2025-10-13 21:38:11 +08:00
parent 153d8d40ad
commit 6c6fdab21c

View File

@ -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<string, string> = {
exhibition: '展览',
workshop: '工作坊',
performance: '演出',
lecture: '讲座',
festival: '节日',
}
const typeColors: Record<string, string> = {
exhibition: 'purple',
workshop: 'blue',
performance: 'red',
lecture: 'green',
festival: 'orange',
}
const statusLabels: Record<string, string> = {
upcoming: '即将开始',
ongoing: '进行中',
@ -65,10 +51,13 @@ const statusColors: Record<string, string> = {
cancelled: 'red',
}
const EventDetail: React.FC = () => {
const EventDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>()
const [data, setData] = useState<Event | null>(null)
const [data, setData] = useState<EventDetail | null>(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 (
<Button
type="default"
size="large"
block
onClick={handleCancelRegistration}
style={{
height: 48,
fontSize: 16,
fontWeight: 600,
}}
>
</Button>
)
}
if (!data.canRegister) {
return (
<Button
type="primary"
size="large"
block
disabled
style={{
height: 48,
fontSize: 16,
fontWeight: 600,
}}
>
{isFull ? '名额已满' : data.status === 'finished' ? '活动已结束' : '无法报名'}
</Button>
)
}
return (
<Button
type="primary"
size="large"
block
onClick={handleRegister}
style={{
height: 48,
fontSize: 16,
fontWeight: 600,
}}
>
</Button>
)
}
return (
<div className="event-detail-page">
@ -135,8 +240,8 @@ const EventDetail: React.FC = () => {
</div>
{/* 封面图 */}
{data.cover && (
<div className="event-detail-cover" style={{ backgroundImage: `url(${data.cover})` }}>
{data.coverImage && (
<div className="event-detail-cover" style={{ backgroundImage: `url(${data.coverImage})` }}>
<div className="cover-overlay"></div>
<div className="cover-badge">
<Tag color={statusColors[data.status]} className="status-badge">
@ -155,9 +260,6 @@ const EventDetail: React.FC = () => {
<div className="event-detail-main">
{/* 头部信息 */}
<div className="event-detail-header">
<Tag color={typeColors[data.type]} className="type-tag">
{typeLabels[data.type]}
</Tag>
<Title level={1} className="event-detail-title">
{data.title}
</Title>
@ -167,18 +269,16 @@ const EventDetail: React.FC = () => {
<span className="meta-item">
<CalendarOutlined />
<Text>
{data.startDate}
{data.startDate !== data.endDate && ` ~ ${data.endDate}`}
{startDate.format('YYYY-MM-DD')}
{!isSameDay && ` ~ ${endDate.format('YYYY-MM-DD')}`}
</Text>
</span>
<span className="meta-item">
<ClockCircleOutlined />
<Text>
{startDate.format('HH:mm')} - {endDate.format('HH:mm')}
</Text>
</span>
{data.startTime && (
<span className="meta-item">
<ClockCircleOutlined />
<Text>
{data.startTime} - {data.endTime}
</Text>
</span>
)}
<span className="meta-item">
<EnvironmentOutlined />
<Text>{data.location}</Text>
@ -193,23 +293,19 @@ const EventDetail: React.FC = () => {
<Divider />
{/* 活动描述 */}
<div className="event-detail-description">
<Title level={3}></Title>
<Paragraph className="description-text">{data.description}</Paragraph>
{/* 摘要 */}
<div className="event-detail-summary">
<Paragraph className="summary-text">{data.summary}</Paragraph>
</div>
{/* 标签 */}
{data.tags && data.tags.length > 0 && (
<div className="event-detail-tags">
<Space size="small" wrap>
<Text type="secondary"></Text>
{data.tags.map((tag) => (
<Tag key={tag}>{tag}</Tag>
))}
</Space>
</div>
)}
{/* 活动介绍 - 使用 dangerouslySetInnerHTML 渲染 HTML */}
<div className="event-detail-description">
<Title level={3}></Title>
<div
className="description-text"
dangerouslySetInnerHTML={{ __html: data.content }}
/>
</div>
</div>
</Col>
@ -218,124 +314,117 @@ const EventDetail: React.FC = () => {
<div className="event-sidebar">
{/* 报名信息卡片 */}
<Card className="enrollment-card" bordered={false}>
<div className="price-info">
<DollarOutlined className="price-icon" />
<div>
<Text type="secondary" style={{ fontSize: 14 }}>
</Text>
<div className="price-value">
{data.isFree ? (
<Text strong style={{ fontSize: 28, color: '#52c41a' }}>
</Text>
) : (
<Text strong style={{ fontSize: 28, color: '#c8363d' }}>
¥{data.price}
</Text>
)}
</div>
</div>
</div>
<Divider />
<div className="enrollment-info">
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div className="info-row">
<TeamOutlined />
<Text></Text>
<Text strong>
{data.enrolled} / {data.capacity || '不限'}
<div className="info-row" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<TeamOutlined style={{ fontSize: 18, color: '#c8363d' }} />
<Text></Text>
</Space>
<Text strong style={{ fontSize: 16 }}>
{data.currentParticipants} / {data.maxParticipants}
</Text>
</div>
{data.capacity && (
<div className="capacity-progress">
<div
className="progress-bar"
style={{
width: `${(data.enrolled / data.capacity) * 100}%`,
backgroundColor: isFull ? '#ff4d4f' : '#52c41a',
}}
></div>
</div>
)}
<div className="capacity-progress" style={{ width: '100%', height: 8, backgroundColor: '#f0f0f0', borderRadius: 4, overflow: 'hidden' }}>
<div
className="progress-bar"
style={{
width: `${(data.currentParticipants / data.maxParticipants) * 100}%`,
height: '100%',
backgroundColor: isFull ? '#ff4d4f' : '#52c41a',
transition: 'width 0.3s ease',
}}
></div>
</div>
{isFull && (
<Tag color="red" style={{ width: '100%', textAlign: 'center' }}>
<Tag color="red" style={{ width: '100%', textAlign: 'center', marginBottom: 0 }}>
</Tag>
)}
{data.status === 'finished' && (
<Tag color="default" style={{ width: '100%', textAlign: 'center' }}>
<Tag color="default" style={{ width: '100%', textAlign: 'center', marginBottom: 0 }}>
</Tag>
)}
{data.status === 'cancelled' && (
<Tag color="red" style={{ width: '100%', textAlign: 'center' }}>
<Tag color="red" style={{ width: '100%', textAlign: 'center', marginBottom: 0 }}>
</Tag>
)}
<Button
type="primary"
size="large"
block
disabled={!isEnrollable}
onClick={handleEnroll}
style={{
height: 48,
fontSize: 16,
fontWeight: 600,
}}
>
{isEnrollable ? '立即报名' : '无法报名'}
</Button>
{renderActionButton()}
</Space>
</div>
</Card>
{/* 组织方信息 */}
{data.organizer && (
<Card className="organizer-card" bordered={false}>
<Title level={4}></Title>
<Divider style={{ margin: '12px 0 16px' }} />
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<div className="organizer-info">
<UserOutlined style={{ fontSize: 20, color: '#c8363d' }} />
<Text strong style={{ fontSize: 16 }}>
{data.organizer}
</Text>
</div>
{data.contactInfo && (data.contactInfo.phone || data.contactInfo.email) && (
<>
{data.contactInfo.phone && (
<div className="contact-item">
<PhoneOutlined />
<Text type="secondary">{data.contactInfo.phone}</Text>
</div>
)}
{data.contactInfo.email && (
<div className="contact-item">
<MailOutlined />
<Text type="secondary">{data.contactInfo.email}</Text>
</div>
)}
</>
)}
</Space>
</Card>
)}
<Divider />
{/* 报名时间 */}
<div className="registration-time">
<Title level={4} style={{ fontSize: 14, marginBottom: 8 }}></Title>
<Text type="secondary" style={{ fontSize: 13 }}>
{registrationStartDate.format('YYYY-MM-DD HH:mm')}
<br />
<br />
{registrationEndDate.format('YYYY-MM-DD HH:mm')}
</Text>
</div>
</Card>
</div>
</Col>
</Row>
</div>
</div>
{/* 报名弹窗 */}
<Modal
title="活动报名"
open={showRegisterModal}
onOk={handleSubmitRegister}
onCancel={() => {
setShowRegisterModal(false)
form.resetFields()
}}
confirmLoading={registering}
okText="提交报名"
cancelText="取消"
width={480}
>
<Form
form={form}
layout="vertical"
style={{ marginTop: 24 }}
>
<Form.Item
label="联系电话"
name="phone"
rules={[
{ required: true, message: '请输入联系电话' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码' }
]}
>
<Input placeholder="请输入您的联系电话" size="large" />
</Form.Item>
<Form.Item
label="备注说明"
name="remark"
>
<Input.TextArea
placeholder="请输入备注说明(选填)"
rows={4}
maxLength={200}
showCount
/>
</Form.Item>
</Form>
</Modal>
</div>
)
}
export default EventDetail
export default EventDetailPage