hookehuyr

refactor(components): 替换Tabs组件为内联实现

在ActivityDetail.vue和UserProfile.vue中,将Tabs组件替换为内联实现,以提高代码的可读性和维护性。同时修复了路径中的空格问题,并调整了路由参数的命名。
......@@ -10,7 +10,6 @@
"preview": "vite preview"
},
"dependencies": {
"@vitejs/plugin-vue-jsx": "4.1.2",
"axios": "^1.8.4",
"pinia": "^2.1.7",
"vue": "^3.4.21",
......@@ -18,6 +17,7 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "4.1.2",
"@vue/compiler-sfc": "^3.4.21",
"autoprefixer": "^10.4.20",
"eslint": "^9.9.0",
......
......@@ -8,9 +8,6 @@ importers:
.:
dependencies:
'@vitejs/plugin-vue-jsx':
specifier: 4.1.2
version: 4.1.2(vite@5.4.18)(vue@3.5.13)
axios:
specifier: ^1.8.4
version: 1.8.4
......@@ -27,6 +24,9 @@ importers:
'@vitejs/plugin-vue':
specifier: ^5.0.4
version: 5.2.3(vite@5.4.18)(vue@3.5.13)
'@vitejs/plugin-vue-jsx':
specifier: 4.1.2
version: 4.1.2(vite@5.4.18)(vue@3.5.13)
'@vue/compiler-sfc':
specifier: ^3.4.21
version: 3.5.13
......
......@@ -16,7 +16,7 @@
<!-- Tab content -->
<div class="pt-4">
<component :is="tabs[currentTab]?.content" />
<component :is="tabs[currentTab]?.component" />
</div>
</div>
</template>
......
/*
* @Date: 2025-04-17 14:26:17
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2025-04-21 13:26:32
* @FilePath: /mlaj-reading-club/src/main.js
* @Description: 文件描述
*/
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createRouter, createWebHistory } from 'vue-router'
......@@ -16,7 +23,7 @@ const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('./pages/HomePage.vue') },
{ path: '/activity/:activityId', component: () => import('./pages/ActivityDetail.vue') },
{ path: '/activity/:id', component: () => import('./pages/ActivityDetail.vue') },
{ path: '/create-activity', component: () => import('./pages/CreateActivity.vue') },
{ path: '/profile', component: () => import('./pages/UserProfile.vue') },
{ path: '/registration/:activityId', component: () => import('./pages/Registration.vue') },
......
......@@ -75,7 +75,64 @@
</div>
<!-- Activity Tabs -->
<Tabs :tabs="tabs" />
<div>
<!-- Tab headers -->
<div class="flex border-b border-gray-200">
<button v-for="(tab, index) in tabs" :key="index"
class="text-base py-2 px-4 font-medium transition-all duration-200 focus:outline-none border-b-2"
:class="currentTab === index
? 'border-green-500 text-green-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
@click="currentTab = index" role="tab" :aria-selected="currentTab === index">
{{ tab.label }}
</button>
</div>
<!-- Tab content -->
<div class="pt-4">
<div v-if="tabs[currentTab].label === '活动详情'">
<div class="space-y-4">
<div v-for="(paragraph, idx) in activity.description.split('\n')" :key="idx"
class="mb-4">
{{ paragraph }}
</div>
</div>
</div>
<div v-if="tabs[currentTab].label === '参与须知'">
<div class="space-y-4">
<h3 class="font-medium text-lg">参与须知</h3>
<ul class="list-disc pl-5 space-y-2">
<li>请准时到达活动地点或登录线上会议</li>
<li>请提前阅读相关书籍或材料</li>
<li>活动开始后,请将手机调至静音模式</li>
<li>尊重他人发言,不打断他人</li>
<li>可携带笔记本进行记录</li>
<li>如需取消参与,请提前24小时通知主办方</li>
</ul>
</div>
</div>
<div v-if="tabs[currentTab].label === '常见问题'">
<div class="space-y-6">
<div>
<h4 class="font-medium text-gray-900">如何取消报名?</h4>
<p class="mt-2 text-gray-600">
您可以在"我的活动"页面找到已报名的活动,点击"取消报名"按钮即可。请注意,活动开始前24小时内取消将无法获得退款。</p>
</div>
<div>
<h4 class="font-medium text-gray-900">活动材料如何获取?</h4>
<p class="mt-2 text-gray-600">报名成功后,您将在"我的活动"页面看到活动详情,相关材料可在页面底部下载或通过邮件接收。
</p>
</div>
<div>
<h4 class="font-medium text-gray-900">线上活动如何参加?</h4>
<p class="mt-2 text-gray-600">
线上活动将在活动开始前30分钟发送会议链接到您的邮箱和手机短信,您也可以在"我的活动"页面找到入口链接。</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
......@@ -226,13 +283,17 @@
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ref, computed, onMounted, h, defineComponent } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useAppStore } from '../stores/app'
import Button from '../components/shared/Button.vue'
import Modal from '../components/shared/Modal.vue'
import Tabs from '../components/shared/Tabs.vue'
import ActivityCard from '../components/shared/ActivityCard.vue'
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
const activities = ref(activitiesData.activities)
const registrations = ref(registrationsData.registrations)
const route = useRoute()
const router = useRouter()
......@@ -246,6 +307,7 @@ const showRegisterModal = ref(false)
const similarActivities = ref([])
const hasRegistered = ref(false)
const registrationStatus = ref(null)
const currentTab = ref(0)
// Computed
const activityStatus = computed(() => {
......@@ -305,44 +367,13 @@ const registrationStatusBadge = computed(() => {
const tabs = computed(() => [
{
label: '活动详情',
content: activity.value?.description.split('\n').map((paragraph, idx) => (
`<p key="${idx}" class="mb-4">${paragraph}</p>`
)).join('')
},
{
label: '参与须知',
content: `
<h3 class="font-medium text-lg mb-4">参与须知</h3>
<ul class="list-disc pl-5 space-y-2">
<li>请准时到达活动地点或登录线上会议</li>
<li>请提前阅读相关书籍或材料</li>
<li>活动开始后,请将手机调至静音模式</li>
<li>尊重他人发言,不打断他人</li>
<li>可携带笔记本进行记录</li>
<li>如需取消参与,请提前24小时通知主办方</li>
</ul>
`
},
{
label: '常见问题',
content: `
<div className="py-4">
<div className="space-y-6">
<div>
<h4 className="font-medium text-gray-900">如何取消报名?</h4>
<p className="mt-2 text-gray-600">您可以在"我的活动"页面找到已报名的活动,点击"取消报名"按钮即可。请注意,活动开始前24小时内取消将无法获得退款。</p>
</div>
<div>
<h4 className="font-medium text-gray-900">活动材料如何获取?</h4>
<p className="mt-2 text-gray-600">报名成功后,您将在"我的活动"页面看到活动详情,相关材料可在页面底部下载或通过邮件接收。</p>
</div>
<div>
<h4 className="font-medium text-gray-900">线上活动如何参加?</h4>
<p className="mt-2 text-gray-600">线上活动将在活动开始前30分钟发送会议链接到您的邮箱和手机短信,您也可以在"我的活动"页面找到入口链接。</p>
</div>
</div>
</div>
`}
}
])
// Functions
......@@ -351,7 +382,7 @@ const goBack = () => {
}
const getActivityImage = (activityId) => {
return `/ assets / images / activities / ${activityId}.jpg`
return `/assets/images/activities/${activityId}.jpg`
}
const formatDate = (dateString) => {
......
......@@ -56,12 +56,170 @@
<!-- Profile Content -->
<div class="container mx-auto py-8 px-4">
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
<Tabs :tabs="profileTabs" />
<!-- Tab headers -->
<div class="flex border-b border-gray-200">
<button v-for="(tab, index) in ['个人资料', '我的活动', '报名记录']" :key="index"
class="text-base py-2 px-4 font-medium transition-all duration-200 focus:outline-none border-b-2"
:class="currentTab === index
? 'border-green-500 text-green-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
@click="currentTab = index" role="tab" :aria-selected="currentTab === index">
{{ tab }}
</button>
</div>
<!-- Tab content -->
<div class="pt-4">
<!-- 个人资料 -->
<div v-show="currentTab === 0" class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Left column -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">基本信息</h3>
<div class="space-y-4">
<!-- Name -->
<div>
<h4 class="text-sm font-medium text-gray-500">姓名</h4>
<p class="mt-1 text-gray-900">{{ currentUser?.name }}</p>
</div>
<!-- Location -->
<div>
<h4 class="text-sm font-medium text-gray-500">所在地</h4>
<p class="mt-1 text-gray-900">{{ currentUser?.location || '未设置' }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">性别</h4>
<p class="mt-1 text-gray-900">{{ currentUser.gender || '未设置' }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">年龄段</h4>
<p class="mt-1 text-gray-900">{{ currentUser.age_group || '未设置' }}</p>
</div>
<div>
<h4 class="text-sm font-medium text-gray-500">注册日期</h4>
<p class="mt-1 text-gray-900">
{{ formatRegistrationDate(currentUser.registration_date) }}
</p>
</div>
</div>
</div>
<!-- Right column -->
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">联系方式</h3>
<div class="space-y-4">
<!-- Email -->
<div>
<h4 class="text-sm font-medium text-gray-500">电子邮箱</h4>
<p class="mt-1 text-gray-900">{{ currentUser?.email }}</p>
</div>
<!-- Phone -->
<div>
<h4 class="text-sm font-medium text-gray-500">手机号码</h4>
<p class="mt-1 text-gray-900">{{ currentUser?.phone }}</p>
</div>
</div>
<h3 class="text-lg font-medium text-gray-900 mt-8 mb-4">个人简介</h3>
<p class="text-gray-700 whitespace-pre-wrap">
{{ currentUser.bio || '暂无个人简介' }}
</p>
</div>
</div>
<div class="mt-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">阅读兴趣</h3>
<div v-if="currentUser.interests && currentUser.interests.length > 0">
<div class="flex flex-wrap gap-2">
<span v-for="(interest, index) in currentUser.interests" :key="index"
class="bg-green-50 text-green-700 px-3 py-1 rounded-full text-sm">
{{ interest }}
</span>
</div>
</div>
<div v-else>
<p class="text-gray-500">暂无兴趣标签</p>
</div>
</div>
</div>
<!-- 我的活动 -->
<div v-show="currentTab === 1" class="p-6">
<div class="mb-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">
<span class="inline-block w-3 h-3 bg-green-500 rounded-full mr-2"></span>
<span>即将参加的活动</span>
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<ActivityCard v-for="activity in upcomingActivities" :key="activity.id"
:activity="activity" />
</div>
</div>
</div>
<!-- 报名记录 -->
<div v-show="currentTab === 2" class="p-6">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
活动信息</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
报名时间</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
状态</th>
<th
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<tr v-for="registration in userRegistrations" :key="registration.id">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">
{{activities.find(a => a.id ===
registration.activity_id)?.title || '未知活动'
}}
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900">
{{ formatRegistrationDate(registration.registration_time) }}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span :class="[
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
registration.status === 'approved' ? 'bg-green-100 text-green-800' :
registration.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
]">
{{ registration.status === 'approved' ? '已通过' :
registration.status === 'pending' ? '审核中' :
registration.status }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<router-link v-if="activities.find(a => a.id === registration.activity_id)" :to="`/activity/${registration.activity_id}`" class="text-green-600 hover:text-green-900 mr-4">
查看详情
</router-link>
<router-link v-if="registration.status === 'approved'" :to="`/check-in/${registration.activity_id}`" class="text-blue-600 hover:text-blue-900">
签到
</router-link>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Profile Modal -->
<Modal v-model:show="isEditModalOpen" title="编辑个人资料">
<Modal :isOpen="isEditModalOpen" title="编辑个人资料">
<div class="space-y-4">
<Input v-model="editForm.name" label="姓名" placeholder="请输入姓名" />
<Input v-model="editForm.email" label="电子邮箱" type="email" placeholder="请输入电子邮箱" />
......@@ -107,18 +265,25 @@ 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 Tabs from '../components/shared/Tabs.vue'
import ActivityCard from '../components/shared/ActivityCard.vue'
import Modal from '../components/shared/Modal.vue'
const store = useAppStore()
const { currentUser, activities, registrations } = store
import activitiesData from '../data/activities.json'
import registrationsData from '../data/registrations.json'
import usersData from '../data/users.json'
const activities = ref(activitiesData.activities)
const registrations = ref(registrationsData.registrations)
const currentUser = ref(usersData.users[0])
const userRegistrations = ref([])
const registeredActivities = ref([])
const pastActivities = ref([])
const upcomingActivities = ref([])
const isEditModalOpen = ref(false)
const currentTab = ref(0)
const editForm = ref({
name: '',
bio: '',
......@@ -173,276 +338,6 @@ const formatRegistrationDate = (dateString) => {
}).format(date)
}
// Profile tabs configuration
const profileTabs = computed(() => [
{
label: "个人资料",
content: {
component: 'div',
class: 'p-6',
children: [
// Basic Info section
{
component: 'div',
class: 'grid grid-cols-1 md:grid-cols-2 gap-8',
children: [
// Left column
{
component: 'div',
children: [
{
component: 'h3',
class: 'text-lg font-medium text-gray-900 mb-4',
text: '基本信息'
},
// User info fields
{
component: 'div',
class: 'space-y-4',
children: [
// Name
{
component: 'div',
children: [
{
component: 'h4',
class: 'text-sm font-medium text-gray-500',
text: '姓名'
},
{
component: 'p',
class: 'mt-1 text-gray-900',
text: currentUser.value?.name
}
]
},
// Location
{
component: 'div',
children: [
{
component: 'h4',
class: 'text-sm font-medium text-gray-500',
text: '所在地'
},
{
component: 'p',
class: 'mt-1 text-gray-900',
text: currentUser.value?.location || '未设置'
}
]
}
]
}
]
},
// Right column
{
component: 'div',
children: [
{
component: 'h3',
class: 'text-lg font-medium text-gray-900 mb-4',
text: '联系方式'
},
// Contact info
{
component: 'div',
class: 'space-y-4',
children: [
// Email
{
component: 'div',
children: [
{
component: 'h4',
class: 'text-sm font-medium text-gray-500',
text: '电子邮箱'
},
{
component: 'p',
class: 'mt-1 text-gray-900',
text: currentUser.value?.email
}
]
},
// Phone
{
component: 'div',
children: [
{
component: 'h4',
class: 'text-sm font-medium text-gray-500',
text: '手机号码'
},
{
component: 'p',
class: 'mt-1 text-gray-900',
text: currentUser.value?.phone
}
]
}
]
}
]
}
]
}
]
}
},
{
label: "我的活动",
content: {
component: 'div',
class: 'p-6',
children: [
// Upcoming activities section
{
component: 'div',
class: 'mb-8',
children: [
{
component: 'h3',
class: 'text-lg font-medium text-gray-900 mb-4',
children: [
{
component: 'span',
class: 'inline-block w-3 h-3 bg-green-500 rounded-full mr-2'
},
{
component: 'span',
text: '即将参加的活动'
}
]
},
// Activity cards
{
component: 'div',
class: 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6',
children: upcomingActivities.value.map(activity => ({
component: ActivityCard,
props: { activity }
}))
}
]
}
]
}
},
{
label: "报名记录",
content: {
component: 'div',
class: 'p-6',
children: [
{
component: 'div',
class: 'overflow-x-auto',
children: [
// Registration table
{
component: 'table',
class: 'min-w-full divide-y divide-gray-200',
children: [
// Table header
{
component: 'thead',
class: 'bg-gray-50',
children: [
{
component: 'tr',
children: [
{
component: 'th',
class: 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider',
text: '活动信息'
},
{
component: 'th',
class: 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider',
text: '报名时间'
},
{
component: 'th',
class: 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider',
text: '状态'
}
]
}
]
},
// Table body
{
component: 'tbody',
class: 'bg-white divide-y divide-gray-200',
children: userRegistrations.value.map(registration => ({
component: 'tr',
key: registration.id,
children: [
// Activity info
{
component: 'td',
class: 'px-6 py-4 whitespace-nowrap',
children: [
{
component: 'div',
class: 'flex items-center',
children: [
{
component: 'div',
class: 'text-sm font-medium text-gray-900',
text: activities.value.find(a => a.id === registration.activity_id)?.title || '未知活动'
}
]
}
]
},
// Registration time
{
component: 'td',
class: 'px-6 py-4 whitespace-nowrap',
children: [
{
component: 'div',
class: 'text-sm text-gray-900',
text: formatRegistrationDate(registration.registration_time)
}
]
},
// Status
{
component: 'td',
class: 'px-6 py-4 whitespace-nowrap',
children: [
{
component: 'span',
class: `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${registration.status === 'approved'
? 'bg-green-100 text-green-800'
: registration.status === 'pending'
? 'bg-yellow-100 text-yellow-800'
: 'bg-gray-100 text-gray-800'
}`,
text: registration.status === 'approved'
? '已通过'
: registration.status === 'pending'
? '审核中'
: registration.status
}
]
}
]
}))
}
]
}
]
}
]
}
}
])
// Lifecycle hooks
onMounted(() => {
if (currentUser.value) {
......@@ -456,6 +351,8 @@ 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)
......