Files
VRCX/src/views/Tools/components/GroupCalendarEventCard.vue
2025-11-08 00:34:09 +11:00

298 lines
10 KiB
Vue

<template>
<el-card :body-style="{ padding: '0px' }" class="event-card" :class="cardClass">
<img :src="bannerUrl" class="banner" />
<div class="event-content">
<div class="event-title">
<div v-if="showGroupName" class="event-group-name" @click="onGroupClick">
{{ groupName }}
</div>
<el-popover placement="right" :width="500" trigger="hover">
<el-descriptions :title="event.title" size="small" :column="2" class="event-title-popover">
<template #extra>
<div>
{{ formatTimeRange(event.startsAt, event.endsAt) }}
</div>
</template>
<el-descriptions-item>
<el-button type="default" :icon="Calendar" size="small" @click="openCalendarEvent(event)">{{
t('dialog.group_calendar.event_card.export_to_calendar')
}}</el-button>
</el-descriptions-item>
<el-descriptions-item>
<el-button type="default" :icon="Download" size="small" @click="downloadEventIcs(event)">{{
t('dialog.group_calendar.event_card.download_ics')
}}</el-button>
</el-descriptions-item>
<el-descriptions-item :label="t('dialog.group_calendar.event_card.category')">
{{ capitalizeFirst(event.category) }}
</el-descriptions-item>
<el-descriptions-item :label="t('dialog.group_calendar.event_card.interested_user')">
{{ event.interestedUserCount }}
</el-descriptions-item>
<el-descriptions-item :label="t('dialog.group_calendar.event_card.close_time')">
{{ event.closeInstanceAfterEndMinutes + ' min' }}
</el-descriptions-item>
<el-descriptions-item :label="t('dialog.group_calendar.event_card.created')">
{{ formatDateFilter(event.createdAt, 'long') }}
</el-descriptions-item>
<el-descriptions-item :label="t('dialog.group_calendar.event_card.description')">
{{ event.description }}
</el-descriptions-item>
</el-descriptions>
<template #reference>
<div class="event-title-content" @click="onGroupClick">
{{ event.title }}
</div>
</template>
</el-popover>
</div>
<div class="event-info">
<div :class="timeClass">
{{ formattedTime }}
</div>
<div>
{{ capitalizeFirst(event.accessType) }}
</div>
</div>
</div>
<div v-if="isFollowing" @click="toggleEventFollow(event)" class="following-badge is-following">
<el-icon><Star /></el-icon>
</div>
<div v-else @click="toggleEventFollow(event)" class="following-badge">
<el-icon><StarFilled /></el-icon>
</div>
</el-card>
</template>
<script setup>
import { Calendar, Download, Star, StarFilled } from '@element-plus/icons-vue';
import { computed, defineEmits } from 'vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { AppDebug } from '../../../service/appConfig';
import { formatDateFilter } from '../../../shared/utils';
import { groupRequest } from '../../../api';
import { useGroupStore } from '../../../stores';
const { t } = useI18n();
const { cachedGroups, showGroupDialog } = useGroupStore();
const props = defineProps({
event: {
type: Object,
required: true
},
mode: {
type: String,
required: true,
// @ts-ignore
validator: (value) => ['timeline', 'grid'].includes(value)
},
isFollowing: {
type: Boolean,
default: false
},
cardClass: {
type: [String, Object, Array],
default: ''
}
});
const emit = defineEmits(['update-following-calendar-data']);
const showGroupName = computed(() => props.mode === 'timeline');
const timeClass = computed(() => (props.mode === 'grid' ? 'event-time' : ''));
const bannerUrl = computed(() => {
if (!props.event) return '';
if (props.event.imageUrl) {
return props.event.imageUrl;
} else {
return cachedGroups.get(props.event.ownerId)?.bannerUrl || '';
}
});
const groupName = computed(() => {
if (!props.event) return '';
return cachedGroups.get(props.event.ownerId)?.name || '';
});
const formattedTime = computed(() => {
if (props.mode === 'timeline') {
return formatTimeRange(props.event.startsAt, props.event.endsAt);
} else {
return `${formatDateFilter(props.event.startsAt, 'short')} - ${formatDateFilter(props.event.endsAt, 'short')}`;
}
});
async function openCalendarEvent(event) {
const content = await getCalendarIcs(event);
if (!content) return;
await AppApi.OpenCalendarFile(content);
}
async function getCalendarIcs(event) {
const url = `${AppDebug.endpointDomain}/calendar/${event.ownerId}/${event.id}.ics`;
try {
const response = await webApiService.execute({
url,
method: 'GET'
});
if (response.status !== 200) {
throw new Error(`Error: ${response.data}`);
}
return response.data;
} catch (error) {
ElMessage({
message: `Failed to download .ics file, ${error.message}`,
type: 'error'
});
console.error('Failed to download .ics file:', error);
}
}
async function downloadEventIcs(event) {
const content = await getCalendarIcs(event);
if (!content) return;
const blob = new Blob([content], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = `${event.id}.ics`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
}
async function toggleEventFollow(event) {
const args = await groupRequest.followGroupEvent({
groupId: event.ownerId,
eventId: event.id,
isFollowing: !props.isFollowing
});
emit('update-following-calendar-data', args.json);
}
const formatTimeRange = (startsAt, endsAt) =>
`${formatDateFilter(startsAt, 'time')} - ${formatDateFilter(endsAt, 'time')}`;
const capitalizeFirst = (str) => str?.charAt(0).toUpperCase() + str?.slice(1);
const onGroupClick = () => {
showGroupDialog(props.event.ownerId);
};
</script>
<style lang="scss" scoped>
.event-card {
transition: all 0.3s ease;
position: relative;
overflow: visible;
border-radius: 8px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.grouped-card {
margin-bottom: 0;
}
&.grid-card {
flex: 0 0 280px;
max-width: 280px;
}
:deep(.el-card__body) {
overflow: visible;
}
.banner {
width: 100%;
object-fit: cover;
border-radius: 8px 8px 0 0;
.timeline-view & {
height: 125px;
}
.grid-view & {
height: 100px;
}
}
.following-badge {
position: absolute;
top: -8px;
right: -9px;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: var(--el-text-color-regular);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 10;
cursor: pointer;
}
.is-following {
background-color: var(--group-calendar-badge-following, #67c23a);
}
.event-content {
font-size: 12px;
.timeline-view & {
padding: 4px 12px 12px 12px;
}
.grid-view & {
padding: 8px 12px 12px 12px;
}
.event-title {
display: flex;
flex-direction: column;
.grid-view & {
margin-bottom: 8px;
}
.event-group-name {
cursor: pointer;
.grid-view & {
display: none;
}
}
.event-title-content {
font-size: 14px;
font-weight: bold;
line-height: 1.2;
cursor: pointer;
.timeline-view & {
margin-bottom: 2px;
}
&:hover {
color: var(--el-color-primary);
}
}
}
.event-info {
display: flex;
justify-content: space-between;
align-items: center;
.timeline-view & > :first-child {
font-size: 14px;
}
.grid-view & {
font-size: 11px;
color: var(--el-text-color-regular);
}
.event-time {
font-weight: 500;
color: var(--el-color-primary);
}
}
}
}
:deep(.el-card) {
border-radius: 8px;
width: 100%;
overflow: visible;
}
</style>