mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-26 18:23:47 +02:00
replace some element plus components
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"@fontsource-variable/noto-sans-kr": "^5.2.9",
|
"@fontsource-variable/noto-sans-kr": "^5.2.9",
|
||||||
"@fontsource-variable/noto-sans-sc": "^5.2.9",
|
"@fontsource-variable/noto-sans-sc": "^5.2.9",
|
||||||
"@fontsource-variable/noto-sans-tc": "^5.2.9",
|
"@fontsource-variable/noto-sans-tc": "^5.2.9",
|
||||||
|
"@internationalized/date": "^3.10.1",
|
||||||
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
||||||
"@sentry/vite-plugin": "^4.6.1",
|
"@sentry/vite-plugin": "^4.6.1",
|
||||||
"@sentry/vue": "^10.33.0",
|
"@sentry/vue": "^10.33.0",
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@fontsource-variable/noto-sans-kr": "^5.2.9",
|
"@fontsource-variable/noto-sans-kr": "^5.2.9",
|
||||||
"@fontsource-variable/noto-sans-sc": "^5.2.9",
|
"@fontsource-variable/noto-sans-sc": "^5.2.9",
|
||||||
"@fontsource-variable/noto-sans-tc": "^5.2.9",
|
"@fontsource-variable/noto-sans-tc": "^5.2.9",
|
||||||
|
"@internationalized/date": "^3.10.1",
|
||||||
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
"@kamiya4047/eslint-plugin-pretty-import": "^0.1.6",
|
||||||
"@sentry/vite-plugin": "^4.6.1",
|
"@sentry/vite-plugin": "^4.6.1",
|
||||||
"@sentry/vue": "^10.33.0",
|
"@sentry/vue": "^10.33.0",
|
||||||
|
|||||||
@@ -73,14 +73,14 @@
|
|||||||
<span>{{ t('nav_menu.whats_new') }}</span>
|
<span>{{ t('nav_menu.whats_new') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<el-divider></el-divider>
|
<Separator />
|
||||||
<div class="nav-menu-support__section">
|
<div class="nav-menu-support__section">
|
||||||
<span class="nav-menu-support__title">{{ t('nav_menu.resources') }}</span>
|
<span class="nav-menu-support__title">{{ t('nav_menu.resources') }}</span>
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('wiki')">
|
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('wiki')">
|
||||||
<span>{{ t('nav_menu.wiki') }}</span>
|
<span>{{ t('nav_menu.wiki') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<el-divider></el-divider>
|
<Separator />
|
||||||
<div class="nav-menu-support__section">
|
<div class="nav-menu-support__section">
|
||||||
<span class="nav-menu-support__title">{{ t('nav_menu.get_help') }}</span>
|
<span class="nav-menu-support__title">{{ t('nav_menu.get_help') }}</span>
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('github')">
|
<button type="button" class="nav-menu-settings__item" @click="handleSupportLink('github')">
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
<span class="nav-menu-settings__version">{{ version }}</span>
|
<span class="nav-menu-settings__version">{{ version }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-divider></el-divider>
|
<Separator />
|
||||||
<button type="button" class="nav-menu-settings__item" @click="handleSettingsClick">
|
<button type="button" class="nav-menu-settings__item" @click="handleSettingsClick">
|
||||||
<span>{{ t('nav_tooltip.settings') }}</span>
|
<span>{{ t('nav_tooltip.settings') }}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
<span v-if="themeMode === theme" class="nav-menu-theme__check">✓</span>
|
<span v-if="themeMode === theme" class="nav-menu-theme__check">✓</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<el-divider></el-divider>
|
<Separator />
|
||||||
|
|
||||||
<el-popover
|
<el-popover
|
||||||
v-model:visible="themeColorMenuVisible"
|
v-model:visible="themeColorMenuVisible"
|
||||||
@@ -247,6 +247,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { dayjs } from 'element-plus';
|
import { dayjs } from 'element-plus';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -752,10 +753,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
:deep(.el-divider) {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-menu-container {
|
.nav-menu-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -194,25 +194,21 @@
|
|||||||
(userDialog.representedGroup && userDialog.representedGroup.isRepresenting)
|
(userDialog.representedGroup && userDialog.representedGroup.isRepresenting)
|
||||||
"
|
"
|
||||||
class="extra">
|
class="extra">
|
||||||
<div style="display: inline-block; flex: none; margin-right: 5px">
|
<div
|
||||||
<el-image
|
|
||||||
v-loading="userDialog.isRepresentedGroupLoading"
|
v-loading="userDialog.isRepresentedGroupLoading"
|
||||||
class="x-link"
|
style="display: inline-block; flex: none; margin-right: 5px">
|
||||||
:src="userDialog.representedGroup.$thumbnailUrl"
|
<Avatar
|
||||||
style="
|
class="x-link size-15! rounded-lg!"
|
||||||
flex: none;
|
|
||||||
width: 60px;
|
|
||||||
height: 60px;
|
|
||||||
border-radius: 4px;
|
|
||||||
object-fit: cover;
|
|
||||||
"
|
|
||||||
:style="{
|
:style="{
|
||||||
background: userDialog.isRepresentedGroupLoading ? '#f5f7fa' : ''
|
background: userDialog.isRepresentedGroupLoading ? '#f5f7fa' : ''
|
||||||
}"
|
}"
|
||||||
@load="userDialog.isRepresentedGroupLoading = false"
|
|
||||||
@click="showFullscreenImageDialog(userDialog.representedGroup.iconUrl)">
|
@click="showFullscreenImageDialog(userDialog.representedGroup.iconUrl)">
|
||||||
<template #error></template>
|
<AvatarImage
|
||||||
</el-image>
|
:src="userDialog.representedGroup.$thumbnailUrl"
|
||||||
|
@load="userDialog.isRepresentedGroupLoading = false"
|
||||||
|
@error="userDialog.isRepresentedGroupLoading = false" />
|
||||||
|
<AvatarFallback class="rounded-lg!" />
|
||||||
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
v-if="userDialog.representedGroup.isRepresenting"
|
v-if="userDialog.representedGroup.isRepresenting"
|
||||||
@@ -1358,6 +1354,7 @@
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
|
|||||||
16
src/components/ui/avatar/Avatar.vue
Normal file
16
src/components/ui/avatar/Avatar.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
import { AvatarRoot } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarRoot
|
||||||
|
data-slot="avatar"
|
||||||
|
:class="cn('relative flex size-8 shrink-0 overflow-hidden rounded-full', props.class)">
|
||||||
|
<slot />
|
||||||
|
</AvatarRoot>
|
||||||
|
</template>
|
||||||
23
src/components/ui/avatar/AvatarFallback.vue
Normal file
23
src/components/ui/avatar/AvatarFallback.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import { AvatarFallback } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
delayMs: { type: Number, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarFallback
|
||||||
|
data-slot="avatar-fallback"
|
||||||
|
v-bind="delegatedProps"
|
||||||
|
:class="cn('bg-muted flex size-full items-center justify-center rounded-full', props.class)">
|
||||||
|
<slot />
|
||||||
|
</AvatarFallback>
|
||||||
|
</template>
|
||||||
17
src/components/ui/avatar/AvatarImage.vue
Normal file
17
src/components/ui/avatar/AvatarImage.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
import { AvatarImage } from 'reka-ui';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: { type: String, required: true },
|
||||||
|
referrerPolicy: { type: null, required: false },
|
||||||
|
crossOrigin: { type: null, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AvatarImage data-slot="avatar-image" v-bind="props" class="aspect-square size-full">
|
||||||
|
<slot />
|
||||||
|
</AvatarImage>
|
||||||
|
</template>
|
||||||
3
src/components/ui/avatar/index.js
Normal file
3
src/components/ui/avatar/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as Avatar } from './Avatar.vue';
|
||||||
|
export { default as AvatarFallback } from './AvatarFallback.vue';
|
||||||
|
export { default as AvatarImage } from './AvatarImage.vue';
|
||||||
211
src/components/ui/calendar/Calendar.vue
Normal file
211
src/components/ui/calendar/Calendar.vue
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarRoot, useDateFormatter, useForwardPropsEmits } from 'reka-ui';
|
||||||
|
import { createReusableTemplate, reactiveOmit, useVModel } from '@vueuse/core';
|
||||||
|
import { createYear, createYearRange, toDate } from 'reka-ui/date';
|
||||||
|
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select';
|
||||||
|
import { computed, toRaw } from 'vue';
|
||||||
|
import { getLocalTimeZone, today } from '@internationalized/date';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CalendarCell,
|
||||||
|
CalendarCellTrigger,
|
||||||
|
CalendarGrid,
|
||||||
|
CalendarGridBody,
|
||||||
|
CalendarGridHead,
|
||||||
|
CalendarGridRow,
|
||||||
|
CalendarHeadCell,
|
||||||
|
CalendarHeader,
|
||||||
|
CalendarHeading,
|
||||||
|
CalendarNextButton,
|
||||||
|
CalendarPrevButton
|
||||||
|
} from '.';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
defaultValue: { type: null, required: false },
|
||||||
|
defaultPlaceholder: { type: null, required: false },
|
||||||
|
placeholder: { type: null, required: false },
|
||||||
|
pagedNavigation: { type: Boolean, required: false },
|
||||||
|
preventDeselect: { type: Boolean, required: false },
|
||||||
|
weekStartsOn: { type: Number, required: false },
|
||||||
|
weekdayFormat: { type: String, required: false },
|
||||||
|
calendarLabel: { type: String, required: false },
|
||||||
|
fixedWeeks: { type: Boolean, required: false },
|
||||||
|
maxValue: { type: null, required: false },
|
||||||
|
minValue: { type: null, required: false },
|
||||||
|
locale: { type: String, required: false },
|
||||||
|
numberOfMonths: { type: Number, required: false },
|
||||||
|
disabled: { type: Boolean, required: false },
|
||||||
|
readonly: { type: Boolean, required: false },
|
||||||
|
initialFocus: { type: Boolean, required: false },
|
||||||
|
isDateDisabled: { type: Function, required: false },
|
||||||
|
isDateUnavailable: { type: Function, required: false },
|
||||||
|
dir: { type: String, required: false },
|
||||||
|
nextPage: { type: Function, required: false },
|
||||||
|
prevPage: { type: Function, required: false },
|
||||||
|
modelValue: { type: null, required: false, default: undefined },
|
||||||
|
multiple: { type: Boolean, required: false },
|
||||||
|
disableDaysOutsideCurrentView: { type: Boolean, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false },
|
||||||
|
layout: { type: null, required: false, default: undefined },
|
||||||
|
yearRange: { type: Array, required: false }
|
||||||
|
});
|
||||||
|
const emits = defineEmits(['update:modelValue', 'update:placeholder']);
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class', 'layout', 'placeholder');
|
||||||
|
|
||||||
|
const placeholder = useVModel(props, 'placeholder', emits, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: props.defaultPlaceholder ?? today(getLocalTimeZone())
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatter = useDateFormatter(props.locale ?? 'en');
|
||||||
|
|
||||||
|
const yearRange = computed(() => {
|
||||||
|
return (
|
||||||
|
props.yearRange ??
|
||||||
|
createYearRange({
|
||||||
|
start:
|
||||||
|
props?.minValue ??
|
||||||
|
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle(
|
||||||
|
'year',
|
||||||
|
-100
|
||||||
|
),
|
||||||
|
|
||||||
|
end:
|
||||||
|
props?.maxValue ??
|
||||||
|
(toRaw(props.placeholder) ?? props.defaultPlaceholder ?? today(getLocalTimeZone())).cycle(
|
||||||
|
'year',
|
||||||
|
10
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [DefineMonthTemplate, ReuseMonthTemplate] = createReusableTemplate();
|
||||||
|
const [DefineYearTemplate, ReuseYearTemplate] = createReusableTemplate();
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DefineMonthTemplate v-slot="{ date }">
|
||||||
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none">
|
||||||
|
{{ formatter.custom(toDate(date), { month: 'short' }) }}
|
||||||
|
</div>
|
||||||
|
<NativeSelect
|
||||||
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
||||||
|
@change="
|
||||||
|
(e) => {
|
||||||
|
placeholder = placeholder.set({
|
||||||
|
month: Number(e?.target?.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<NativeSelectOption
|
||||||
|
v-for="month in createYear({ dateObj: date })"
|
||||||
|
:key="month.toString()"
|
||||||
|
:value="month.month"
|
||||||
|
:selected="date.month === month.month">
|
||||||
|
{{ formatter.custom(toDate(month), { month: 'short' }) }}
|
||||||
|
</NativeSelectOption>
|
||||||
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefineMonthTemplate>
|
||||||
|
|
||||||
|
<DefineYearTemplate v-slot="{ date }">
|
||||||
|
<div class="**:data-[slot=native-select-icon]:right-1">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute inset-0 flex h-full items-center text-sm pl-2 pointer-events-none">
|
||||||
|
{{ formatter.custom(toDate(date), { year: 'numeric' }) }}
|
||||||
|
</div>
|
||||||
|
<NativeSelect
|
||||||
|
class="text-xs h-8 pr-6 pl-2 text-transparent relative"
|
||||||
|
@change="
|
||||||
|
(e) => {
|
||||||
|
placeholder = placeholder.set({
|
||||||
|
year: Number(e?.target?.value)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
">
|
||||||
|
<NativeSelectOption
|
||||||
|
v-for="year in yearRange"
|
||||||
|
:key="year.toString()"
|
||||||
|
:value="year.year"
|
||||||
|
:selected="date.year === year.year">
|
||||||
|
{{ formatter.custom(toDate(year), { year: 'numeric' }) }}
|
||||||
|
</NativeSelectOption>
|
||||||
|
</NativeSelect>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefineYearTemplate>
|
||||||
|
|
||||||
|
<CalendarRoot
|
||||||
|
v-slot="{ grid, weekDays, date }"
|
||||||
|
v-bind="forwarded"
|
||||||
|
v-model:placeholder="placeholder"
|
||||||
|
data-slot="calendar"
|
||||||
|
:class="cn('p-3', props.class)">
|
||||||
|
<CalendarHeader class="pt-0">
|
||||||
|
<nav class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between">
|
||||||
|
<CalendarPrevButton>
|
||||||
|
<slot name="calendar-prev-icon" />
|
||||||
|
</CalendarPrevButton>
|
||||||
|
<CalendarNextButton>
|
||||||
|
<slot name="calendar-next-icon" />
|
||||||
|
</CalendarNextButton>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<slot name="calendar-heading" :date="date" :month="ReuseMonthTemplate" :year="ReuseYearTemplate">
|
||||||
|
<template v-if="layout === 'month-and-year'">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<ReuseMonthTemplate :date="date" />
|
||||||
|
<ReuseYearTemplate :date="date" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="layout === 'month-only'">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
<ReuseMonthTemplate :date="date" />
|
||||||
|
{{ formatter.custom(toDate(date), { year: 'numeric' }) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="layout === 'year-only'">
|
||||||
|
<div class="flex items-center justify-center gap-1">
|
||||||
|
{{ formatter.custom(toDate(date), { month: 'short' }) }}
|
||||||
|
<ReuseYearTemplate :date="date" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<CalendarHeading />
|
||||||
|
</template>
|
||||||
|
</slot>
|
||||||
|
</CalendarHeader>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
|
||||||
|
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
|
||||||
|
<CalendarGridHead>
|
||||||
|
<CalendarGridRow>
|
||||||
|
<CalendarHeadCell v-for="day in weekDays" :key="day">
|
||||||
|
{{ day }}
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridHead>
|
||||||
|
<CalendarGridBody>
|
||||||
|
<CalendarGridRow
|
||||||
|
v-for="(weekDates, index) in month.rows"
|
||||||
|
:key="`weekDate-${index}`"
|
||||||
|
class="mt-2 w-full">
|
||||||
|
<CalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
|
||||||
|
<CalendarCellTrigger :day="weekDate" :month="month.value" />
|
||||||
|
</CalendarCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridBody>
|
||||||
|
</CalendarGrid>
|
||||||
|
</div>
|
||||||
|
</CalendarRoot>
|
||||||
|
</template>
|
||||||
30
src/components/ui/calendar/CalendarCell.vue
Normal file
30
src/components/ui/calendar/CalendarCell.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarCell, useForwardProps } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
date: { type: null, required: true },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarCell
|
||||||
|
data-slot="calendar-cell"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarCell>
|
||||||
|
</template>
|
||||||
42
src/components/ui/calendar/CalendarCellTrigger.vue
Normal file
42
src/components/ui/calendar/CalendarCellTrigger.vue
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarCellTrigger, useForwardProps } from 'reka-ui';
|
||||||
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
day: { type: null, required: true },
|
||||||
|
month: { type: null, required: true },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false, default: 'button' },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarCellTrigger
|
||||||
|
data-slot="calendar-cell-trigger"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
buttonVariants({ variant: 'ghost' }),
|
||||||
|
'size-8 p-0 font-normal aria-selected:opacity-100 cursor-default',
|
||||||
|
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
|
||||||
|
// Selected
|
||||||
|
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
|
||||||
|
// Disabled
|
||||||
|
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
|
||||||
|
// Unavailable
|
||||||
|
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
|
||||||
|
// Outside months
|
||||||
|
'data-[outside-view]:text-muted-foreground',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarCellTrigger>
|
||||||
|
</template>
|
||||||
24
src/components/ui/calendar/CalendarGrid.vue
Normal file
24
src/components/ui/calendar/CalendarGrid.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarGrid, useForwardProps } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGrid
|
||||||
|
data-slot="calendar-grid"
|
||||||
|
:class="cn('w-full border-collapse space-x-1', props.class)"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarGrid>
|
||||||
|
</template>
|
||||||
14
src/components/ui/calendar/CalendarGridBody.vue
Normal file
14
src/components/ui/calendar/CalendarGridBody.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarGridBody } from 'reka-ui';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridBody data-slot="calendar-grid-body" v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</CalendarGridBody>
|
||||||
|
</template>
|
||||||
15
src/components/ui/calendar/CalendarGridHead.vue
Normal file
15
src/components/ui/calendar/CalendarGridHead.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarGridHead } from 'reka-ui';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridHead data-slot="calendar-grid-head" v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</CalendarGridHead>
|
||||||
|
</template>
|
||||||
21
src/components/ui/calendar/CalendarGridRow.vue
Normal file
21
src/components/ui/calendar/CalendarGridRow.vue
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarGridRow, useForwardProps } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarGridRow data-slot="calendar-grid-row" :class="cn('flex', props.class)" v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarGridRow>
|
||||||
|
</template>
|
||||||
24
src/components/ui/calendar/CalendarHeadCell.vue
Normal file
24
src/components/ui/calendar/CalendarHeadCell.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarHeadCell, useForwardProps } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeadCell
|
||||||
|
data-slot="calendar-head-cell"
|
||||||
|
:class="cn('text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem]', props.class)"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</template>
|
||||||
24
src/components/ui/calendar/CalendarHeader.vue
Normal file
24
src/components/ui/calendar/CalendarHeader.vue
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarHeader, useForwardProps } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeader
|
||||||
|
data-slot="calendar-header"
|
||||||
|
:class="cn('flex justify-center pt-1 relative items-center w-full px-8', props.class)"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</CalendarHeader>
|
||||||
|
</template>
|
||||||
29
src/components/ui/calendar/CalendarHeading.vue
Normal file
29
src/components/ui/calendar/CalendarHeading.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarHeading, useForwardProps } from 'reka-ui';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
defineSlots();
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarHeading
|
||||||
|
v-slot="{ headingValue }"
|
||||||
|
data-slot="calendar-heading"
|
||||||
|
:class="cn('text-sm font-medium', props.class)"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot :heading-value>
|
||||||
|
{{ headingValue }}
|
||||||
|
</slot>
|
||||||
|
</CalendarHeading>
|
||||||
|
</template>
|
||||||
35
src/components/ui/calendar/CalendarNextButton.vue
Normal file
35
src/components/ui/calendar/CalendarNextButton.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarNext, useForwardProps } from 'reka-ui';
|
||||||
|
import { ChevronRight } from 'lucide-vue-next';
|
||||||
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
nextPage: { type: Function, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarNext
|
||||||
|
data-slot="calendar-next-button"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot>
|
||||||
|
<ChevronRight class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</CalendarNext>
|
||||||
|
</template>
|
||||||
35
src/components/ui/calendar/CalendarPrevButton.vue
Normal file
35
src/components/ui/calendar/CalendarPrevButton.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script setup>
|
||||||
|
import { CalendarPrev, useForwardProps } from 'reka-ui';
|
||||||
|
import { ChevronLeft } from 'lucide-vue-next';
|
||||||
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import { reactiveOmit } from '@vueuse/core';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
prevPage: { type: Function, required: false },
|
||||||
|
asChild: { type: Boolean, required: false },
|
||||||
|
as: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CalendarPrev
|
||||||
|
data-slot="calendar-prev-button"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
buttonVariants({ variant: 'outline' }),
|
||||||
|
'size-7 bg-transparent p-0 opacity-50 hover:opacity-100',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
v-bind="forwardedProps">
|
||||||
|
<slot>
|
||||||
|
<ChevronLeft class="size-4" />
|
||||||
|
</slot>
|
||||||
|
</CalendarPrev>
|
||||||
|
</template>
|
||||||
12
src/components/ui/calendar/index.js
Normal file
12
src/components/ui/calendar/index.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export { default as Calendar } from './Calendar.vue';
|
||||||
|
export { default as CalendarCell } from './CalendarCell.vue';
|
||||||
|
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue';
|
||||||
|
export { default as CalendarGrid } from './CalendarGrid.vue';
|
||||||
|
export { default as CalendarGridBody } from './CalendarGridBody.vue';
|
||||||
|
export { default as CalendarGridHead } from './CalendarGridHead.vue';
|
||||||
|
export { default as CalendarGridRow } from './CalendarGridRow.vue';
|
||||||
|
export { default as CalendarHeadCell } from './CalendarHeadCell.vue';
|
||||||
|
export { default as CalendarHeader } from './CalendarHeader.vue';
|
||||||
|
export { default as CalendarHeading } from './CalendarHeading.vue';
|
||||||
|
export { default as CalendarNextButton } from './CalendarNextButton.vue';
|
||||||
|
export { default as CalendarPrevButton } from './CalendarPrevButton.vue';
|
||||||
46
src/components/ui/native-select/NativeSelect.vue
Normal file
46
src/components/ui/native-select/NativeSelect.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
import { reactiveOmit, useVModel } from '@vueuse/core';
|
||||||
|
import { ChevronDownIcon } from 'lucide-vue-next';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
inheritAttrs: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: { type: null, required: false },
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
const modelValue = useVModel(props, 'modelValue', emit, {
|
||||||
|
passive: true,
|
||||||
|
defaultValue: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const delegatedProps = reactiveOmit(props, 'class');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="group/native-select relative w-fit has-[select:disabled]:opacity-50" data-slot="native-select-wrapper">
|
||||||
|
<select
|
||||||
|
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||||
|
v-model="modelValue"
|
||||||
|
data-slot="native-select"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||||
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||||
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
">
|
||||||
|
<slot />
|
||||||
|
</select>
|
||||||
|
<ChevronDownIcon
|
||||||
|
class="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="native-select-icon" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
16
src/components/ui/native-select/NativeSelectOptGroup.vue
Normal file
16
src/components/ui/native-select/NativeSelectOptGroup.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- @fallthroughAttributes true -->
|
||||||
|
<!-- @strictTemplates true -->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<optgroup data-slot="native-select-optgroup" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</optgroup>
|
||||||
|
</template>
|
||||||
16
src/components/ui/native-select/NativeSelectOption.vue
Normal file
16
src/components/ui/native-select/NativeSelectOption.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- @fallthroughAttributes true -->
|
||||||
|
<!-- @strictTemplates true -->
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<option data-slot="native-select-option" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||||
|
<slot />
|
||||||
|
</option>
|
||||||
|
</template>
|
||||||
3
src/components/ui/native-select/index.js
Normal file
3
src/components/ui/native-select/index.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as NativeSelect } from './NativeSelect.vue';
|
||||||
|
export { default as NativeSelectOptGroup } from './NativeSelectOptGroup.vue';
|
||||||
|
export { default as NativeSelectOption } from './NativeSelectOption.vue';
|
||||||
11
src/components/ui/skeleton/Skeleton.vue
Normal file
11
src/components/ui/skeleton/Skeleton.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
class: { type: null, required: false }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-slot="skeleton" :class="cn('animate-pulse rounded-md bg-primary/10', props.class)" />
|
||||||
|
</template>
|
||||||
1
src/components/ui/skeleton/index.js
Normal file
1
src/components/ui/skeleton/index.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { default as Skeleton } from './Skeleton.vue';
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<HoverCardTrigger as-child>
|
<HoverCardTrigger as-child>
|
||||||
<Info style="margin-left: 5px; font-size: 12px; opacity: 0.7" />
|
<Info style="margin-left: 5px; font-size: 12px; opacity: 0.7" />
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent side="bottom" align="start" class="w-[300px]">
|
<HoverCardContent side="bottom" align="start" class="w-75">
|
||||||
<div class="tips-popover">
|
<div class="tips-popover">
|
||||||
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
|
<div>{{ t('view.charts.instance_activity.tips.online_time') }}</div>
|
||||||
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
|
<div>{{ t('view.charts.instance_activity.tips.click_Y_axis') }}</div>
|
||||||
@@ -98,13 +98,28 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<el-date-picker
|
<Popover v-model:open="isDatePickerOpen">
|
||||||
v-model="selectedDate"
|
<PopoverTrigger asChild>
|
||||||
type="date"
|
<div>
|
||||||
:clearable="false"
|
<Button
|
||||||
:default-value="dayjs().toDate()"
|
variant="outline"
|
||||||
:disabled-date="getDatePickerDisabledDate"
|
class="w-50 justify-start text-left font-normal"
|
||||||
@change="reloadData"></el-date-picker>
|
:disabled="isLoading">
|
||||||
|
<CalendarIcon class="mr-2 h-4 w-4" />
|
||||||
|
{{ dayjs(selectedDate).format('YYYY-MM-DD') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent class="w-auto p-0" align="end">
|
||||||
|
<Calendar
|
||||||
|
:model-value="calendarModelValue"
|
||||||
|
:default-placeholder="defaultCalendarPlaceholder"
|
||||||
|
:is-date-disabled="isCalendarDateDisabled"
|
||||||
|
:prevent-deselect="true"
|
||||||
|
initial-focus
|
||||||
|
@update:modelValue="handleCalendarModelUpdate" />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-online">
|
<div class="status-online">
|
||||||
@@ -125,7 +140,11 @@
|
|||||||
|
|
||||||
<transition name="el-fade-in-linear">
|
<transition name="el-fade-in-linear">
|
||||||
<div v-show="isDetailVisible && !isLoading && activityData.length !== 0" class="divider">
|
<div v-show="isDetailVisible && !isLoading && activityData.length !== 0" class="divider">
|
||||||
<el-divider>·</el-divider>
|
<div class="flex items-center">
|
||||||
|
<Separator class="flex-1" />
|
||||||
|
<span class="px-2 text-muted-foreground">·</span>
|
||||||
|
<Separator class="flex-1" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
<template v-if="isDetailVisible && activityData.length !== 0">
|
<template v-if="isDetailVisible && activityData.length !== 0">
|
||||||
@@ -141,11 +160,15 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { AlertTriangle, ArrowLeft, ArrowRight, Info, RefreshCcw, Settings } from 'lucide-vue-next';
|
import { ArrowLeft, ArrowRight, Calendar as CalendarIcon, Info, RefreshCcw, Settings } from 'lucide-vue-next';
|
||||||
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card';
|
||||||
|
import { fromDate, getLocalTimeZone, today } from '@internationalized/date';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { ButtonGroup } from '@/components/ui/button-group';
|
import { ButtonGroup } from '@/components/ui/button-group';
|
||||||
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { toDate } from 'reka-ui/date';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
@@ -258,6 +281,30 @@
|
|||||||
getDatePickerDisabledDate
|
getDatePickerDisabledDate
|
||||||
} = useDateNavigation(allDateOfActivity, () => reloadData());
|
} = useDateNavigation(allDateOfActivity, () => reloadData());
|
||||||
|
|
||||||
|
const isDatePickerOpen = ref(false);
|
||||||
|
const calendarTimeZone = getLocalTimeZone();
|
||||||
|
const defaultCalendarPlaceholder = today(calendarTimeZone);
|
||||||
|
|
||||||
|
const calendarModelValue = computed(() => {
|
||||||
|
// Keep the rest of the feature using JS Date; adapt to Calendar's DateValue model.
|
||||||
|
return fromDate(selectedDate.value ?? new Date(), calendarTimeZone);
|
||||||
|
});
|
||||||
|
|
||||||
|
function isCalendarDateDisabled(dateValue) {
|
||||||
|
try {
|
||||||
|
return getDatePickerDisabledDate(toDate(dateValue, calendarTimeZone));
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCalendarModelUpdate(dateValue) {
|
||||||
|
if (!dateValue) return;
|
||||||
|
selectedDate.value = toDate(dateValue, calendarTimeZone);
|
||||||
|
isDatePickerOpen.value = false;
|
||||||
|
reloadData();
|
||||||
|
}
|
||||||
|
|
||||||
const activityChartRef = ref(null);
|
const activityChartRef = ref(null);
|
||||||
const activityDetailChartRef = ref(null);
|
const activityDetailChartRef = ref(null);
|
||||||
|
|
||||||
@@ -702,11 +749,6 @@
|
|||||||
.el-date-editor.el-input__inner {
|
.el-date-editor.el-input__inner {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
.el-divider__text {
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-online {
|
.status-online {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
<Card class="friend-card p-0 gap-0" :style="cardStyle" @click="showUserDialog(friend.id)">
|
<Card class="friend-card p-0 gap-0" :style="cardStyle" @click="showUserDialog(friend.id)">
|
||||||
<div class="friend-card__header">
|
<div class="friend-card__header">
|
||||||
<div class="friend-card__avatar-wrapper">
|
<div class="friend-card__avatar-wrapper">
|
||||||
<el-avatar :size="avatarSize" :src="userImage(props.friend.ref, true)" class="friend-card__avatar">
|
<Avatar class="friend-card__avatar" :style="{ width: `${avatarSize}px`, height: `${avatarSize}px` }">
|
||||||
{{ avatarFallback }}
|
<AvatarImage :src="userImage(props.friend.ref, true)" />
|
||||||
</el-avatar>
|
<AvatarFallback>{{ avatarFallback }}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
</div>
|
</div>
|
||||||
<span class="friend-card__status-dot" :class="statusDotClass"></span>
|
<span class="friend-card__status-dot" :class="statusDotClass"></span>
|
||||||
<div class="friend-card__name ml-0.5" :title="friend.name">{{ friend.name }}</div>
|
<div class="friend-card__name ml-0.5" :title="friend.name">{{ friend.name }}</div>
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Pencil } from 'lucide-vue-next';
|
import { Pencil } from 'lucide-vue-next';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|||||||
@@ -199,7 +199,7 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
<el-divider direction="vertical"></el-divider>
|
<Separator orientation="vertical" class="mx-2 h-5" />
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
:model-value="searchAvatarFilterRemote"
|
:model-value="searchAvatarFilterRemote"
|
||||||
class="flex items-center gap-4"
|
class="flex items-center gap-4"
|
||||||
@@ -361,6 +361,7 @@
|
|||||||
import { ButtonGroup } from '@/components/ui/button-group';
|
import { ButtonGroup } from '@/components/ui/button-group';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { InputGroupField } from '@/components/ui/input-group';
|
import { InputGroupField } from '@/components/ui/input-group';
|
||||||
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Spinner } from '@/components/ui/spinner';
|
import { Spinner } from '@/components/ui/spinner';
|
||||||
import { TabsUnderline } from '@/components/ui/tabs';
|
import { TabsUnderline } from '@/components/ui/tabs';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
|
|||||||
@@ -45,23 +45,22 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<el-skeleton v-else animated class="skeleton" :throttle="100">
|
<div v-else class="skeleton" aria-busy="true" aria-label="Loading">
|
||||||
<template #template>
|
|
||||||
<div>
|
<div>
|
||||||
<el-skeleton-item variant="circle" />
|
<Skeleton class="h-10 w-10 rounded-full" />
|
||||||
<div>
|
<div>
|
||||||
<el-skeleton-item variant="text" />
|
<Skeleton class="h-3.5 w-1/2" />
|
||||||
<el-skeleton-item variant="text" />
|
<Skeleton class="mt-1.5 h-3 w-full" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
</el-skeleton>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { AlertTriangle, Loader2, Trash2 } from 'lucide-vue-next';
|
import { AlertTriangle, Loader2, Trash2 } from 'lucide-vue-next';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
@@ -108,19 +107,5 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-skeleton__circle {
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
.el-skeleton__text {
|
|
||||||
&:first-child {
|
|
||||||
height: 14px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
height: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
220
src/views/Tools/components/GroupCalendarMonth.vue
Normal file
220
src/views/Tools/components/GroupCalendarMonth.vue
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
CalendarCell,
|
||||||
|
CalendarCellTrigger,
|
||||||
|
CalendarGrid,
|
||||||
|
CalendarGridBody,
|
||||||
|
CalendarGridHead,
|
||||||
|
CalendarGridRow,
|
||||||
|
CalendarHeadCell,
|
||||||
|
CalendarHeader,
|
||||||
|
CalendarHeading,
|
||||||
|
CalendarNextButton,
|
||||||
|
CalendarPrevButton
|
||||||
|
} from '@/components/ui/calendar';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { fromDate, getLocalTimeZone } from '@internationalized/date';
|
||||||
|
import { CalendarRoot } from 'reka-ui';
|
||||||
|
import { toDate } from 'reka-ui/date';
|
||||||
|
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Date,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isLoading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
eventsByDate: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
followingByDate: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue']);
|
||||||
|
|
||||||
|
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(
|
||||||
|
() => props.modelValue,
|
||||||
|
(next) => {
|
||||||
|
if (!next) return;
|
||||||
|
internalValue.value = fromDate(next, timeZone);
|
||||||
|
placeholder.value = fromDate(next, timeZone);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedDayKey = computed(() => dayjs(props.modelValue).format('YYYY-MM-DD'));
|
||||||
|
|
||||||
|
function toKey(dateValue) {
|
||||||
|
return dayjs(toDate(dateValue, timeZone)).format('YYYY-MM-DD');
|
||||||
|
}
|
||||||
|
|
||||||
|
function eventCountFor(dateValue) {
|
||||||
|
const key = toKey(dateValue);
|
||||||
|
return props.eventsByDate?.[key]?.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasFollowingFor(dateValue) {
|
||||||
|
const key = toKey(dateValue);
|
||||||
|
return Boolean(props.followingByDate?.[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdateModelValue(next) {
|
||||||
|
if (!next) return;
|
||||||
|
internalValue.value = next;
|
||||||
|
const asDate = toDate(next, timeZone);
|
||||||
|
placeholder.value = next;
|
||||||
|
emit('update:modelValue', asDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onUpdatePlaceholder(next) {
|
||||||
|
if (!next) return;
|
||||||
|
placeholder.value = next;
|
||||||
|
internalValue.value = next;
|
||||||
|
emit('update:modelValue', toDate(next, timeZone));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dayLabel(dateValue) {
|
||||||
|
return dayjs(toDate(dateValue, timeZone)).format('D');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-loading="props.isLoading" class="group-calendar-month">
|
||||||
|
<CalendarRoot
|
||||||
|
v-slot="{ grid, weekDays }"
|
||||||
|
:model-value="internalValue"
|
||||||
|
@update:modelValue="onUpdateModelValue"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@update:placeholder="onUpdatePlaceholder"
|
||||||
|
:prevent-deselect="true"
|
||||||
|
class="p-4">
|
||||||
|
<CalendarHeader class="pt-0">
|
||||||
|
<nav class="flex items-center gap-1 absolute top-0 inset-x-0 justify-between">
|
||||||
|
<CalendarPrevButton class="size-9" />
|
||||||
|
<CalendarNextButton class="size-9" />
|
||||||
|
</nav>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<CalendarHeading class="text-base" />
|
||||||
|
</div>
|
||||||
|
</CalendarHeader>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-y-4 mt-6">
|
||||||
|
<CalendarGrid v-for="month in grid" :key="month.value.toString()" class="w-full">
|
||||||
|
<CalendarGridHead>
|
||||||
|
<CalendarGridRow>
|
||||||
|
<CalendarHeadCell v-for="day in weekDays" :key="day" class="text-sm">
|
||||||
|
{{ day }}
|
||||||
|
</CalendarHeadCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridHead>
|
||||||
|
<CalendarGridBody>
|
||||||
|
<CalendarGridRow
|
||||||
|
v-for="(weekDates, index) in month.rows"
|
||||||
|
:key="`weekDate-${index}`"
|
||||||
|
class="mt-2 w-full">
|
||||||
|
<CalendarCell v-for="weekDate in weekDates" :key="weekDate.toString()" :date="weekDate">
|
||||||
|
<CalendarCellTrigger
|
||||||
|
:day="weekDate"
|
||||||
|
:month="month.value"
|
||||||
|
class="size-12 cursor-pointer">
|
||||||
|
<div class="date">
|
||||||
|
<div
|
||||||
|
class="calendar-date-content"
|
||||||
|
:class="{
|
||||||
|
'has-events': eventCountFor(weekDate) > 0,
|
||||||
|
'is-selected': toKey(weekDate) === selectedDayKey
|
||||||
|
}">
|
||||||
|
{{ dayLabel(weekDate) }}
|
||||||
|
<div
|
||||||
|
v-if="eventCountFor(weekDate) > 0"
|
||||||
|
class="calendar-event-badge"
|
||||||
|
:class="hasFollowingFor(weekDate) ? 'has-following' : 'no-following'">
|
||||||
|
{{ eventCountFor(weekDate) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CalendarCellTrigger>
|
||||||
|
</CalendarCell>
|
||||||
|
</CalendarGridRow>
|
||||||
|
</CalendarGridBody>
|
||||||
|
</CalendarGrid>
|
||||||
|
</div>
|
||||||
|
</CalendarRoot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.group-calendar-month {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-date-content {
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 18px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-date-content.has-events {
|
||||||
|
background-color: var(--group-calendar-event-bg, color-mix(in oklch, var(--el-color-primary) 10%, transparent));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-event-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
color: var(--el-color-white, #fff);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: var(--el-box-shadow-lighter);
|
||||||
|
z-index: 10;
|
||||||
|
padding: 0 5px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-event-badge.has-following {
|
||||||
|
background-color: var(--group-calendar-badge-following, var(--el-color-success));
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-event-badge.no-following {
|
||||||
|
background-color: var(--group-calendar-badge-normal, var(--el-color-primary));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
|
<Dialog :open="visible" @update:open="(open) => (open ? null : closeDialog())">
|
||||||
<DialogContent class="x-dialog w-[90vw] max-w-[90vw] h-[80vh] overflow-hidden">
|
<DialogContent class="x-dialog w-[90vw] max-w-[90vw] sm:max-w-[70vw] h-[60vh] overflow-hidden">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div class="dialog-title-container">
|
<div class="dialog-title-container">
|
||||||
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
|
<DialogTitle>{{ t('dialog.group_calendar.header') }}</DialogTitle>
|
||||||
@@ -46,29 +46,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="calendar-container">
|
<div class="calendar-container">
|
||||||
<el-calendar v-model="selectedDay" v-loading="isLoading">
|
<GroupCalendarMonth
|
||||||
<template #date-cell="{ data }">
|
v-model="selectedDay"
|
||||||
<div class="date">
|
:is-loading="isLoading"
|
||||||
<div
|
:events-by-date="filteredCalendar"
|
||||||
class="calendar-date-content"
|
:following-by-date="followingCalendarDate" />
|
||||||
:class="{
|
|
||||||
'has-events': filteredCalendar[formatDateKey(data.date)]?.length
|
|
||||||
}">
|
|
||||||
{{ dayjs(data.date).format('D') }}
|
|
||||||
<div
|
|
||||||
v-if="filteredCalendar[formatDateKey(data.date)]?.length"
|
|
||||||
class="calendar-event-badge"
|
|
||||||
:class="
|
|
||||||
followingCalendarDate[formatDateKey(data.date)]
|
|
||||||
? 'has-following'
|
|
||||||
: 'no-following'
|
|
||||||
">
|
|
||||||
{{ filteredCalendar[formatDateKey(data.date)]?.length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-calendar>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else key="grid" class="grid-view">
|
<div v-else key="grid" class="grid-view">
|
||||||
@@ -134,6 +116,7 @@
|
|||||||
import { useGroupStore } from '../../../stores';
|
import { useGroupStore } from '../../../stores';
|
||||||
|
|
||||||
import GroupCalendarEventCard from '../components/GroupCalendarEventCard.vue';
|
import GroupCalendarEventCard from '../components/GroupCalendarEventCard.vue';
|
||||||
|
import GroupCalendarMonth from '../components/GroupCalendarMonth.vue';
|
||||||
import configRepository from '../../../service/config';
|
import configRepository from '../../../service/config';
|
||||||
|
|
||||||
const { applyGroupEvent, showGroupDialog } = useGroupStore();
|
const { applyGroupEvent, showGroupDialog } = useGroupStore();
|
||||||
|
|||||||
Reference in New Issue
Block a user