hookehuyr

refactor: 优化页面组件和路由配置

重构了用户个人资料页、活动详情页、签到页和消息页的代码,移除了未使用的导入和调试日志。调整了路由配置,简化了消息页的UI,并优化了签到逻辑。这些改动旨在提高代码的可维护性和用户体验。
<!--
* @Date: 2025-04-17 13:16:20
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-04-21 14:56:50
* @LastEditTime: 2025-04-21 15:29:42
* @FilePath: /mlaj-reading-club/src/components/shared/ActivityCard.vue
* @Description: 文件描述
-->
......@@ -61,17 +61,29 @@
</div>
</div>
<router-link :to="`/activity/${activity.id}`"
<router-link v-if="!isRegistrationOpen" :to="`/activity/${activity.id}`"
class="mt-4 inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-500 to-blue-500 hover:from-green-600 hover:to-blue-600">
{{ isPast ? '查看详情' : isRegistrationOpen ? '立即报名' : '了解更多' }}
{{ isPast ? '查看详情' : '了解更多' }}
</router-link>
<div v-else @click="handleRegistration"
class="mt-4 inline-flex items-center justify-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-gradient-to-r from-green-500 to-blue-500 hover:from-green-600 hover:to-blue-600">
立即报名
</div>
</div>
</div>
</div>
<!-- Register Modal -->
<Modal :isOpen="showRegisterModal" title="活动报名" @close="showRegisterModal = false">
<form @submit.prevent="submitRegistration">
<!-- Registration form fields -->
</form>
</Modal>
</template>
<script setup>
import { ref, computed, defineProps } from 'vue'
import Modal from './Modal.vue'
const props = defineProps({
activity: {
......@@ -136,4 +148,15 @@ const statusText = computed(() => {
}
return '';
});
const showRegisterModal = ref(false)
const handleRegistration = () => {
showRegisterModal.value = true
}
const submitRegistration = async () => {
// TODO: Implement registration submission
showRegisterModal.value = false
}
</script>
......
/*
* @Date: 2025-04-17 14:26:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-04-21 13:26:32
* @LastEditTime: 2025-04-21 15:07:36
* @FilePath: /mlaj-reading-club/src/main.js
* @Description: 文件描述
*/
......@@ -27,8 +27,8 @@ const router = createRouter({
{ path: '/create-activity', component: () => import('./pages/CreateActivity.vue') },
{ path: '/profile', component: () => import('./pages/UserProfile.vue') },
{ path: '/registration/:activityId', component: () => import('./pages/Registration.vue') },
{ path: '/check-in/:activityId', component: () => import('./pages/CheckIn.vue') },
{ path: '/messages/:activityId', component: () => import('./pages/Messages.vue') },
{ path: '/check-in/:id', component: () => import('./pages/CheckIn.vue') },
{ path: '/messages', component: () => import('./pages/Messages.vue') },
{ path: '/:pathMatch(.*)*', redirect: '/' }
]
})
......
......@@ -231,7 +231,7 @@
</div>
<div class="mt-2 flex items-center">
<span class="text-sm text-green-700 mr-2">报名状态:</span>
<span :class="registrationStatusBadge.class">{{ registrationStatusBadge.text
<span :class="registrationStatusBadge?.class">{{ registrationStatusBadge?.text
}}</span>
</div>
</div>
......@@ -271,13 +271,10 @@
</template>
<!-- Register Modal -->
<Modal v-if="showRegisterModal" @close="showRegisterModal = false">
<template #title>活动报名</template>
<template #content>
<Modal :isOpen="showRegisterModal" title="活动报名" @close="showRegisterModal = false">
<form @submit.prevent="submitRegistration">
<!-- Registration form fields -->
</form>
</template>
</Modal>
</div>
</template>
......
......@@ -261,6 +261,7 @@ import { useRoute, useRouter } from 'vue-router'
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
import usersData from '../data/users.json'
import checkinsData from '../data/checkins.json'
import Button from '../components/shared/Button.vue'
const route = useRoute()
......@@ -268,8 +269,9 @@ const router = useRouter()
const activities = ref(activitiesData.activities)
const registrations = ref(registrationsData.registrations)
const currentUser = ref(usersData.users[0])
const checkIns = ref(checkinsData)
const activityId = route.params.activityId
const activityId = route.params.id
const activity = ref(null)
const loading = ref(true)
const error = ref(null)
......@@ -303,8 +305,8 @@ const fetchData = async () => {
userRegistration.value = registration
// Check if user already checked in
const checkIn = appStore.checkIns.find(
check => check.activity_id === activityId && check.user_id === appStore.currentUser.id
const checkIn = checkIns.value.find(
check => check.activity_id === activityId && check.user_id === currentUser.id
)
if (checkIn) {
......@@ -341,8 +343,8 @@ const handleSubmitCode = async () => {
try {
if (inputCode.value === checkInCode.value) {
const now = new Date().toISOString().replace('T', ' ').substring(0, 19)
const checkInResult = await appStore.addCheckIn({
user_id: appStore.currentUser.id,
const checkInResult = await addCheckIn({
user_id: currentUser.id,
activity_id: activityId,
check_in_time: now,
status: 'checked_in'
......@@ -350,7 +352,7 @@ const handleSubmitCode = async () => {
if (checkInResult.success) {
userCheckIn.value = {
user_id: appStore.currentUser.id,
user_id: currentUser.id,
activity_id: activityId,
check_in_time: now,
status: 'checked_in'
......@@ -418,4 +420,33 @@ watch(countdown, (newValue) => {
// Initial data fetch
onMounted(fetchData)
// 添加签到记录
const addCheckIn = async (checkInData) => {
try {
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1000))
// 创建新的签到记录
const newCheckIn = {
id: `C${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`,
activity_id: checkInData.activity_id,
user_id: checkInData.user_id,
checkin_time: checkInData.check_in_time,
checkin_type: 'code',
status: 'successful',
notes: '',
is_late: false
}
// 添加到签到记录中
checkIns.value.push(newCheckIn)
return { success: true }
} catch (err) {
console.error('Failed to add check-in:', err)
return { success: false }
}
}
</script>
......
<template>
<div class="min-h-screen bg-gray-50">
<!-- Header -->
<div class="bg-white shadow-sm sticky top-0 z-10">
<div class="container mx-auto px-4 py-3">
<div class="flex items-center justify-between">
<div class="flex items-center">
<button @click="goBack" class="mr-3 text-gray-600 hover:text-gray-900">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20"
fill="currentColor">
<path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg>
</button>
<div>
<h1 class="text-lg font-medium text-gray-900 truncate max-w-xs">{{ activity?.title }}</h1>
<p class="text-sm text-gray-500">消息交流</p>
</div>
</div>
<router-link :to="`/activity/${activityId}`"
class="text-sm text-green-600 hover:text-green-700 font-medium">
活动详情
</router-link>
</div>
<div class="container mx-auto px-4 py-8">
<h1 class="text-2xl font-bold mb-6">系统消息</h1>
<!-- 消息列表 -->
<div class="space-y-4">
<div v-for="message in messages" :key="message.id"
class="bg-white p-4 rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer"
:class="{'border-l-4 border-blue-500': !message.read_status}"
@click="openMessageDetail(message)">
<div class="flex justify-between items-start">
<div class="flex-1">
<p class="text-gray-900 font-medium">{{ message.content }}</p>
<p class="text-sm text-gray-500 mt-1">{{ message.send_time }}</p>
</div>
<div class="ml-4">
<span v-if="!message.read_status"
class="inline-block w-2 h-2 bg-blue-500 rounded-full"></span>
</div>
<!-- Message Tabs -->
<div class="bg-white shadow-sm">
<div class="container mx-auto px-4">
<div class="flex overflow-x-auto hide-scrollbar">
<button v-for="tab in tabs" :key="tab.value" :class="[
'py-3 px-4 text-sm font-medium border-b-2 whitespace-nowrap',
activeTab === tab.value
? 'border-green-500 text-green-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
]" @click="activeTab = tab.value">
{{ tab.label }}
</button>
</div>
</div>
</div>
<!-- Messages Container -->
<div class="container mx-auto px-4 py-4 flex flex-col h-[calc(100vh-13rem)]">
<!-- Message List -->
<div class="flex-grow overflow-y-auto mb-4 pr-1">
<div v-if="filteredMessages.length === 0" class="flex flex-col items-center justify-center h-full">
<div class="text-gray-400">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
<!-- 消息详情弹窗 -->
<div v-if="showModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4">
<div class="flex justify-between items-start mb-4">
<h2 class="text-xl font-semibold">消息详情</h2>
<button @click="closeModal" class="text-gray-500 hover:text-gray-700">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
<p class="text-gray-500 mt-2">暂无消息</p>
<button v-if="activeTab !== 'all'" @click="activeTab = 'all'"
class="mt-4 text-green-600 hover:text-green-700 font-medium text-sm">
查看全部消息
</button>
</div>
<div v-else>
<div v-for="date in sortedDates" :key="date">
<div class="flex items-center justify-center my-4">
<div class="border-t border-gray-200 flex-grow"></div>
<span class="mx-4 text-xs text-gray-500">{{ formatDate(date) }}</span>
<div class="border-t border-gray-200 flex-grow"></div>
</div>
<div v-for="message in groupedMessages[date]" :key="message.id">
<component :is="messageComponent(message)" :message="message" />
</div>
</div>
<div ref="messageEndRef" />
</div>
</div>
<!-- Message Input -->
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden">
<div v-if="isOrganizer" class="p-2 bg-gray-50 border-b border-gray-200">
<div class="flex items-center">
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
组织者
</span>
<span class="text-xs text-gray-500 ml-2">
您可以发送公告消息
</span>
<div class="mb-6">
<p class="text-gray-900 mb-2">{{ selectedMessage?.content }}</p>
<p class="text-sm text-gray-500">{{ selectedMessage?.send_time }}</p>
</div>
<!-- 回复框 -->
<div class="mt-4">
<textarea
v-model="replyContent"
rows="3"
class="w-full border rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="输入回复内容..."
></textarea>
<div class="flex justify-end mt-2">
<button
@click="sendReply"
class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors"
>
发送回复
</button>
</div>
</div>
<form @submit.prevent="handleSendMessage" class="flex p-2">
<input type="text" v-model="messageInput" placeholder="输入消息..."
class="flex-grow border-none focus:ring-0 focus:outline-none" :disabled="isSubmitting" />
<Button type="submit" variant="primary" size="sm" :disabled="!messageInput.trim() || isSubmitting">
发送
</Button>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
import Button from '../components/shared/Button.vue'
import { ref, onMounted } from 'vue'
import messagesData from '../data/messages.json'
// Route and store setup
const route = useRoute()
const router = useRouter()
const store = useAppStore()
const messages = ref([])
const showModal = ref(false)
const selectedMessage = ref(null)
const replyContent = ref('')
// Props and refs
const activityId = route.params.activityId
const messageEndRef = ref(null)
const messageInput = ref('')
const activeTab = ref('all')
const isSubmitting = ref(false)
const activity = ref(null)
const filteredMessages = ref([])
const loading = ref(true)
const error = ref(null)
// Computed properties
const isOrganizer = computed(() => {
return store.currentUser && store.currentUser.id === activity.value?.organizer_id
onMounted(() => {
messages.value = messagesData.messages
})
// Tabs configuration
const tabs = [
{ label: '全部消息', value: 'all' },
{ label: '活动公告', value: 'announcements' },
{ label: '问答交流', value: 'questions' },
{ label: '我的消息', value: 'personal' }
]
// Methods
const goBack = () => router.back()
const formatTime = (dateString) => {
const date = new Date(dateString.replace(' ', 'T'))
return new Intl.DateTimeFormat('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date)
}
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('zh-CN')
}
const handleSendMessage = async () => {
if (!messageInput.value.trim()) return
if (!store.currentUser) {
alert('请先登录再发送消息')
return
}
isSubmitting.value = true
try {
const now = new Date().toISOString().replace('T', ' ').substring(0, 19)
const newMessage = {
activity_id: activityId,
user_id: store.currentUser.id,
type: 'question',
content: messageInput.value,
timestamp: now,
user_name: store.currentUser.name,
user_avatar: store.currentUser.avatar
}
const result = await store.addMessage(newMessage)
if (result.success) {
messageInput.value = ''
} else {
alert(`发送失败: ${result.error}`)
}
} catch (error) {
console.error('Failed to send message:', error)
alert('发送消息失败,请稍后重试')
} finally {
isSubmitting.value = false
const openMessageDetail = (message) => {
selectedMessage.value = message
showModal.value = true
// 更新消息已读状态
if (!message.read_status) {
message.read_status = true
}
}
// Message components
const AnnouncementMessage = {
props: ['message'],
template: `
<div class="bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4">
<div class="flex items-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-blue-500 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 3a1 1 0 00-1.447-.894L8.763 6H5a3 3 0 000 6h.28l1.771 5.316A1 1 0 008 18h1a1 1 0 001-1v-4.382l6.553 3.276A1 1 0 0018 15V3z" clip-rule="evenodd" />
</svg>
<span class="font-semibold text-blue-800">活动公告</span>
</div>
<p class="text-gray-700">{{ message.content }}</p>
<div class="mt-2 text-xs text-gray-500 flex items-center justify-between">
<span>{{ message.user_name }}</span>
<span>{{ formatTime(message.timestamp) }}</span>
</div>
</div>
`
const closeModal = () => {
showModal.value = false
selectedMessage.value = null
replyContent.value = ''
}
const AnswerMessage = {
props: ['message'],
template: `
<div class="bg-green-50 border border-green-100 rounded-lg p-4 mb-4">
<div class="flex items-center mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 13V5a2 2 0 00-2-2H4a2 2 0 00-2 2v8a2 2 0 002 2h3l3 3 3-3h3a2 2 0 002-2zM5 7a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1zm1 3a1 1 0 100 2h3a1 1 0 100-2H6z" clip-rule="evenodd" />
</svg>
<span class="font-semibold text-green-800">回复</span>
</div>
<div v-if="message.reply_to" class="bg-white rounded p-2 mb-2 text-sm text-gray-600 border-l-2 border-gray-300">
<p class="truncate">{{ message.reply_to }}</p>
</div>
<p class="text-gray-700">{{ message.content }}</p>
<div class="mt-2 text-xs text-gray-500 flex items-center justify-between">
<span>{{ message.user_name }}</span>
<span>{{ formatTime(message.timestamp) }}</span>
</div>
</div>
`
}
const sendReply = () => {
if (!replyContent.value.trim()) return
const DefaultMessage = {
props: ['message'],
setup(props) {
const store = useAppStore()
const isCurrentUser = computed(() => props.message.user_id === store.currentUser?.id)
return { isCurrentUser, formatTime }
},
template: `
<div :class="['flex', isCurrentUser ? 'justify-end' : 'justify-start', 'mb-4']">
<div :class="['max-w-[80%]', isCurrentUser ? 'order-1' : 'order-2']">
<div :class="['flex items-center', isCurrentUser ? 'justify-end' : 'justify-start', 'mb-1']">
<span class="text-xs text-gray-500 mr-2">{{ message.user_name }}</span>
<div class="h-6 w-6 rounded-full overflow-hidden bg-gray-200">
<img v-if="message.user_avatar" :src="message.user_avatar" :alt="message.user_name" class="h-full w-full object-cover" />
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-full w-full text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
<div :class="['rounded-lg p-3', isCurrentUser ? 'bg-green-100' : 'bg-white border border-gray-200']">
<p class="text-gray-700">{{ message.content }}</p>
</div>
<div :class="['text-xs text-gray-500 mt-1', isCurrentUser ? 'text-right' : 'text-left']">
{{ formatTime(message.timestamp) }}
</div>
</div>
</div>
`
}
// 这里可以添加发送回复的逻辑
console.log('回复内容:', replyContent.value)
console.log('回复给消息:', selectedMessage.value)
const messageComponent = (message) => {
switch (message.type) {
case 'announcement':
return AnnouncementMessage
case 'answer':
return AnswerMessage
default:
return DefaultMessage
}
// 清空回复内容并关闭弹窗
replyContent.value = ''
closeModal()
}
// Computed properties for message grouping
const groupedMessages = computed(() => {
return filteredMessages.value.reduce((groups, message) => {
const date = message.timestamp.split(' ')[0]
if (!groups[date]) {
groups[date] = []
}
groups[date].push(message)
return groups
}, {})
})
const sortedDates = computed(() => {
return Object.keys(groupedMessages.value).sort()
})
// Watch for changes in messages and scroll to bottom
watch(filteredMessages, () => {
if (messageEndRef.value) {
messageEndRef.value.scrollIntoView({ behavior: 'smooth' })
}
})
// Filter messages based on active tab
watch([() => store.messages, activeTab], () => {
if (!store.messages) return
let filtered = store.messages.filter(msg => msg.activity_id === activityId)
switch (activeTab.value) {
case 'announcements':
filtered = filtered.filter(msg => msg.type === 'announcement')
break
case 'questions':
filtered = filtered.filter(msg => msg.type === 'question' || msg.type === 'answer')
break
case 'personal':
filtered = filtered.filter(msg =>
msg.user_id === store.currentUser?.id || msg.to_user_id === store.currentUser?.id
)
break
}
filteredMessages.value = filtered.sort((a, b) => {
return new Date(a.timestamp.replace(' ', 'T')) - new Date(b.timestamp.replace(' ', 'T'))
})
})
// Initial data fetch
onMounted(async () => {
try {
if (!store.currentUser) {
error.value = '请先登录'
loading.value = false
return
}
if (store.activities.length > 0) {
const foundActivity = store.getActivityById(activityId)
if (foundActivity) {
activity.value = foundActivity
// Filter messages for this activity
const activityMessages = store.messages.filter(
msg => msg.activity_id === activityId
).sort((a, b) => {
return new Date(a.timestamp.replace(' ', 'T')) - new Date(b.timestamp.replace(' ', 'T'))
})
filteredMessages.value = activityMessages
} else {
error.value = '未找到活动信息'
}
}
loading.value = false
} catch (err) {
console.error('Failed to fetch data:', err)
error.value = '加载数据失败'
loading.value = false
}
})
</script>
<style scoped>
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
</style>
......
......@@ -262,7 +262,6 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useAppStore } from '../stores/app'
import Button from '../components/shared/Button.vue'
import Input from '../components/shared/Input.vue'
import ActivityCard from '../components/shared/ActivityCard.vue'
......@@ -351,8 +350,6 @@ onMounted(() => {
}
}
console.warn(registrations.value);
console.warn(activities.value);
if (currentUser.value && registrations.value && activities.value) {
// Filter user registrations
const userRegs = registrations.value.filter(reg => reg.user_id === currentUser.value.id)
......