mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-04 13:56:07 +02:00
replace element plus components
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
<MutualFriends />
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
|
||||
<BackToTop target="#chart" :right="30" :bottom="30" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import BackToTop from '@/components/BackToTop.vue';
|
||||
|
||||
import { useChartsStore } from '../../stores';
|
||||
|
||||
const InstanceActivity = defineAsyncComponent(() => import('./components/InstanceActivity.vue'));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div ref="instanceActivityRef" class="pt-12">
|
||||
<BackToTop :target="instanceActivityRef" :right="30" :bottom="30" :teleport="false" />
|
||||
<div class="options-container instance-activity" style="margin-top: 0">
|
||||
<div>
|
||||
<span>{{ t('view.charts.instance_activity.header') }}</span>
|
||||
@@ -171,6 +172,7 @@
|
||||
import { toDate } from 'reka-ui/date';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import BackToTop from '@/components/BackToTop.vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../../components/ui/popover';
|
||||
@@ -204,7 +206,7 @@
|
||||
|
||||
function setInstanceActivityHeight() {
|
||||
if (instanceActivityRef.value) {
|
||||
const availableHeight = window.innerHeight - 100;
|
||||
const availableHeight = window.innerHeight - 110;
|
||||
instanceActivityRef.value.style.height = `${availableHeight}px`;
|
||||
instanceActivityRef.value.style.overflowY = 'auto';
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
<DropdownMenuSubContent
|
||||
side="right"
|
||||
align="start"
|
||||
class="w-[180px] p-1 rounded-lg">
|
||||
class="w-45 p-1 rounded-lg">
|
||||
<div class="group-visibility-menu">
|
||||
<button
|
||||
v-for="visibility in avatarGroupVisibilityOptions"
|
||||
@@ -476,7 +476,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="activeLocalGroupName">
|
||||
<el-scrollbar
|
||||
<ScrollArea
|
||||
ref="localAvatarScrollbarRef"
|
||||
class="favorites-content__scroll"
|
||||
@scroll="handleLocalAvatarScroll">
|
||||
@@ -495,7 +495,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="favorites-empty">No Data</div>
|
||||
</el-scrollbar>
|
||||
</ScrollArea>
|
||||
</template>
|
||||
<template v-else-if="isHistorySelected">
|
||||
<div class="favorites-content__scroll favorites-content__scroll--native">
|
||||
@@ -529,6 +529,7 @@
|
||||
import { ArrowUpDown, Check, Ellipsis, Loader, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
||||
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
@@ -1101,7 +1102,7 @@
|
||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||
return;
|
||||
}
|
||||
const wrap = localAvatarScrollbarRef.value?.wrapRef;
|
||||
const wrap = localAvatarScrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -1135,7 +1136,7 @@
|
||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||
return;
|
||||
}
|
||||
const wrap = localAvatarScrollbarRef.value?.wrapRef;
|
||||
const wrap = localAvatarScrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@
|
||||
</template>
|
||||
<div v-else class="favorites-empty">No Data</div>
|
||||
</div>
|
||||
<el-scrollbar
|
||||
<ScrollArea
|
||||
v-else-if="activeLocalGroupName && isLocalGroupSelected"
|
||||
ref="localFavoritesScrollbarRef"
|
||||
class="favorites-content__scroll"
|
||||
@@ -424,7 +424,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="favorites-empty">No Data</div>
|
||||
</el-scrollbar>
|
||||
</ScrollArea>
|
||||
<div v-else class="favorites-empty">No Data</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -440,6 +440,7 @@
|
||||
import { ArrowUpDown, Ellipsis, MoreHorizontal, Plus, RefreshCcw, RefreshCw } from 'lucide-vue-next';
|
||||
import { InputGroupField, InputGroupSearch } from '@/components/ui/input-group';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
@@ -1004,7 +1005,7 @@
|
||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||
return;
|
||||
}
|
||||
const wrap = localFavoritesScrollbarRef.value?.wrapRef;
|
||||
const wrap = localFavoritesScrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -1097,7 +1098,7 @@
|
||||
if (!isLocalGroupSelected.value || isSearchActive.value) {
|
||||
return;
|
||||
}
|
||||
const wrap = localFavoritesScrollbarRef.value?.wrapRef;
|
||||
const wrap = localFavoritesScrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
:rows="10"
|
||||
style="margin-top: 10px"
|
||||
input-class="resize-none" />
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 5px">
|
||||
<div>
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Select
|
||||
:model-value="worldImportFavoriteGroupSelection"
|
||||
|
||||
@@ -306,6 +306,7 @@ export const columns = [
|
||||
location={original.location}
|
||||
hint={original.worldName}
|
||||
grouphint={original.groupName}
|
||||
disableTooltip
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -318,6 +319,7 @@ export const columns = [
|
||||
location={original.location}
|
||||
hint={original.worldName}
|
||||
grouphint={original.groupName}
|
||||
disableTooltip
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -350,7 +352,7 @@ export const columns = [
|
||||
}
|
||||
|
||||
return (
|
||||
<span class="block w-full min-w-0 truncate">
|
||||
<div class="block w-full min-w-0 truncate">
|
||||
<i
|
||||
class={[
|
||||
'x-user-status',
|
||||
@@ -359,7 +361,7 @@ export const columns = [
|
||||
]}
|
||||
></i>
|
||||
<span>{original.statusDescription}</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -379,13 +381,9 @@ export const columns = [
|
||||
|
||||
if (type === 'Bio') {
|
||||
return (
|
||||
<span
|
||||
class="block w-full min-w-0 truncate"
|
||||
innerHTML={formatDifference(
|
||||
original.previousBio,
|
||||
original.bio
|
||||
)}
|
||||
></span>
|
||||
<div class="block w-full min-w-0 truncate">
|
||||
{original.bio}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div v-else class="friend-view__toolbar friend-view__toolbar--loading">
|
||||
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
|
||||
</div>
|
||||
<el-scrollbar v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
|
||||
<ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
|
||||
<template v-if="isSameInstanceView">
|
||||
<div v-if="visibleSameInstanceGroups.length" class="friend-view__instances">
|
||||
<section
|
||||
@@ -151,7 +151,7 @@
|
||||
<Loader2 class="friend-view__loading-icon" :size="18" />
|
||||
<span>{{ t('view.friends_locations.loading_more') }}</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</ScrollArea>
|
||||
<div v-else class="friend-view__initial-loading">
|
||||
<Loader2 class="friend-view__loading-icon" :size="22" />
|
||||
</div>
|
||||
@@ -164,6 +164,7 @@
|
||||
import { Loader2, Settings } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InputGroupSearch } from '@/components/ui/input-group';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@@ -262,7 +263,7 @@
|
||||
let cleanupResize;
|
||||
|
||||
const updateGridWidth = () => {
|
||||
const wrap = scrollbarRef.value?.wrapRef;
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -276,7 +277,7 @@
|
||||
cleanupResize = undefined;
|
||||
}
|
||||
|
||||
const wrap = scrollbarRef.value?.wrapRef;
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -559,7 +560,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = scrollbarRef.value?.wrapRef;
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
|
||||
if (!wrap) {
|
||||
return;
|
||||
@@ -590,7 +591,7 @@
|
||||
|
||||
function maybeFillViewport() {
|
||||
nextTick(() => {
|
||||
const wrap = scrollbarRef.value?.wrapRef;
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -634,7 +635,6 @@
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
scrollbarRef.value?.update?.();
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
});
|
||||
@@ -697,7 +697,6 @@
|
||||
settingsReady.value = true;
|
||||
nextTick(() => {
|
||||
setupResizeHandling();
|
||||
scrollbarRef.value?.update?.();
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
});
|
||||
|
||||
@@ -352,66 +352,52 @@
|
||||
@change="updateTrustColor('', '', true)"></simple-switch>
|
||||
<div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.untrusted"
|
||||
size="small"
|
||||
:predefine="['#CCCCCC']"
|
||||
@change="updateTrustColor('untrusted', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#CCCCCC']"
|
||||
@change="updateTrustColor('untrusted', $event)" />
|
||||
<span class="color-picker x-tag-untrusted">Visitor</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.basic"
|
||||
size="small"
|
||||
:predefine="['#1778ff']"
|
||||
@change="updateTrustColor('basic', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#1778ff']"
|
||||
@change="updateTrustColor('basic', $event)" />
|
||||
<span class="color-picker x-tag-basic">New User</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.known"
|
||||
size="small"
|
||||
:predefine="['#2bcf5c']"
|
||||
@change="updateTrustColor('known', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#2bcf5c']"
|
||||
@change="updateTrustColor('known', $event)" />
|
||||
<span class="color-picker x-tag-known">User</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.trusted"
|
||||
size="small"
|
||||
:predefine="['#ff7b42']"
|
||||
@change="updateTrustColor('trusted', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#ff7b42']"
|
||||
@change="updateTrustColor('trusted', $event)" />
|
||||
<span class="color-picker x-tag-trusted">Known User</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.veteran"
|
||||
size="small"
|
||||
:predefine="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']"
|
||||
@change="updateTrustColor('veteran', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#b18fff', '#8143e6', '#ff69b4', '#b52626', '#ffd000', '#abcdef']"
|
||||
@change="updateTrustColor('veteran', $event)" />
|
||||
<span class="color-picker x-tag-veteran">Trusted User</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.vip"
|
||||
size="small"
|
||||
:predefine="['#ff2626']"
|
||||
@change="updateTrustColor('vip', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#ff2626']"
|
||||
@change="updateTrustColor('vip', $event)" />
|
||||
<span class="color-picker x-tag-vip">VRChat Team</span>
|
||||
</div>
|
||||
<div>
|
||||
<el-color-picker
|
||||
<PresetColorPicker
|
||||
:model-value="trustColor.troll"
|
||||
size="small"
|
||||
:predefine="['#782f2f']"
|
||||
@change="updateTrustColor('troll', $event)">
|
||||
</el-color-picker>
|
||||
:presets="['#782f2f']"
|
||||
@change="updateTrustColor('troll', $event)" />
|
||||
<span class="color-picker x-tag-troll">Nuisance</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -445,6 +431,8 @@
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import PresetColorPicker from '@/components/PresetColorPicker.vue';
|
||||
|
||||
import { useAppearanceSettingsStore, useFavoriteStore, useVrStore } from '../../../../stores';
|
||||
import { getLanguageName, languageCodes } from '../../../../localization';
|
||||
import { THEME_CONFIG } from '../../../../shared/constants';
|
||||
|
||||
@@ -4,10 +4,7 @@
|
||||
<div style="flex: 1; padding: 10px; padding-left: 0">
|
||||
<Popover v-model:open="isQuickSearchOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Input
|
||||
v-model="quickSearchQuery"
|
||||
:placeholder="t('side_panel.search_placeholder')"
|
||||
@focus="handleQuickSearchFocus" />
|
||||
<Input v-model="quickSearchQuery" :placeholder="t('side_panel.search_placeholder')" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="bottom"
|
||||
@@ -78,6 +75,7 @@
|
||||
:items="sidebarTabs"
|
||||
:unmount-on-hide="false"
|
||||
variant="equal"
|
||||
fill
|
||||
class="zero-margin-tabs"
|
||||
style="height: calc(100% - 70px); margin-top: 5px">
|
||||
<template #label-friends>
|
||||
@@ -89,14 +87,19 @@
|
||||
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
|
||||
</template>
|
||||
<template #friends>
|
||||
<div class="el-tabs__content">
|
||||
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
|
||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||
<div class="h-full overflow-hidden">
|
||||
<ScrollArea ref="friendsScrollAreaRef" class="h-full">
|
||||
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
|
||||
</ScrollArea>
|
||||
<BackToTop :target="friendsScrollTarget" :bottom="20" :right="20" :teleport="false" />
|
||||
</div>
|
||||
</template>
|
||||
<template #groups>
|
||||
<div class="el-tabs__content">
|
||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||
<div class="h-full overflow-hidden">
|
||||
<ScrollArea ref="groupsScrollAreaRef" class="h-full">
|
||||
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
|
||||
</ScrollArea>
|
||||
<BackToTop :target="groupsScrollTarget" :bottom="20" :right="20" :teleport="false" />
|
||||
</div>
|
||||
</template>
|
||||
</TabsUnderline>
|
||||
@@ -104,16 +107,19 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RefreshCw } from 'lucide-vue-next';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { TabsUnderline } from '@/components/ui/tabs';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import BackToTop from '@/components/BackToTop.vue';
|
||||
|
||||
import { useFriendStore, useGroupStore, useSearchStore } from '../../stores';
|
||||
import { userImage } from '../../shared/utils';
|
||||
|
||||
@@ -134,6 +140,25 @@
|
||||
const quickSearchQuery = ref('');
|
||||
const isQuickSearchOpen = ref(false);
|
||||
|
||||
const friendsScrollAreaRef = ref(null);
|
||||
const groupsScrollAreaRef = ref(null);
|
||||
const friendsScrollTarget = ref(null);
|
||||
const groupsScrollTarget = ref(null);
|
||||
|
||||
function resolveScrollViewport(scrollAreaComponentRef) {
|
||||
// Our ScrollArea renders a DOM element root; the viewport is marked by data-slot.
|
||||
const rootEl = scrollAreaComponentRef?.$el ?? null;
|
||||
if (!rootEl || typeof rootEl.querySelector !== 'function') return null;
|
||||
return rootEl.querySelector('[data-slot="scroll-area-viewport"]');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// Ensure child components are mounted before querying their DOM.
|
||||
await nextTick();
|
||||
friendsScrollTarget.value = resolveScrollViewport(friendsScrollAreaRef.value);
|
||||
groupsScrollTarget.value = resolveScrollViewport(groupsScrollAreaRef.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
quickSearchQuery,
|
||||
(value) => {
|
||||
@@ -142,11 +167,6 @@
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function handleQuickSearchFocus() {
|
||||
isQuickSearchOpen.value = true;
|
||||
quickSearchRemoteMethod(String(quickSearchQuery.value ?? ''));
|
||||
}
|
||||
|
||||
function handleQuickSearchSelect(value) {
|
||||
if (!value) {
|
||||
return;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<div v-else class="skeleton" aria-busy="true" aria-label="Loading">
|
||||
<!-- <div v-else class="skeleton" aria-busy="true" aria-label="Loading">
|
||||
<div>
|
||||
<Skeleton class="h-10 w-10 rounded-full" />
|
||||
<div>
|
||||
@@ -53,7 +53,7 @@
|
||||
<Skeleton class="mt-1.5 h-3 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -42,11 +42,7 @@
|
||||
|
||||
const timeZone = getLocalTimeZone();
|
||||
|
||||
// JSDoc casts: this project can end up with nominal-type mismatches for DateValue
|
||||
// due to duplicate @internationalized/date copies in tooling.
|
||||
/** @type {import('vue').Ref<any>} */
|
||||
const internalValue = ref(fromDate(props.modelValue ?? new Date(), timeZone));
|
||||
/** @type {import('vue').Ref<any>} */
|
||||
const placeholder = ref(fromDate(props.modelValue ?? new Date(), timeZone));
|
||||
|
||||
watch(
|
||||
@@ -147,6 +143,10 @@
|
||||
:class="hasFollowingFor(weekDate) ? 'has-following' : 'no-following'">
|
||||
{{ eventCountFor(weekDate) }}
|
||||
</div>
|
||||
<!-- <div
|
||||
v-if="eventCountFor(weekDate) > 0"
|
||||
class="calendar-event-dot"
|
||||
aria-hidden="true" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</CalendarCellTrigger>
|
||||
@@ -165,7 +165,6 @@
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.date {
|
||||
@@ -193,21 +192,20 @@
|
||||
|
||||
.calendar-event-badge {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
min-width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 9px;
|
||||
color: var(--el-color-white, #fff);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: bold;
|
||||
font-size: 10px;
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
z-index: 10;
|
||||
padding: 0 5px;
|
||||
line-height: 18px;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.calendar-event-badge.has-following {
|
||||
@@ -217,4 +215,18 @@
|
||||
.calendar-event-badge.no-following {
|
||||
background-color: var(--group-calendar-badge-normal, var(--el-color-primary));
|
||||
}
|
||||
|
||||
.calendar-event-dot {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 4px;
|
||||
transform: translateX(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--group-calendar-event-dot, #ef4444);
|
||||
box-shadow: var(--el-box-shadow-lighter);
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
|
||||
<DialogContent class="x-dialog w-[90vw] max-w-[90vw] sm:max-w-[70vw] h-[60vh] overflow-hidden">
|
||||
<DialogContent class="x-dialog sm:max-w-[50vw] h-[60vh] overflow-hidden">
|
||||
<DialogHeader>
|
||||
<div class="dialog-title-container">
|
||||
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
|
||||
@@ -18,82 +18,77 @@
|
||||
</div>
|
||||
</DialogHeader>
|
||||
<div class="top-content">
|
||||
<transition name="el-fade-in-linear" mode="out-in">
|
||||
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
|
||||
<div class="timeline-container">
|
||||
<el-timeline v-if="groupedTimelineEvents.length">
|
||||
<el-timeline-item
|
||||
v-for="(timeGroup, key) of groupedTimelineEvents"
|
||||
:key="key"
|
||||
:timestamp="
|
||||
dayjs(timeGroup.startsAt).format('MM-DD ddd') + ' ' + timeGroup.startTime
|
||||
"
|
||||
placement="top">
|
||||
<div class="time-group-container">
|
||||
<GroupCalendarEventCard
|
||||
v-for="value in timeGroup.events"
|
||||
:key="value.id"
|
||||
:event="value"
|
||||
mode="timeline"
|
||||
:is-following="isEventFollowing(value.id)"
|
||||
:card-class="{ 'grouped-card': timeGroup.events.length > 1 }"
|
||||
@update-following-calendar-data="updateFollowingCalendarData"
|
||||
@click-action="showGroupDialog(value.ownerId)" />
|
||||
</div>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
<div v-else>{{ t('dialog.group_calendar.no_events') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-container">
|
||||
<GroupCalendarMonth
|
||||
v-model="selectedDay"
|
||||
:is-loading="isLoading"
|
||||
:events-by-date="filteredCalendar"
|
||||
:following-by-date="followingCalendarDate" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else key="grid" class="grid-view">
|
||||
<div class="search-container">
|
||||
<InputGroupSearch
|
||||
v-model="searchQuery"
|
||||
size="sm"
|
||||
:placeholder="t('dialog.group_calendar.search_placeholder')"
|
||||
class="search-input" />
|
||||
</div>
|
||||
|
||||
<div class="groups-grid" v-loading="isLoading">
|
||||
<div v-if="filteredGroupEvents.length" class="groups-container">
|
||||
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
|
||||
<div class="group-header" @click="toggleGroup(group.groupId)">
|
||||
<ArrowRight
|
||||
class="rotation-transition"
|
||||
:class="{ rotate: !groupCollapsed[group.groupId] }" />
|
||||
{{ group.groupName }}
|
||||
</div>
|
||||
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
|
||||
<GroupCalendarEventCard
|
||||
v-for="event in group.events"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
mode="grid"
|
||||
:is-following="isEventFollowing(event.id)"
|
||||
@update-following-calendar-data="updateFollowingCalendarData"
|
||||
@click-action="showGroupDialog(event.ownerId)"
|
||||
card-class="grid-card" />
|
||||
</div>
|
||||
<div v-if="viewMode === 'timeline'" key="timeline" class="timeline-view">
|
||||
<div class="timeline-container">
|
||||
<div v-if="groupedTimelineEvents.length" class="timeline-list">
|
||||
<div v-for="(timeGroup, key) of groupedTimelineEvents" :key="key" class="timeline-group">
|
||||
<div class="timeline-timestamp">
|
||||
{{ dayjs(timeGroup.startsAt).format('MM-DD ddd') }} {{ timeGroup.startTime }}
|
||||
</div>
|
||||
<div class="time-group-container">
|
||||
<GroupCalendarEventCard
|
||||
v-for="value in timeGroup.events"
|
||||
:key="value.id"
|
||||
:event="value"
|
||||
mode="timeline"
|
||||
:is-following="isEventFollowing(value.id)"
|
||||
:card-class="{ 'grouped-card': timeGroup.events.length > 1 }"
|
||||
@update-following-calendar-data="updateFollowingCalendarData"
|
||||
@click-action="showGroupDialog(value.ownerId)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-events">
|
||||
{{
|
||||
searchQuery
|
||||
? t('dialog.group_calendar.search_no_matching')
|
||||
: t('dialog.group_calendar.search_no_this_month')
|
||||
}}
|
||||
</div>
|
||||
<div v-else class="timeline-empty">{{ t('dialog.group_calendar.no_events') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-container">
|
||||
<GroupCalendarMonth
|
||||
v-model="selectedDay"
|
||||
:is-loading="isLoading"
|
||||
:events-by-date="filteredCalendar"
|
||||
:following-by-date="followingCalendarDate" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else key="grid" class="grid-view">
|
||||
<div class="search-container">
|
||||
<InputGroupSearch
|
||||
v-model="searchQuery"
|
||||
size="sm"
|
||||
:placeholder="t('dialog.group_calendar.search_placeholder')"
|
||||
class="search-input" />
|
||||
</div>
|
||||
|
||||
<div class="groups-grid" v-loading="isLoading">
|
||||
<div v-if="filteredGroupEvents.length" class="groups-container">
|
||||
<div v-for="group in filteredGroupEvents" :key="group.groupId" class="group-row">
|
||||
<div class="group-header" @click="toggleGroup(group.groupId)">
|
||||
<ArrowRight
|
||||
class="rotation-transition"
|
||||
:class="{ rotate: !groupCollapsed[group.groupId] }" />
|
||||
{{ group.groupName }}
|
||||
</div>
|
||||
<div class="events-row" v-show="!groupCollapsed[group.groupId]">
|
||||
<GroupCalendarEventCard
|
||||
v-for="event in group.events"
|
||||
:key="event.id"
|
||||
:event="event"
|
||||
mode="grid"
|
||||
:is-following="isEventFollowing(event.id)"
|
||||
@update-following-calendar-data="updateFollowingCalendarData"
|
||||
@click-action="showGroupDialog(event.ownerId)"
|
||||
card-class="grid-card" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-events">
|
||||
{{
|
||||
searchQuery
|
||||
? t('dialog.group_calendar.search_no_matching')
|
||||
: t('dialog.group_calendar.search_no_this_month')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -334,7 +329,8 @@
|
||||
.sort((a, b) => dayjs(a.startsAt).diff(dayjs(b.startsAt)));
|
||||
});
|
||||
|
||||
const formatDateKey = (date) => formatDateFilter(date, 'date');
|
||||
// Use a stable key for calendar maps (independent of locale/appearance date formatting).
|
||||
const formatDateKey = (date) => dayjs(date).format('YYYY-MM-DD');
|
||||
|
||||
function getGroupNameFromCache(groupId) {
|
||||
if (!groupNamesCache.has(groupId)) {
|
||||
@@ -462,18 +458,36 @@
|
||||
overflow: hidden;
|
||||
.timeline-view {
|
||||
.timeline-container {
|
||||
:deep(.el-timeline) {
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
padding-left: 4px;
|
||||
padding-right: 16px;
|
||||
margin-left: 10px;
|
||||
margin-right: 6px;
|
||||
overflow: auto;
|
||||
|
||||
.timeline-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.timeline-group {
|
||||
padding: 0 20px 6px 10px;
|
||||
}
|
||||
|
||||
.timeline-timestamp {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.timeline-empty {
|
||||
height: 100%;
|
||||
min-width: 200px;
|
||||
padding-left: 4px;
|
||||
padding-right: 16px;
|
||||
margin-left: 10px;
|
||||
margin-right: 6px;
|
||||
overflow: auto;
|
||||
.el-timeline-item {
|
||||
padding: 0 20px 20px 10px;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.time-group-container {
|
||||
display: flex;
|
||||
@@ -571,7 +585,6 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.calendar-container {
|
||||
width: 609px;
|
||||
|
||||
Reference in New Issue
Block a user