refactor: 优化页面组件和路由配置
重构了用户个人资料页、活动详情页、签到页和消息页的代码,移除了未使用的导入和调试日志。调整了路由配置,简化了消息页的UI,并优化了签到逻辑。这些改动旨在提高代码的可维护性和用户体验。
Showing
6 changed files
with
148 additions
and
368 deletions
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2025-04-17 13:16:20 | 2 | * @Date: 2025-04-17 13:16:20 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-04-21 14:56:50 | 4 | + * @LastEditTime: 2025-04-21 15:29:42 |
| 5 | * @FilePath: /mlaj-reading-club/src/components/shared/ActivityCard.vue | 5 | * @FilePath: /mlaj-reading-club/src/components/shared/ActivityCard.vue |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | --> | 7 | --> |
| ... | @@ -61,17 +61,29 @@ | ... | @@ -61,17 +61,29 @@ |
| 61 | </div> | 61 | </div> |
| 62 | </div> | 62 | </div> |
| 63 | 63 | ||
| 64 | - <router-link :to="`/activity/${activity.id}`" | 64 | + <router-link v-if="!isRegistrationOpen" :to="`/activity/${activity.id}`" |
| 65 | 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"> | 65 | 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"> |
| 66 | - {{ isPast ? '查看详情' : isRegistrationOpen ? '立即报名' : '了解更多' }} | 66 | + {{ isPast ? '查看详情' : '了解更多' }} |
| 67 | </router-link> | 67 | </router-link> |
| 68 | + <div v-else @click="handleRegistration" | ||
| 69 | + 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"> | ||
| 70 | + 立即报名 | ||
| 71 | + </div> | ||
| 68 | </div> | 72 | </div> |
| 69 | </div> | 73 | </div> |
| 70 | </div> | 74 | </div> |
| 75 | + | ||
| 76 | + <!-- Register Modal --> | ||
| 77 | + <Modal :isOpen="showRegisterModal" title="活动报名" @close="showRegisterModal = false"> | ||
| 78 | + <form @submit.prevent="submitRegistration"> | ||
| 79 | + <!-- Registration form fields --> | ||
| 80 | + </form> | ||
| 81 | + </Modal> | ||
| 71 | </template> | 82 | </template> |
| 72 | 83 | ||
| 73 | <script setup> | 84 | <script setup> |
| 74 | import { ref, computed, defineProps } from 'vue' | 85 | import { ref, computed, defineProps } from 'vue' |
| 86 | +import Modal from './Modal.vue' | ||
| 75 | 87 | ||
| 76 | const props = defineProps({ | 88 | const props = defineProps({ |
| 77 | activity: { | 89 | activity: { |
| ... | @@ -136,4 +148,15 @@ const statusText = computed(() => { | ... | @@ -136,4 +148,15 @@ const statusText = computed(() => { |
| 136 | } | 148 | } |
| 137 | return ''; | 149 | return ''; |
| 138 | }); | 150 | }); |
| 151 | + | ||
| 152 | +const showRegisterModal = ref(false) | ||
| 153 | + | ||
| 154 | +const handleRegistration = () => { | ||
| 155 | + showRegisterModal.value = true | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +const submitRegistration = async () => { | ||
| 159 | + // TODO: Implement registration submission | ||
| 160 | + showRegisterModal.value = false | ||
| 161 | +} | ||
| 139 | </script> | 162 | </script> | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-04-17 14:26:17 | 2 | * @Date: 2025-04-17 14:26:17 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2025-04-21 13:26:32 | 4 | + * @LastEditTime: 2025-04-21 15:07:36 |
| 5 | * @FilePath: /mlaj-reading-club/src/main.js | 5 | * @FilePath: /mlaj-reading-club/src/main.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| ... | @@ -27,8 +27,8 @@ const router = createRouter({ | ... | @@ -27,8 +27,8 @@ const router = createRouter({ |
| 27 | { path: '/create-activity', component: () => import('./pages/CreateActivity.vue') }, | 27 | { path: '/create-activity', component: () => import('./pages/CreateActivity.vue') }, |
| 28 | { path: '/profile', component: () => import('./pages/UserProfile.vue') }, | 28 | { path: '/profile', component: () => import('./pages/UserProfile.vue') }, |
| 29 | { path: '/registration/:activityId', component: () => import('./pages/Registration.vue') }, | 29 | { path: '/registration/:activityId', component: () => import('./pages/Registration.vue') }, |
| 30 | - { path: '/check-in/:activityId', component: () => import('./pages/CheckIn.vue') }, | 30 | + { path: '/check-in/:id', component: () => import('./pages/CheckIn.vue') }, |
| 31 | - { path: '/messages/:activityId', component: () => import('./pages/Messages.vue') }, | 31 | + { path: '/messages', component: () => import('./pages/Messages.vue') }, |
| 32 | { path: '/:pathMatch(.*)*', redirect: '/' } | 32 | { path: '/:pathMatch(.*)*', redirect: '/' } |
| 33 | ] | 33 | ] |
| 34 | }) | 34 | }) | ... | ... |
| ... | @@ -231,7 +231,7 @@ | ... | @@ -231,7 +231,7 @@ |
| 231 | </div> | 231 | </div> |
| 232 | <div class="mt-2 flex items-center"> | 232 | <div class="mt-2 flex items-center"> |
| 233 | <span class="text-sm text-green-700 mr-2">报名状态:</span> | 233 | <span class="text-sm text-green-700 mr-2">报名状态:</span> |
| 234 | - <span :class="registrationStatusBadge.class">{{ registrationStatusBadge.text | 234 | + <span :class="registrationStatusBadge?.class">{{ registrationStatusBadge?.text |
| 235 | }}</span> | 235 | }}</span> |
| 236 | </div> | 236 | </div> |
| 237 | </div> | 237 | </div> |
| ... | @@ -271,13 +271,10 @@ | ... | @@ -271,13 +271,10 @@ |
| 271 | </template> | 271 | </template> |
| 272 | 272 | ||
| 273 | <!-- Register Modal --> | 273 | <!-- Register Modal --> |
| 274 | - <Modal v-if="showRegisterModal" @close="showRegisterModal = false"> | 274 | + <Modal :isOpen="showRegisterModal" title="活动报名" @close="showRegisterModal = false"> |
| 275 | - <template #title>活动报名</template> | 275 | + <form @submit.prevent="submitRegistration"> |
| 276 | - <template #content> | ||
| 277 | - <form @submit.prevent="submitRegistration"> | ||
| 278 | <!-- Registration form fields --> | 276 | <!-- Registration form fields --> |
| 279 | </form> | 277 | </form> |
| 280 | - </template> | ||
| 281 | </Modal> | 278 | </Modal> |
| 282 | </div> | 279 | </div> |
| 283 | </template> | 280 | </template> | ... | ... |
| ... | @@ -261,6 +261,7 @@ import { useRoute, useRouter } from 'vue-router' | ... | @@ -261,6 +261,7 @@ import { useRoute, useRouter } from 'vue-router' |
| 261 | import activitiesData from '../data/activities.json' | 261 | import activitiesData from '../data/activities.json' |
| 262 | import registrationsData from '../data/registrations.json' | 262 | import registrationsData from '../data/registrations.json' |
| 263 | import usersData from '../data/users.json' | 263 | import usersData from '../data/users.json' |
| 264 | +import checkinsData from '../data/checkins.json' | ||
| 264 | import Button from '../components/shared/Button.vue' | 265 | import Button from '../components/shared/Button.vue' |
| 265 | 266 | ||
| 266 | const route = useRoute() | 267 | const route = useRoute() |
| ... | @@ -268,8 +269,9 @@ const router = useRouter() | ... | @@ -268,8 +269,9 @@ const router = useRouter() |
| 268 | const activities = ref(activitiesData.activities) | 269 | const activities = ref(activitiesData.activities) |
| 269 | const registrations = ref(registrationsData.registrations) | 270 | const registrations = ref(registrationsData.registrations) |
| 270 | const currentUser = ref(usersData.users[0]) | 271 | const currentUser = ref(usersData.users[0]) |
| 272 | +const checkIns = ref(checkinsData) | ||
| 271 | 273 | ||
| 272 | -const activityId = route.params.activityId | 274 | +const activityId = route.params.id |
| 273 | const activity = ref(null) | 275 | const activity = ref(null) |
| 274 | const loading = ref(true) | 276 | const loading = ref(true) |
| 275 | const error = ref(null) | 277 | const error = ref(null) |
| ... | @@ -303,8 +305,8 @@ const fetchData = async () => { | ... | @@ -303,8 +305,8 @@ const fetchData = async () => { |
| 303 | userRegistration.value = registration | 305 | userRegistration.value = registration |
| 304 | 306 | ||
| 305 | // Check if user already checked in | 307 | // Check if user already checked in |
| 306 | - const checkIn = appStore.checkIns.find( | 308 | + const checkIn = checkIns.value.find( |
| 307 | - check => check.activity_id === activityId && check.user_id === appStore.currentUser.id | 309 | + check => check.activity_id === activityId && check.user_id === currentUser.id |
| 308 | ) | 310 | ) |
| 309 | 311 | ||
| 310 | if (checkIn) { | 312 | if (checkIn) { |
| ... | @@ -341,8 +343,8 @@ const handleSubmitCode = async () => { | ... | @@ -341,8 +343,8 @@ const handleSubmitCode = async () => { |
| 341 | try { | 343 | try { |
| 342 | if (inputCode.value === checkInCode.value) { | 344 | if (inputCode.value === checkInCode.value) { |
| 343 | const now = new Date().toISOString().replace('T', ' ').substring(0, 19) | 345 | const now = new Date().toISOString().replace('T', ' ').substring(0, 19) |
| 344 | - const checkInResult = await appStore.addCheckIn({ | 346 | + const checkInResult = await addCheckIn({ |
| 345 | - user_id: appStore.currentUser.id, | 347 | + user_id: currentUser.id, |
| 346 | activity_id: activityId, | 348 | activity_id: activityId, |
| 347 | check_in_time: now, | 349 | check_in_time: now, |
| 348 | status: 'checked_in' | 350 | status: 'checked_in' |
| ... | @@ -350,7 +352,7 @@ const handleSubmitCode = async () => { | ... | @@ -350,7 +352,7 @@ const handleSubmitCode = async () => { |
| 350 | 352 | ||
| 351 | if (checkInResult.success) { | 353 | if (checkInResult.success) { |
| 352 | userCheckIn.value = { | 354 | userCheckIn.value = { |
| 353 | - user_id: appStore.currentUser.id, | 355 | + user_id: currentUser.id, |
| 354 | activity_id: activityId, | 356 | activity_id: activityId, |
| 355 | check_in_time: now, | 357 | check_in_time: now, |
| 356 | status: 'checked_in' | 358 | status: 'checked_in' |
| ... | @@ -418,4 +420,33 @@ watch(countdown, (newValue) => { | ... | @@ -418,4 +420,33 @@ watch(countdown, (newValue) => { |
| 418 | 420 | ||
| 419 | // Initial data fetch | 421 | // Initial data fetch |
| 420 | onMounted(fetchData) | 422 | onMounted(fetchData) |
| 423 | + | ||
| 424 | + | ||
| 425 | +// 添加签到记录 | ||
| 426 | +const addCheckIn = async (checkInData) => { | ||
| 427 | + try { | ||
| 428 | + // 模拟API调用延迟 | ||
| 429 | + await new Promise(resolve => setTimeout(resolve, 1000)) | ||
| 430 | + | ||
| 431 | + // 创建新的签到记录 | ||
| 432 | + const newCheckIn = { | ||
| 433 | + id: `C${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`, | ||
| 434 | + activity_id: checkInData.activity_id, | ||
| 435 | + user_id: checkInData.user_id, | ||
| 436 | + checkin_time: checkInData.check_in_time, | ||
| 437 | + checkin_type: 'code', | ||
| 438 | + status: 'successful', | ||
| 439 | + notes: '', | ||
| 440 | + is_late: false | ||
| 441 | + } | ||
| 442 | + | ||
| 443 | + // 添加到签到记录中 | ||
| 444 | + checkIns.value.push(newCheckIn) | ||
| 445 | + | ||
| 446 | + return { success: true } | ||
| 447 | + } catch (err) { | ||
| 448 | + console.error('Failed to add check-in:', err) | ||
| 449 | + return { success: false } | ||
| 450 | + } | ||
| 451 | +} | ||
| 421 | </script> | 452 | </script> | ... | ... |
| 1 | <template> | 1 | <template> |
| 2 | - <div class="min-h-screen bg-gray-50"> | 2 | + <div class="container mx-auto px-4 py-8"> |
| 3 | - <!-- Header --> | 3 | + <h1 class="text-2xl font-bold mb-6">系统消息</h1> |
| 4 | - <div class="bg-white shadow-sm sticky top-0 z-10"> | 4 | + |
| 5 | - <div class="container mx-auto px-4 py-3"> | 5 | + <!-- 消息列表 --> |
| 6 | - <div class="flex items-center justify-between"> | 6 | + <div class="space-y-4"> |
| 7 | - <div class="flex items-center"> | 7 | + <div v-for="message in messages" :key="message.id" |
| 8 | - <button @click="goBack" class="mr-3 text-gray-600 hover:text-gray-900"> | 8 | + class="bg-white p-4 rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer" |
| 9 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" | 9 | + :class="{'border-l-4 border-blue-500': !message.read_status}" |
| 10 | - fill="currentColor"> | 10 | + @click="openMessageDetail(message)"> |
| 11 | - <path fill-rule="evenodd" | 11 | + <div class="flex justify-between items-start"> |
| 12 | - 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" | 12 | + <div class="flex-1"> |
| 13 | - clip-rule="evenodd" /> | 13 | + <p class="text-gray-900 font-medium">{{ message.content }}</p> |
| 14 | - </svg> | 14 | + <p class="text-sm text-gray-500 mt-1">{{ message.send_time }}</p> |
| 15 | - </button> | 15 | + </div> |
| 16 | - <div> | 16 | + <div class="ml-4"> |
| 17 | - <h1 class="text-lg font-medium text-gray-900 truncate max-w-xs">{{ activity?.title }}</h1> | 17 | + <span v-if="!message.read_status" |
| 18 | - <p class="text-sm text-gray-500">消息交流</p> | 18 | + class="inline-block w-2 h-2 bg-blue-500 rounded-full"></span> |
| 19 | - </div> | 19 | + </div> |
| 20 | - </div> | ||
| 21 | - <router-link :to="`/activity/${activityId}`" | ||
| 22 | - class="text-sm text-green-600 hover:text-green-700 font-medium"> | ||
| 23 | - 活动详情 | ||
| 24 | - </router-link> | ||
| 25 | - </div> | ||
| 26 | - </div> | ||
| 27 | </div> | 20 | </div> |
| 21 | + </div> | ||
| 22 | + </div> | ||
| 28 | 23 | ||
| 29 | - <!-- Message Tabs --> | 24 | + <!-- 消息详情弹窗 --> |
| 30 | - <div class="bg-white shadow-sm"> | 25 | + <div v-if="showModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center"> |
| 31 | - <div class="container mx-auto px-4"> | 26 | + <div class="bg-white rounded-lg p-6 max-w-lg w-full mx-4"> |
| 32 | - <div class="flex overflow-x-auto hide-scrollbar"> | 27 | + <div class="flex justify-between items-start mb-4"> |
| 33 | - <button v-for="tab in tabs" :key="tab.value" :class="[ | 28 | + <h2 class="text-xl font-semibold">消息详情</h2> |
| 34 | - 'py-3 px-4 text-sm font-medium border-b-2 whitespace-nowrap', | 29 | + <button @click="closeModal" class="text-gray-500 hover:text-gray-700"> |
| 35 | - activeTab === tab.value | 30 | + <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> |
| 36 | - ? 'border-green-500 text-green-600' | 31 | + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> |
| 37 | - : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' | 32 | + </svg> |
| 38 | - ]" @click="activeTab = tab.value"> | 33 | + </button> |
| 39 | - {{ tab.label }} | ||
| 40 | - </button> | ||
| 41 | - </div> | ||
| 42 | - </div> | ||
| 43 | </div> | 34 | </div> |
| 44 | 35 | ||
| 45 | - <!-- Messages Container --> | 36 | + <div class="mb-6"> |
| 46 | - <div class="container mx-auto px-4 py-4 flex flex-col h-[calc(100vh-13rem)]"> | 37 | + <p class="text-gray-900 mb-2">{{ selectedMessage?.content }}</p> |
| 47 | - <!-- Message List --> | 38 | + <p class="text-sm text-gray-500">{{ selectedMessage?.send_time }}</p> |
| 48 | - <div class="flex-grow overflow-y-auto mb-4 pr-1"> | 39 | + </div> |
| 49 | - <div v-if="filteredMessages.length === 0" class="flex flex-col items-center justify-center h-full"> | ||
| 50 | - <div class="text-gray-400"> | ||
| 51 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16" fill="none" viewBox="0 0 24 24" | ||
| 52 | - stroke="currentColor"> | ||
| 53 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" | ||
| 54 | - 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" /> | ||
| 55 | - </svg> | ||
| 56 | - </div> | ||
| 57 | - <p class="text-gray-500 mt-2">暂无消息</p> | ||
| 58 | - <button v-if="activeTab !== 'all'" @click="activeTab = 'all'" | ||
| 59 | - class="mt-4 text-green-600 hover:text-green-700 font-medium text-sm"> | ||
| 60 | - 查看全部消息 | ||
| 61 | - </button> | ||
| 62 | - </div> | ||
| 63 | - <div v-else> | ||
| 64 | - <div v-for="date in sortedDates" :key="date"> | ||
| 65 | - <div class="flex items-center justify-center my-4"> | ||
| 66 | - <div class="border-t border-gray-200 flex-grow"></div> | ||
| 67 | - <span class="mx-4 text-xs text-gray-500">{{ formatDate(date) }}</span> | ||
| 68 | - <div class="border-t border-gray-200 flex-grow"></div> | ||
| 69 | - </div> | ||
| 70 | - <div v-for="message in groupedMessages[date]" :key="message.id"> | ||
| 71 | - <component :is="messageComponent(message)" :message="message" /> | ||
| 72 | - </div> | ||
| 73 | - </div> | ||
| 74 | - <div ref="messageEndRef" /> | ||
| 75 | - </div> | ||
| 76 | - </div> | ||
| 77 | 40 | ||
| 78 | - <!-- Message Input --> | 41 | + <!-- 回复框 --> |
| 79 | - <div class="bg-white border border-gray-200 rounded-lg overflow-hidden"> | 42 | + <div class="mt-4"> |
| 80 | - <div v-if="isOrganizer" class="p-2 bg-gray-50 border-b border-gray-200"> | 43 | + <textarea |
| 81 | - <div class="flex items-center"> | 44 | + v-model="replyContent" |
| 82 | - <span | 45 | + rows="3" |
| 83 | - class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800"> | 46 | + class="w-full border rounded-lg p-2 focus:outline-none focus:ring-2 focus:ring-blue-500" |
| 84 | - 组织者 | 47 | + placeholder="输入回复内容..." |
| 85 | - </span> | 48 | + ></textarea> |
| 86 | - <span class="text-xs text-gray-500 ml-2"> | 49 | + <div class="flex justify-end mt-2"> |
| 87 | - 您可以发送公告消息 | 50 | + <button |
| 88 | - </span> | 51 | + @click="sendReply" |
| 89 | - </div> | 52 | + class="bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 transition-colors" |
| 90 | - </div> | 53 | + > |
| 91 | - <form @submit.prevent="handleSendMessage" class="flex p-2"> | 54 | + 发送回复 |
| 92 | - <input type="text" v-model="messageInput" placeholder="输入消息..." | 55 | + </button> |
| 93 | - class="flex-grow border-none focus:ring-0 focus:outline-none" :disabled="isSubmitting" /> | 56 | + </div> |
| 94 | - <Button type="submit" variant="primary" size="sm" :disabled="!messageInput.trim() || isSubmitting"> | ||
| 95 | - 发送 | ||
| 96 | - </Button> | ||
| 97 | - </form> | ||
| 98 | - </div> | ||
| 99 | </div> | 57 | </div> |
| 58 | + </div> | ||
| 100 | </div> | 59 | </div> |
| 60 | + </div> | ||
| 101 | </template> | 61 | </template> |
| 102 | 62 | ||
| 103 | <script setup> | 63 | <script setup> |
| 104 | -import { ref, computed, onMounted, watch } from 'vue' | 64 | +import { ref, onMounted } from 'vue' |
| 105 | -import { useRoute, useRouter } from 'vue-router' | 65 | +import messagesData from '../data/messages.json' |
| 106 | -import { useAppStore } from '../stores/app' | ||
| 107 | -import Button from '../components/shared/Button.vue' | ||
| 108 | 66 | ||
| 109 | -// Route and store setup | 67 | +const messages = ref([]) |
| 110 | -const route = useRoute() | 68 | +const showModal = ref(false) |
| 111 | -const router = useRouter() | 69 | +const selectedMessage = ref(null) |
| 112 | -const store = useAppStore() | 70 | +const replyContent = ref('') |
| 113 | 71 | ||
| 114 | -// Props and refs | 72 | +onMounted(() => { |
| 115 | -const activityId = route.params.activityId | 73 | + messages.value = messagesData.messages |
| 116 | -const messageEndRef = ref(null) | ||
| 117 | -const messageInput = ref('') | ||
| 118 | -const activeTab = ref('all') | ||
| 119 | -const isSubmitting = ref(false) | ||
| 120 | -const activity = ref(null) | ||
| 121 | -const filteredMessages = ref([]) | ||
| 122 | -const loading = ref(true) | ||
| 123 | -const error = ref(null) | ||
| 124 | - | ||
| 125 | -// Computed properties | ||
| 126 | -const isOrganizer = computed(() => { | ||
| 127 | - return store.currentUser && store.currentUser.id === activity.value?.organizer_id | ||
| 128 | }) | 74 | }) |
| 129 | 75 | ||
| 130 | -// Tabs configuration | 76 | +const openMessageDetail = (message) => { |
| 131 | -const tabs = [ | 77 | + selectedMessage.value = message |
| 132 | - { label: '全部消息', value: 'all' }, | 78 | + showModal.value = true |
| 133 | - { label: '活动公告', value: 'announcements' }, | 79 | + // 更新消息已读状态 |
| 134 | - { label: '问答交流', value: 'questions' }, | 80 | + if (!message.read_status) { |
| 135 | - { label: '我的消息', value: 'personal' } | 81 | + message.read_status = true |
| 136 | -] | 82 | + } |
| 137 | - | ||
| 138 | -// Methods | ||
| 139 | -const goBack = () => router.back() | ||
| 140 | - | ||
| 141 | -const formatTime = (dateString) => { | ||
| 142 | - const date = new Date(dateString.replace(' ', 'T')) | ||
| 143 | - return new Intl.DateTimeFormat('zh-CN', { | ||
| 144 | - month: 'short', | ||
| 145 | - day: 'numeric', | ||
| 146 | - hour: '2-digit', | ||
| 147 | - minute: '2-digit', | ||
| 148 | - hour12: false | ||
| 149 | - }).format(date) | ||
| 150 | } | 83 | } |
| 151 | 84 | ||
| 152 | -const formatDate = (dateString) => { | 85 | +const closeModal = () => { |
| 153 | - return new Date(dateString).toLocaleDateString('zh-CN') | 86 | + showModal.value = false |
| 87 | + selectedMessage.value = null | ||
| 88 | + replyContent.value = '' | ||
| 154 | } | 89 | } |
| 155 | 90 | ||
| 156 | -const handleSendMessage = async () => { | 91 | +const sendReply = () => { |
| 157 | - if (!messageInput.value.trim()) return | 92 | + if (!replyContent.value.trim()) return |
| 158 | - | ||
| 159 | - if (!store.currentUser) { | ||
| 160 | - alert('请先登录再发送消息') | ||
| 161 | - return | ||
| 162 | - } | ||
| 163 | - | ||
| 164 | - isSubmitting.value = true | ||
| 165 | - | ||
| 166 | - try { | ||
| 167 | - const now = new Date().toISOString().replace('T', ' ').substring(0, 19) | ||
| 168 | - | ||
| 169 | - const newMessage = { | ||
| 170 | - activity_id: activityId, | ||
| 171 | - user_id: store.currentUser.id, | ||
| 172 | - type: 'question', | ||
| 173 | - content: messageInput.value, | ||
| 174 | - timestamp: now, | ||
| 175 | - user_name: store.currentUser.name, | ||
| 176 | - user_avatar: store.currentUser.avatar | ||
| 177 | - } | ||
| 178 | - | ||
| 179 | - const result = await store.addMessage(newMessage) | ||
| 180 | - | ||
| 181 | - if (result.success) { | ||
| 182 | - messageInput.value = '' | ||
| 183 | - } else { | ||
| 184 | - alert(`发送失败: ${result.error}`) | ||
| 185 | - } | ||
| 186 | - } catch (error) { | ||
| 187 | - console.error('Failed to send message:', error) | ||
| 188 | - alert('发送消息失败,请稍后重试') | ||
| 189 | - } finally { | ||
| 190 | - isSubmitting.value = false | ||
| 191 | - } | ||
| 192 | -} | ||
| 193 | - | ||
| 194 | -// Message components | ||
| 195 | -const AnnouncementMessage = { | ||
| 196 | - props: ['message'], | ||
| 197 | - template: ` | ||
| 198 | - <div class="bg-blue-50 border border-blue-100 rounded-lg p-4 mb-4"> | ||
| 199 | - <div class="flex items-center mb-2"> | ||
| 200 | - <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"> | ||
| 201 | - <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" /> | ||
| 202 | - </svg> | ||
| 203 | - <span class="font-semibold text-blue-800">活动公告</span> | ||
| 204 | - </div> | ||
| 205 | - <p class="text-gray-700">{{ message.content }}</p> | ||
| 206 | - <div class="mt-2 text-xs text-gray-500 flex items-center justify-between"> | ||
| 207 | - <span>{{ message.user_name }}</span> | ||
| 208 | - <span>{{ formatTime(message.timestamp) }}</span> | ||
| 209 | - </div> | ||
| 210 | - </div> | ||
| 211 | - ` | ||
| 212 | -} | ||
| 213 | - | ||
| 214 | -const AnswerMessage = { | ||
| 215 | - props: ['message'], | ||
| 216 | - template: ` | ||
| 217 | - <div class="bg-green-50 border border-green-100 rounded-lg p-4 mb-4"> | ||
| 218 | - <div class="flex items-center mb-2"> | ||
| 219 | - <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"> | ||
| 220 | - <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" /> | ||
| 221 | - </svg> | ||
| 222 | - <span class="font-semibold text-green-800">回复</span> | ||
| 223 | - </div> | ||
| 224 | - <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"> | ||
| 225 | - <p class="truncate">{{ message.reply_to }}</p> | ||
| 226 | - </div> | ||
| 227 | - <p class="text-gray-700">{{ message.content }}</p> | ||
| 228 | - <div class="mt-2 text-xs text-gray-500 flex items-center justify-between"> | ||
| 229 | - <span>{{ message.user_name }}</span> | ||
| 230 | - <span>{{ formatTime(message.timestamp) }}</span> | ||
| 231 | - </div> | ||
| 232 | - </div> | ||
| 233 | - ` | ||
| 234 | -} | ||
| 235 | 93 | ||
| 236 | -const DefaultMessage = { | 94 | + // 这里可以添加发送回复的逻辑 |
| 237 | - props: ['message'], | 95 | + console.log('回复内容:', replyContent.value) |
| 238 | - setup(props) { | 96 | + console.log('回复给消息:', selectedMessage.value) |
| 239 | - const store = useAppStore() | ||
| 240 | - const isCurrentUser = computed(() => props.message.user_id === store.currentUser?.id) | ||
| 241 | - return { isCurrentUser, formatTime } | ||
| 242 | - }, | ||
| 243 | - template: ` | ||
| 244 | - <div :class="['flex', isCurrentUser ? 'justify-end' : 'justify-start', 'mb-4']"> | ||
| 245 | - <div :class="['max-w-[80%]', isCurrentUser ? 'order-1' : 'order-2']"> | ||
| 246 | - <div :class="['flex items-center', isCurrentUser ? 'justify-end' : 'justify-start', 'mb-1']"> | ||
| 247 | - <span class="text-xs text-gray-500 mr-2">{{ message.user_name }}</span> | ||
| 248 | - <div class="h-6 w-6 rounded-full overflow-hidden bg-gray-200"> | ||
| 249 | - <img v-if="message.user_avatar" :src="message.user_avatar" :alt="message.user_name" class="h-full w-full object-cover" /> | ||
| 250 | - <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"> | ||
| 251 | - <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" /> | ||
| 252 | - </svg> | ||
| 253 | - </div> | ||
| 254 | - </div> | ||
| 255 | - <div :class="['rounded-lg p-3', isCurrentUser ? 'bg-green-100' : 'bg-white border border-gray-200']"> | ||
| 256 | - <p class="text-gray-700">{{ message.content }}</p> | ||
| 257 | - </div> | ||
| 258 | - <div :class="['text-xs text-gray-500 mt-1', isCurrentUser ? 'text-right' : 'text-left']"> | ||
| 259 | - {{ formatTime(message.timestamp) }} | ||
| 260 | - </div> | ||
| 261 | - </div> | ||
| 262 | - </div> | ||
| 263 | - ` | ||
| 264 | -} | ||
| 265 | 97 | ||
| 266 | -const messageComponent = (message) => { | 98 | + // 清空回复内容并关闭弹窗 |
| 267 | - switch (message.type) { | 99 | + replyContent.value = '' |
| 268 | - case 'announcement': | 100 | + closeModal() |
| 269 | - return AnnouncementMessage | ||
| 270 | - case 'answer': | ||
| 271 | - return AnswerMessage | ||
| 272 | - default: | ||
| 273 | - return DefaultMessage | ||
| 274 | - } | ||
| 275 | } | 101 | } |
| 276 | - | ||
| 277 | -// Computed properties for message grouping | ||
| 278 | -const groupedMessages = computed(() => { | ||
| 279 | - return filteredMessages.value.reduce((groups, message) => { | ||
| 280 | - const date = message.timestamp.split(' ')[0] | ||
| 281 | - if (!groups[date]) { | ||
| 282 | - groups[date] = [] | ||
| 283 | - } | ||
| 284 | - groups[date].push(message) | ||
| 285 | - return groups | ||
| 286 | - }, {}) | ||
| 287 | -}) | ||
| 288 | - | ||
| 289 | -const sortedDates = computed(() => { | ||
| 290 | - return Object.keys(groupedMessages.value).sort() | ||
| 291 | -}) | ||
| 292 | - | ||
| 293 | -// Watch for changes in messages and scroll to bottom | ||
| 294 | -watch(filteredMessages, () => { | ||
| 295 | - if (messageEndRef.value) { | ||
| 296 | - messageEndRef.value.scrollIntoView({ behavior: 'smooth' }) | ||
| 297 | - } | ||
| 298 | -}) | ||
| 299 | - | ||
| 300 | -// Filter messages based on active tab | ||
| 301 | -watch([() => store.messages, activeTab], () => { | ||
| 302 | - if (!store.messages) return | ||
| 303 | - | ||
| 304 | - let filtered = store.messages.filter(msg => msg.activity_id === activityId) | ||
| 305 | - | ||
| 306 | - switch (activeTab.value) { | ||
| 307 | - case 'announcements': | ||
| 308 | - filtered = filtered.filter(msg => msg.type === 'announcement') | ||
| 309 | - break | ||
| 310 | - case 'questions': | ||
| 311 | - filtered = filtered.filter(msg => msg.type === 'question' || msg.type === 'answer') | ||
| 312 | - break | ||
| 313 | - case 'personal': | ||
| 314 | - filtered = filtered.filter(msg => | ||
| 315 | - msg.user_id === store.currentUser?.id || msg.to_user_id === store.currentUser?.id | ||
| 316 | - ) | ||
| 317 | - break | ||
| 318 | - } | ||
| 319 | - | ||
| 320 | - filteredMessages.value = filtered.sort((a, b) => { | ||
| 321 | - return new Date(a.timestamp.replace(' ', 'T')) - new Date(b.timestamp.replace(' ', 'T')) | ||
| 322 | - }) | ||
| 323 | -}) | ||
| 324 | - | ||
| 325 | -// Initial data fetch | ||
| 326 | -onMounted(async () => { | ||
| 327 | - try { | ||
| 328 | - if (!store.currentUser) { | ||
| 329 | - error.value = '请先登录' | ||
| 330 | - loading.value = false | ||
| 331 | - return | ||
| 332 | - } | ||
| 333 | - | ||
| 334 | - if (store.activities.length > 0) { | ||
| 335 | - const foundActivity = store.getActivityById(activityId) | ||
| 336 | - | ||
| 337 | - if (foundActivity) { | ||
| 338 | - activity.value = foundActivity | ||
| 339 | - | ||
| 340 | - // Filter messages for this activity | ||
| 341 | - const activityMessages = store.messages.filter( | ||
| 342 | - msg => msg.activity_id === activityId | ||
| 343 | - ).sort((a, b) => { | ||
| 344 | - return new Date(a.timestamp.replace(' ', 'T')) - new Date(b.timestamp.replace(' ', 'T')) | ||
| 345 | - }) | ||
| 346 | - | ||
| 347 | - filteredMessages.value = activityMessages | ||
| 348 | - } else { | ||
| 349 | - error.value = '未找到活动信息' | ||
| 350 | - } | ||
| 351 | - } | ||
| 352 | - loading.value = false | ||
| 353 | - } catch (err) { | ||
| 354 | - console.error('Failed to fetch data:', err) | ||
| 355 | - error.value = '加载数据失败' | ||
| 356 | - loading.value = false | ||
| 357 | - } | ||
| 358 | -}) | ||
| 359 | </script> | 102 | </script> |
| 360 | - | ||
| 361 | -<style scoped> | ||
| 362 | -.hide-scrollbar { | ||
| 363 | - -ms-overflow-style: none; | ||
| 364 | - scrollbar-width: none; | ||
| 365 | -} | ||
| 366 | - | ||
| 367 | -.hide-scrollbar::-webkit-scrollbar { | ||
| 368 | - display: none; | ||
| 369 | -} | ||
| 370 | -</style> | ... | ... |
| ... | @@ -262,7 +262,6 @@ | ... | @@ -262,7 +262,6 @@ |
| 262 | 262 | ||
| 263 | <script setup> | 263 | <script setup> |
| 264 | import { ref, computed, onMounted } from 'vue' | 264 | import { ref, computed, onMounted } from 'vue' |
| 265 | -import { useAppStore } from '../stores/app' | ||
| 266 | import Button from '../components/shared/Button.vue' | 265 | import Button from '../components/shared/Button.vue' |
| 267 | import Input from '../components/shared/Input.vue' | 266 | import Input from '../components/shared/Input.vue' |
| 268 | import ActivityCard from '../components/shared/ActivityCard.vue' | 267 | import ActivityCard from '../components/shared/ActivityCard.vue' |
| ... | @@ -351,8 +350,6 @@ onMounted(() => { | ... | @@ -351,8 +350,6 @@ onMounted(() => { |
| 351 | } | 350 | } |
| 352 | } | 351 | } |
| 353 | 352 | ||
| 354 | - console.warn(registrations.value); | ||
| 355 | - console.warn(activities.value); | ||
| 356 | if (currentUser.value && registrations.value && activities.value) { | 353 | if (currentUser.value && registrations.value && activities.value) { |
| 357 | // Filter user registrations | 354 | // Filter user registrations |
| 358 | const userRegs = registrations.value.filter(reg => reg.user_id === currentUser.value.id) | 355 | const userRegs = registrations.value.filter(reg => reg.user_id === currentUser.value.id) | ... | ... |
-
Please register or login to post a comment