feat: 为活动报名添加认证守卫
- 在活动报名功能中集成认证守卫 - 替换原有的手动跳转逻辑为统一的认证守卫 - 未登录用户点击报名时显示提示并跳转到登录页 - 移除未使用的imports和dependencies
This commit is contained in:
parent
153d8d40ad
commit
6c6fdab21c
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user