Skip to content

Commit

Permalink
feat: improve ActivityDetail Page Style & Layout & Logics
Browse files Browse the repository at this point in the history
  • Loading branch information
1834423612 committed Feb 22, 2025
1 parent ceedd07 commit 1c6c55f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 53 deletions.
27 changes: 12 additions & 15 deletions src/components/ActivityCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@
<div
class="bg-white rounded-lg shadow-lg overflow-hidden cursor-pointer transition-all duration-300 hover:shadow-xl hover:-translate-y-1">
<div class="relative">
<div v-if="activity.activityPictures" class="h-48 overflow-hidden">
<div v-if="activity.activityPictures && isImageValid" class="h-48 overflow-hidden">
<img :src="activity.activityPictures"
:alt="truncateDescription(activity.activityName, 10)"
class="w-full h-full object-cover" />
class="w-full h-full object-cover"
@error="handleImageError" />
</div>
<div v-else :class="['h-48 flex items-center justify-center', gradientClass]">
<span class="text-4xl text-white">{{ truncateDescription(activity.activityName, 10) }}</span>
Expand Down Expand Up @@ -59,15 +60,18 @@
import { defineComponent, computed, defineProps, watch, ref } from 'vue';
import { Icon } from '@iconify/vue';
import config from 'config';
import { getGradientColor } from '../utils/gradientColors';
const gradients = {
blue: 'bg-gradient-to-br from-blue-400 to-indigo-600',
green: 'bg-gradient-to-br from-green-400 to-emerald-600',
purple: 'bg-gradient-to-br from-purple-400 to-fuchsia-600',
orange: 'bg-gradient-to-br from-orange-400 to-red-600',
teal: 'bg-gradient-to-br from-teal-400 to-cyan-600',
const isImageValid = ref(true);
const handleImageError = () => {
isImageValid.value = false;
};
const gradientClass = computed(() => {
return getGradientColor(props.activity);
});
defineComponent(['Icon']);
const props = defineProps({
Expand Down Expand Up @@ -109,13 +113,6 @@ watch(() => props.serviceLocation, (newValue) => {
}, { deep: true, immediate: true });
const baseURL = config.baseURL;
const gradientClass = computed(() => {
if (props.activity.gradientColor && gradients[props.activity.gradientColor]) {
return gradients[props.activity.gradientColor];
}
const colors = Object.values(gradients);
return colors[Math.floor(Math.random() * colors.length)];
});
const getStatusClass = (status) => {
switch (status) {
Expand Down
192 changes: 154 additions & 38 deletions src/components/ActivityDetail.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
<template>
<div class="max-w-4xl mx-auto px-4 py-6 sm:px-6 lg:px-8">
<div class="container mx-auto px-4 py-6 sm:px-6 lg:px-8">
<div class="bg-white rounded-xl shadow-lg overflow-hidden" v-loading="loading" :element-loading-svg="svg">
<!-- Activity Image/Header Section -->
<div class="relative">
<div v-if="tempActivity?.activityPictures" class="h-64 sm:h-96 overflow-hidden">
<div v-if="tempActivity?.activityPictures && isImageValid" class="h-64 sm:h-96 overflow-hidden">
<img
:src="tempActivity.activityPictures"
:alt="truncateDescription(tempActivity.activityName, 10)"
class="w-full h-full object-cover"
@error="handleImageError"
/>
</div>
<div v-else class="h-64 sm:h-96 flex items-center justify-center bg-gradient-to-br from-blue-500 to-indigo-600">
<div v-else
class="h-64 sm:h-96 flex items-center justify-center"
:class="gradientClass">
<span class="text-3xl sm:text-4xl text-white font-bold px-6 text-center">
{{ truncateDescription(tempActivity.activityName, 20) }}
</span>
Expand Down Expand Up @@ -47,93 +50,128 @@
<Icon icon="mdi:account-group" class="w-6 h-6 text-blue-600" />
</div>
<div>
<div class="text-sm text-gray-500">招募人数</div>
<div class="text-sm text-gray-500">{{ $t('page.activityDetail.recruitmentStatus') }}</div>
<div class="text-lg font-semibold text-gray-900">
{{ tempActivity.recruitedNumber }} / {{ tempActivity.recruitNumber }}
</div>
</div>
</div>

<!-- Activity Date -->
<!-- Activity Duration -->
<div class="flex items-center space-x-3 p-4 bg-gray-50 rounded-lg">
<div class="p-2 bg-green-100 rounded-lg">
<Icon icon="mdi:calendar" class="w-6 h-6 text-green-600" />
<Icon icon="mdi:clock-outline" class="w-6 h-6 text-green-600" />
</div>
<div>
<div class="text-sm text-gray-500">活动日期</div>
<div class="text-sm text-gray-500">{{ $t('page.activityDetail.activityDuration') }}</div>
<div class="text-lg font-semibold text-gray-900">
{{ tempActivity.startTime }}
{{ tempActivity.duration }} {{ $t('page.activityDetail.hours') }}
</div>
</div>
</div>

<!-- Location -->
<!-- Activity Date -->
<div class="flex items-center space-x-3 p-4 bg-gray-50 rounded-lg sm:col-span-2">
<div class="p-2 bg-purple-100 rounded-lg">
<Icon icon="mdi:map-marker" class="w-6 h-6 text-purple-600" />
<Icon icon="mdi:calendar" class="w-6 h-6 text-purple-600" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm text-gray-500">活动地址</div>
<div class="text-lg font-semibold text-gray-900 truncate">
{{ tempActivity.address }}
<div>
<div class="text-sm text-gray-500">{{ $t('page.activityDetail.activityDate') }}</div>
<div class="text-lg font-semibold text-gray-900">
{{ tempActivity.startTime }}
</div>
</div>
</div>
</div>

<!-- Location Information -->
<div class="bg-gray-50 rounded-lg p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ $t('page.activityDetail.activityAddress') }}</h2>
<div class="flex items-center justify-between mb-2">
<p class="text-gray-700 break-words">{{ tempActivity.address }}</p>
<el-button type="primary" size="small" @click="copyText(tempActivity.address)">
{{ $t('page.activityDetail.copyAddress') }}
</el-button>
</div>
<div class="mt-4">
<el-button type="primary" size="small" @click="toggleMap">
{{ showMap ? $t('page.activityDetail.hideMap') : $t('page.activityDetail.showMap') }}
</el-button>
</div>
<transition name="fade">
<div v-if="showMap" class="mt-4">
<div v-if="mapUrl" class="w-full h-64 rounded-lg overflow-hidden">
<iframe :src="mapUrl"
width="100%"
height="100%"
frameborder="0"
style="border:0;"
allowfullscreen=""
loading="lazy"></iframe>
</div>
<div v-else class="w-full h-64 bg-gray-200 rounded-lg flex items-center justify-center">
<p class="text-gray-500">{{ $t('page.activityDetail.mapUnavailable') }}</p>
</div>
</div>
</transition>
</div>

<!-- Organizer Information -->
<div class="bg-gray-50 rounded-lg p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">主办方信息</h2>
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ $t('page.activityDetail.organizerInfo') }}</h2>
<div class="space-y-3">
<div class="flex items-center">
<Icon icon="mdi:account" class="w-5 h-5 text-gray-400 mr-3" />
<span class="text-sm text-gray-500 w-20">举办者:</span>
<span class="text-sm text-gray-900">{{ tempActivity?.dept?.leader }}</span>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon icon="mdi:account" class="w-5 h-5 text-gray-400 mr-3" />
<span class="text-sm text-gray-500 w-20">{{ $t('page.activityDetail.organizer') }}:</span>
<span class="text-sm text-gray-900">{{ tempActivity?.dept?.leader }}</span>
</div>
</div>
<div class="flex items-center">
<Icon icon="mdi:email" class="w-5 h-5 text-gray-400 mr-3" />
<span class="text-sm text-gray-500 w-20">邮箱:</span>
<span
class="text-sm text-blue-600 cursor-pointer hover:text-blue-800"
@click="copyCurrentUrl(tempActivity?.dept?.email)"
>
{{ tempActivity?.dept?.email }}
</span>
<div class="flex items-center justify-between">
<div class="flex items-center">
<Icon icon="mdi:email" class="w-5 h-5 text-gray-400 mr-3" />
<span class="text-sm text-gray-500 w-20">{{ $t('page.activityDetail.email') }}:</span>
<span class="text-sm text-blue-600">{{ tempActivity?.dept?.email }}</span>
</div>
<el-button type="primary" size="small" @click="copyText(tempActivity?.dept?.email)">
{{ $t('page.activityDetail.copyEmail') }}
</el-button>
</div>
</div>
</div>

<!-- Activity Details -->
<div class="mb-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4">活动详情</h2>
<h2 class="text-lg font-semibold text-gray-900 mb-4">{{ $t('page.activityDetail.activityDetails') }}</h2>
<div class="bg-gray-50 rounded-lg p-6">
<p class="text-gray-700 whitespace-pre-line">{{ tempActivity.details }}</p>
</div>
</div>

<!-- Submit Button -->
<!-- Submit Button hover:scale-[1.02] -->
<el-button
:loading="loading"
type="primary"
class="w-full py-3 px-4 text-base font-medium transition-all duration-300 hover:scale-[1.02]"
class="w-full py-5 px-4 text-base font-medium transition-all duration-300"
@click.prevent="handleSubmit"
>
{{ loading ? '报名中...' : '立即报名' }}
{{ loading ? $t('page.activityDetail.signingUp') : $t('page.activityDetail.signUpNow') }}
</el-button>
</div>
</div>
</div>
</template>

<script setup>
import { defineProps, ref, watch, onMounted, defineComponent } from 'vue';
import { defineProps, ref, watch, onMounted, defineComponent, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { signUpActivity, getActivityDetailById } from '../api/openness/openness';
import { getToken } from '../utils/auth';
import { copyCurrentUrl } from '../utils/ruoyi';
import { Icon } from '@iconify/vue';
import { useRouter, useRoute } from 'vue-router';
import config from 'config';
import { getGradientColor } from '../utils/gradientColors';
defineComponent(['Icon']);
Expand All @@ -143,6 +181,7 @@ const baseURL = config.baseURL;
const tempActivity = ref({});
const loading = ref(false);
const showMap = ref(false);
const svg = `
<path class="path" d="
Expand Down Expand Up @@ -177,6 +216,28 @@ watch(() => props.serviceLocation, (newValue) => {
serviceLocations.value = newValue;
}, { deep: true, immediate: true });
const isImageValid = ref(true);
const handleImageError = () => {
isImageValid.value = false;
};
const gradientClass = computed(() => {
return getGradientColor(tempActivity.value);
});
// Google Map URL
// [Note] If you see this code
// Don't try this API Key, it's won't work in your environment, you need to use your own API Key, and it's free
// [Reference] https://developers.google.com/maps/documentation/embed/get-api-key
// [Google Web Console] https://console.cloud.google.com/google/maps-apis/
const mapUrl = computed(() => {
if (tempActivity.value.address) {
return `https://www.google.com/maps/embed/v1/place?key=AIzaSyCZ8Eopy3VZwfjMzw1q15NQh99CqbDjV8Q&q=${encodeURIComponent(tempActivity.value.address)}`;
}
return null;
});
const truncateDescription = (text, maxLength = 100) => {
if (!text) {
return '';
Expand All @@ -195,26 +256,44 @@ const handleSubmit = async () => {
const res = await signUpActivity(tempActivity.value.activityId);
if (res.code === 200) {
ElMessage({
message: '报名成功',
message: $t('page.activityDetail.signUpSuccess'),
type: 'success'
});
router.push('/home');
} else {
ElMessage({
message: res.msg || '报名失败',
message: res.msg || $t('page.activityDetail.signUpFailed'),
type: 'error'
});
}
} catch (error) {
ElMessage({
message: '报名失败',
message: $t('page.activityDetail.signUpFailed'),
type: 'error'
});
} finally {
loading.value = false;
}
};
const copyText = (text) => {
navigator.clipboard.writeText(text).then(() => {
ElMessage({
message: $t('page.activityDetail.copySuccess'),
type: 'success'
});
}, () => {
ElMessage({
message: $t('page.activityDetail.copyFailed'),
type: 'error'
});
});
};
const toggleMap = () => {
showMap.value = !showMap.value;
};
onMounted(async () => {
loading.value = true;
try {
Expand All @@ -224,13 +303,13 @@ onMounted(async () => {
tempActivity.value = res.data;
} else {
ElMessage({
message: res.msg || '获取活动详情失败',
message: res.msg || $t('page.activityDetail.fetchFailed'),
type: 'error'
});
}
} catch (error) {
ElMessage({
message: '获取活动详情失败',
message: $t('page.activityDetail.fetchFailed'),
type: 'error'
});
} finally {
Expand All @@ -240,6 +319,35 @@ onMounted(async () => {
</script>

<style scoped>
/* 添加容器响应式宽度控制 */
.container {
max-width: 100%;
}
@media (min-width: 640px) {
.container {
max-width: 640px;
}
}
@media (min-width: 768px) {
.container {
max-width: 768px;
}
}
@media (min-width: 1024px) {
.container {
max-width: 1024px;
}
}
@media (min-width: 1280px) {
.container {
max-width: 1440px;
}
}
.el-button--primary {
background-color: #3b82f6;
border-color: #3b82f6;
Expand All @@ -249,4 +357,12 @@ onMounted(async () => {
background-color: #2563eb;
border-color: #2563eb;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
Loading

0 comments on commit 1c6c55f

Please sign in to comment.