replace el-tabs

This commit is contained in:
pa
2026-01-14 22:25:52 +09:00
committed by Natsumi
parent 442b1060f7
commit b7f3d91a03
17 changed files with 1382 additions and 1061 deletions
@@ -325,8 +325,12 @@
</div> </div>
</div> </div>
</div> </div>
<el-tabs v-model="avatarDialogLastActiveTab" @tab-click="avatarDialogTabClick"> <TabsUnderline
<el-tab-pane name="Info" :label="t('dialog.avatar.info.header')"> v-model="avatarDialogLastActiveTab"
:items="avatarDialogTabs"
:unmount-on-hide="false"
@update:modelValue="avatarDialogTabClick">
<template #Info>
<div class="x-friend-list" style="max-height: unset"> <div class="x-friend-list" style="max-height: unset">
<div <div
v-if="avatarDialog.galleryImages.length || avatarDialog.ref.authorId === currentUser.id" v-if="avatarDialog.galleryImages.length || avatarDialog.ref.authorId === currentUser.id"
@@ -479,8 +483,8 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="JSON" :label="t('dialog.avatar.json.header')" style="max-height: 50vh" lazy> <template #JSON>
<Button <Button
class="rounded-full h-6 w-6 mr-2" class="rounded-full h-6 w-6 mr-2"
size="icon-sm" size="icon-sm"
@@ -503,8 +507,8 @@
:deep="2" :deep="2"
:theme="isDarkMode ? 'dark' : 'light'" :theme="isDarkMode ? 'dark' : 'light'"
show-icon /> show-icon />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
<template v-if="avatarDialog.visible"> <template v-if="avatarDialog.visible">
<SetAvatarTagsDialog v-model:setAvatarTagsDialog="setAvatarTagsDialog" /> <SetAvatarTagsDialog v-model:setAvatarTagsDialog="setAvatarTagsDialog" />
@@ -535,6 +539,7 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { InputGroupTextareaField } from '@/components/ui/input-group'; import { InputGroupTextareaField } from '@/components/ui/input-group';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -595,6 +600,10 @@
const modalStore = useModalStore(); const modalStore = useModalStore();
const { t } = useI18n(); const { t } = useI18n();
const avatarDialogTabs = computed(() => [
{ value: 'Info', label: t('dialog.avatar.info.header') },
{ value: 'JSON', label: t('dialog.avatar.json.header') }
]);
const avatarDialogIndex = ref(2000); const avatarDialogIndex = ref(2000);
const avatarDialogLastActiveTab = ref('Info'); const avatarDialogLastActiveTab = ref('Info');
@@ -680,12 +689,11 @@
handleAvatarDialogTab(avatarDialogLastActiveTab.value); handleAvatarDialogTab(avatarDialogLastActiveTab.value);
} }
function avatarDialogTabClick(obj) { function avatarDialogTabClick(tabName) {
if (obj.props.name === avatarDialogLastActiveTab.value) { if (tabName === avatarDialogLastActiveTab.value) {
return; return;
} }
handleAvatarDialogTab(obj.props.name); handleAvatarDialogTab(tabName);
avatarDialogLastActiveTab.value = obj.props.name;
} }
function getImageUrlFromImageId(imageId) { function getImageUrlFromImageId(imageId) {
@@ -335,8 +335,12 @@
</div> </div>
</div> </div>
</div> </div>
<el-tabs v-model="groupDialogLastActiveTab" @tab-click="groupDialogTabClick"> <TabsUnderline
<el-tab-pane name="Info" :label="t('dialog.group.info.header')"> v-model="groupDialogLastActiveTab"
:items="groupDialogTabs"
:unmount-on-hide="false"
@update:modelValue="groupDialogTabClick">
<template #Info>
<div class="group-banner-image-info"> <div class="group-banner-image-info">
<img <img
:src="groupDialog.ref.bannerUrl" :src="groupDialog.ref.bannerUrl"
@@ -706,8 +710,8 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="Posts" :label="t('dialog.group.posts.header')" lazy> <template #Posts>
<template v-if="groupDialog.visible"> <template v-if="groupDialog.visible">
<span style="margin-right: 10px; vertical-align: top" <span style="margin-right: 10px; vertical-align: top"
>{{ t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }}</span >{{ t('dialog.group.posts.posts_count') }} {{ groupDialog.posts.length }}</span
@@ -822,8 +826,8 @@
</div> </div>
</div> </div>
</template> </template>
</el-tab-pane> </template>
<el-tab-pane name="Members" :label="t('dialog.group.members.header')" lazy> <template #Members>
<template v-if="groupDialog.visible"> <template v-if="groupDialog.visible">
<span <span
v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')" v-if="hasGroupPermission(groupDialog.ref, 'group-members-viewall')"
@@ -1039,8 +1043,8 @@
</div> </div>
</ul> </ul>
</template> </template>
</el-tab-pane> </template>
<el-tab-pane name="Photos" :label="t('dialog.group.gallery.header')" lazy> <template #Photos>
<Button <Button
class="rounded-full" class="rounded-full"
variant="outline" variant="outline"
@@ -1050,48 +1054,51 @@
<Spinner v-if="isGroupGalleryLoading" /> <Spinner v-if="isGroupGalleryLoading" />
<Refresh v-else /> <Refresh v-else />
</Button> </Button>
<el-tabs <TabsUnderline
v-model="groupDialogGalleryCurrentName" v-model="groupDialogGalleryCurrentName"
:items="groupGalleryTabs"
:unmount-on-hide="false"
v-loading="isGroupGalleryLoading" v-loading="isGroupGalleryLoading"
style="margin-top: 10px"> class="mt-2.5">
<template v-for="(gallery, index) in groupDialog.ref.galleries" :key="index"> <template
<el-tab-pane> v-for="(gallery, index) in groupDialog.ref.galleries"
<template #label> :key="`label-${index}`"
<span style="font-weight: bold; font-size: 16px" v-text="gallery.name" /> v-slot:[`label-${index}`]>
<i <span style="font-weight: bold; font-size: 16px" v-text="gallery.name" />
class="x-status-icon" <i class="x-status-icon" style="margin-left: 5px" :class="groupGalleryStatus(gallery)" />
style="margin-left: 5px" <span style="color: #909399; font-size: 12px; margin-left: 5px">{{
:class="groupGalleryStatus(gallery)" /> groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{ }}</span>
groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0
}}</span>
</template>
<span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" />
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 10px;
max-height: 600px;
overflow-y: auto;
">
<Card
v-for="image in groupDialog.galleries[gallery.id]"
:key="image.id"
class="p-0 overflow-hidden transition-shadow hover:shadow-md">
<img
:src="image.imageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(image.imageUrl)"
loading="lazy" />
</Card>
</div>
</el-tab-pane>
</template> </template>
</el-tabs> <template
</el-tab-pane> v-for="(gallery, index) in groupDialog.ref.galleries"
<el-tab-pane name="JSON" :label="t('dialog.group.json.header')" lazy> :key="`content-${index}`"
v-slot:[String(index)]>
<span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" />
<div
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 10px;
max-height: 600px;
overflow-y: auto;
">
<Card
v-for="image in groupDialog.galleries[gallery.id]"
:key="image.id"
class="p-0 overflow-hidden transition-shadow hover:shadow-md">
<img
:src="image.imageUrl"
:class="['x-link', 'x-popover-image']"
@click="showFullscreenImageDialog(image.imageUrl)"
loading="lazy" />
</Card>
</div>
</template>
</TabsUnderline>
</template>
<template #JSON>
<Button <Button
class="rounded-full h-6 w-6 mr-2" class="rounded-full h-6 w-6 mr-2"
size="icon-sm" size="icon-sm"
@@ -1111,8 +1118,8 @@
:deep="2" :deep="2"
:theme="isDarkMode ? 'dark' : 'light'" :theme="isDarkMode ? 'dark' : 'light'"
show-icon /> show-icon />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
<GroupPostEditDialog :dialog-data="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" /> <GroupPostEditDialog :dialog-data="groupPostEditDialog" :selected-gallery-file="selectedGalleryFile" />
<PreviousInstancesGroupDialog <PreviousInstancesGroupDialog
@@ -1153,6 +1160,7 @@
import { InputGroupField } from '@/components/ui/input-group'; import { InputGroupField } from '@/components/ui/input-group';
import { RefreshCcw } from 'lucide-vue-next'; import { RefreshCcw } from 'lucide-vue-next';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { TabsUnderline } from '@/components/ui/tabs';
import { VirtualCombobox } from '@/components/ui/virtual-combobox'; import { VirtualCombobox } from '@/components/ui/virtual-combobox';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
@@ -1202,6 +1210,19 @@
import * as workerTimers from 'worker-timers'; import * as workerTimers from 'worker-timers';
const { t } = useI18n(); const { t } = useI18n();
const groupDialogTabs = computed(() => [
{ value: 'Info', label: t('dialog.group.info.header') },
{ value: 'Posts', label: t('dialog.group.posts.header') },
{ value: 'Members', label: t('dialog.group.members.header') },
{ value: 'Photos', label: t('dialog.group.gallery.header') },
{ value: 'JSON', label: t('dialog.group.json.header') }
]);
const groupGalleryTabs = computed(() =>
(groupDialog.value?.ref?.galleries || []).map((gallery, index) => ({
value: String(index),
label: gallery?.name ?? ''
}))
);
const modalStore = useModalStore(); const modalStore = useModalStore();
@@ -1654,12 +1675,12 @@
handleGroupDialogTab(groupDialogLastActiveTab.value); handleGroupDialogTab(groupDialogLastActiveTab.value);
} }
function groupDialogTabClick(obj) { function groupDialogTabClick(tabName) {
if (obj.props.name === groupDialogTabCurrentName.value) { if (tabName === groupDialogTabCurrentName.value) {
return; return;
} }
handleGroupDialogTab(obj.props.name); handleGroupDialogTab(tabName);
groupDialogTabCurrentName.value = obj.props.name; groupDialogTabCurrentName.value = tabName;
} }
function showGroupPostEditDialog(groupId, post) { function showGroupPostEditDialog(groupId, post) {
@@ -7,8 +7,12 @@
width="90vw"> width="90vw">
<div> <div>
<h3>{{ groupMemberModeration.groupRef.name }}</h3> <h3>{{ groupMemberModeration.groupRef.name }}</h3>
<el-tabs style="height: 100%"> <TabsUnderline
<el-tab-pane :label="t('dialog.group_member_moderation.members')"> default-value="members"
:items="groupModerationTabs"
:unmount-on-hide="false"
style="height: 100%">
<template #members>
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<Button <Button
class="rounded-full" class="rounded-full"
@@ -124,11 +128,9 @@
:page-sizes="pageSizes" :page-sizes="pageSizes"
:total-items="groupMemberModerationTotalItems" /> :total-items="groupMemberModerationTotalItems" />
</div> </div>
</el-tab-pane> </template>
<el-tab-pane <template #bans>
:label="t('dialog.group_member_moderation.bans')"
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-bans-manage')">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<Button <Button
class="rounded-full" class="rounded-full"
@@ -159,11 +161,9 @@
:page-sizes="pageSizes" :page-sizes="pageSizes"
:total-items="groupBansModerationTotalItems" /> :total-items="groupBansModerationTotalItems" />
</div> </div>
</el-tab-pane> </template>
<el-tab-pane <template #invites>
:label="t('dialog.group_member_moderation.invites')"
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-invites-manage')">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<Button <Button
class="rounded-full" class="rounded-full"
@@ -175,16 +175,32 @@
<Refresh v-else /> <Refresh v-else />
</Button> </Button>
<br /> <br />
<el-tabs> <TabsUnderline default-value="sent" :items="groupInvitesTabs" :unmount-on-hide="false">
<el-tab-pane> <template #label-sent>
<template #label> <span style="font-weight: bold; font-size: 16px">{{
<span style="font-weight: bold; font-size: 16px">{{ t('dialog.group_member_moderation.sent_invites')
t('dialog.group_member_moderation.sent_invites') }}</span>
}}</span> <span style="color: #909399; font-size: 12px; margin-left: 5px">{{
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{ groupInvitesModerationTable.data.length
groupInvitesModerationTable.data.length }}</span>
}}</span> </template>
</template> <template #label-join>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.join_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupJoinRequestsModerationTable.data.length
}}</span>
</template>
<template #label-blocked>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.blocked_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupBlockedModerationTable.data.length
}}</span>
</template>
<template #sent>
<Button size="sm" variant="outline" @click="selectAllGroupInvites">{{ <Button size="sm" variant="outline" @click="selectAllGroupInvites">{{
t('dialog.group_member_moderation.select_all') t('dialog.group_member_moderation.select_all')
}}</Button> }}</Button>
@@ -206,17 +222,9 @@
@click="groupMembersDeleteSentInvite" @click="groupMembersDeleteSentInvite"
>{{ t('dialog.group_member_moderation.delete_sent_invite') }}</Button >{{ t('dialog.group_member_moderation.delete_sent_invite') }}</Button
> >
</el-tab-pane> </template>
<el-tab-pane> <template #join>
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.join_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupJoinRequestsModerationTable.data.length
}}</span>
</template>
<Button size="sm" variant="outline" @click="selectAllGroupJoinRequests">{{ <Button size="sm" variant="outline" @click="selectAllGroupJoinRequests">{{
t('dialog.group_member_moderation.select_all') t('dialog.group_member_moderation.select_all')
}}</Button> }}</Button>
@@ -262,17 +270,9 @@
@click="groupMembersBlockJoinRequest" @click="groupMembersBlockJoinRequest"
>{{ t('dialog.group_member_moderation.block_join_requests') }}</Button >{{ t('dialog.group_member_moderation.block_join_requests') }}</Button
> >
</el-tab-pane> </template>
<el-tab-pane> <template #blocked>
<template #label>
<span style="font-weight: bold; font-size: 16px">{{
t('dialog.group_member_moderation.blocked_requests')
}}</span>
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupBlockedModerationTable.data.length
}}</span>
</template>
<Button size="sm" variant="outline" @click="selectAllGroupBlocked">{{ <Button size="sm" variant="outline" @click="selectAllGroupBlocked">{{
t('dialog.group_member_moderation.select_all') t('dialog.group_member_moderation.select_all')
}}</Button> }}</Button>
@@ -294,14 +294,12 @@
@click="groupMembersDeleteBlockedRequest" @click="groupMembersDeleteBlockedRequest"
>{{ t('dialog.group_member_moderation.delete_blocked_requests') }}</Button >{{ t('dialog.group_member_moderation.delete_blocked_requests') }}</Button
> >
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane <template #logs>
:label="t('dialog.group_member_moderation.logs')"
:disabled="!hasGroupPermission(groupMemberModeration.groupRef, 'group-audit-view')">
<div style="margin-top: 10px"> <div style="margin-top: 10px">
<Button <Button
class="rounded-full" class="rounded-full"
@@ -352,8 +350,8 @@
:page-sizes="pageSizes" :page-sizes="pageSizes"
:total-items="groupLogsModerationTotalItems" /> :total-items="groupLogsModerationTotalItems" />
</div> </div>
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
<br /> <br />
<br /> <br />
@@ -533,6 +531,7 @@
import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group'; import { InputGroupField, InputGroupTextareaField } from '@/components/ui/input-group';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { TabsUnderline } from '@/components/ui/tabs';
import { Trash2 } from 'lucide-vue-next'; import { Trash2 } from 'lucide-vue-next';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
@@ -566,6 +565,29 @@
const { applyGroupMember, handleGroupMember, handleGroupMemberProps } = useGroupStore(); const { applyGroupMember, handleGroupMember, handleGroupMemberProps } = useGroupStore();
const { showFullscreenImageDialog } = useGalleryStore(); const { showFullscreenImageDialog } = useGalleryStore();
const { t } = useI18n(); const { t } = useI18n();
const groupModerationTabs = computed(() => [
{ value: 'members', label: t('dialog.group_member_moderation.members') },
{
value: 'bans',
label: t('dialog.group_member_moderation.bans'),
disabled: !hasGroupPermission(groupMemberModeration.value?.groupRef, 'group-bans-manage')
},
{
value: 'invites',
label: t('dialog.group_member_moderation.invites'),
disabled: !hasGroupPermission(groupMemberModeration.value?.groupRef, 'group-invites-manage')
},
{
value: 'logs',
label: t('dialog.group_member_moderation.logs'),
disabled: !hasGroupPermission(groupMemberModeration.value?.groupRef, 'group-audit-view')
}
]);
const groupInvitesTabs = computed(() => [
{ value: 'sent', label: t('dialog.group_member_moderation.sent_invites') },
{ value: 'join', label: t('dialog.group_member_moderation.join_requests') },
{ value: 'blocked', label: t('dialog.group_member_moderation.blocked_requests') }
]);
const selectedUsers = reactive({}); const selectedUsers = reactive({});
const selectedUsersArray = ref([]); const selectedUsersArray = ref([]);
const isGroupMembersLoading = ref(false); const isGroupMembersLoading = ref(false);
+17 -8
View File
@@ -5,8 +5,12 @@
:title="t('dialog.new_instance.header')" :title="t('dialog.new_instance.header')"
width="650px" width="650px"
append-to-body> append-to-body>
<el-tabs v-model="newInstanceDialog.selectedTab" @tab-click="newInstanceTabClick"> <TabsUnderline
<el-tab-pane name="Normal" :label="t('dialog.new_instance.normal')"> v-model="newInstanceDialog.selectedTab"
:items="newInstanceTabs"
:unmount-on-hide="false"
@update:modelValue="newInstanceTabClick">
<template #Normal>
<FieldGroup class="gap-4"> <FieldGroup class="gap-4">
<Field> <Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel> <FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
@@ -221,8 +225,8 @@
</Field> </Field>
</template> </template>
</FieldGroup> </FieldGroup>
</el-tab-pane> </template>
<el-tab-pane name="Legacy" :label="t('dialog.new_instance.legacy')"> <template #Legacy>
<FieldGroup class="gap-4"> <FieldGroup class="gap-4">
<Field> <Field>
<FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel> <FieldLabel>{{ t('dialog.new_instance.access_type') }}</FieldLabel>
@@ -429,8 +433,8 @@
</FieldContent> </FieldContent>
</Field> </Field>
</FieldGroup> </FieldGroup>
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
<template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer> <template v-if="newInstanceDialog.selectedTab === 'Normal'" #footer>
<template v-if="newInstanceDialog.instanceCreated"> <template v-if="newInstanceDialog.instanceCreated">
<Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{ <Button variant="outline" class="mr-2" @click="copyInstanceUrl(newInstanceDialog.location)">{{
@@ -514,6 +518,7 @@
import { Check as CheckIcon } from 'lucide-vue-next'; import { Check as CheckIcon } from 'lucide-vue-next';
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 { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -551,6 +556,10 @@
required: true required: true
} }
}); });
const newInstanceTabs = computed(() => [
{ value: 'Normal', label: t('dialog.new_instance.normal') },
{ value: 'Legacy', label: t('dialog.new_instance.legacy') }
]);
const { t } = useI18n(); const { t } = useI18n();
@@ -858,8 +867,8 @@
configRepository.setBool('instanceDialogAgeGate', ageGate); configRepository.setBool('instanceDialogAgeGate', ageGate);
configRepository.setString('instanceDialogDisplayName', displayName); configRepository.setString('instanceDialogDisplayName', displayName);
} }
function newInstanceTabClick(obj) { function newInstanceTabClick(tabName) {
if (obj.props.name === 'Normal') { if (tabName === 'Normal') {
buildInstance(); buildInstance();
} else { } else {
buildLegacyInstance(); buildLegacyInstance();
@@ -14,8 +14,12 @@
:toggle-badge-showcased="toggleBadgeShowcased" :toggle-badge-showcased="toggleBadgeShowcased"
:user-dialog-command="userDialogCommand" /> :user-dialog-command="userDialogCommand" />
<el-tabs v-model="userDialogLastActiveTab" @tab-click="userDialogTabClick"> <TabsUnderline
<el-tab-pane name="Info" :label="t('dialog.user.info.header')"> v-model="userDialogLastActiveTab"
:items="userDialogTabs"
:unmount-on-hide="false"
@update:modelValue="userDialogTabClick">
<template #Info>
<template v-if="isFriendOnline(userDialog.friend) || currentUser.id === userDialog.id"> <template v-if="isFriendOnline(userDialog.friend) || currentUser.id === userDialog.id">
<div <div
v-if="userDialog.ref.location" v-if="userDialog.ref.location"
@@ -546,13 +550,9 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane <template v-if="userDialog.id !== currentUser.id && !currentUser.hasSharedConnectionsOptOut" #mutual>
name="Mutual Friends"
v-if="userDialog.id !== currentUser.id && !currentUser.hasSharedConnectionsOptOut"
:label="t('dialog.user.mutual_friends.header')"
lazy>
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<Button <Button
@@ -621,9 +621,9 @@
</div> </div>
</li> </li>
</ul> </ul>
</el-tab-pane> </template>
<el-tab-pane name="Groups" :label="t('dialog.user.groups.header')" lazy> <template #Groups>
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<Button <Button
@@ -1012,9 +1012,9 @@
</template> </template>
</template> </template>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="Worlds" :label="t('dialog.user.worlds.header')" lazy> <template #Worlds>
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<Button <Button
@@ -1085,9 +1085,9 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="Favorite Worlds" :label="t('dialog.user.favorite_worlds.header')" lazy> <template #favorite-worlds>
<!-- <Button <!-- <Button
variant="outline" variant="outline"
v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0" v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0"
@@ -1099,33 +1099,35 @@
style="position: absolute; right: 15px; bottom: 15px; z-index: 99" style="position: absolute; right: 15px; bottom: 15px; z-index: 99"
@click="getUserFavoriteWorlds(userDialog.id)"> @click="getUserFavoriteWorlds(userDialog.id)">
</Button> --> </Button> -->
<el-tabs <template v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0">
ref="favoriteWorldsRef" <TabsUnderline
v-loading="userDialog.isFavoriteWorldsLoading" v-model="favoriteWorldsTab"
class="zero-margin-tabs" :items="favoriteWorldTabs"
type="card" :unmount-on-hide="false"
stretch v-loading="userDialog.isFavoriteWorldsLoading"
style="margin-top: 10px; height: 50vh"> class="zero-margin-tabs"
<template v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0"> style="margin-top: 10px; height: 50vh">
<el-tab-pane v-for="(list, index) in userFavoriteWorlds" :key="index" lazy> <template
<template #label> v-for="(list, index) in userFavoriteWorlds"
<span> :key="`favorite-worlds-label-${index}`"
<i v-slot:[`label-${index}`]>
class="x-status-icon" <span>
style="margin-right: 6px" <i
:class="userFavoriteWorldsStatus(list[1])"> class="x-status-icon"
</i> style="margin-right: 6px"
<span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span> :class="userFavoriteWorldsStatus(list[1])">
<span </i>
style=" <span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span>
color: var(--el-text-color-secondary); <span
font-size: 10px; style="color: var(--el-text-color-secondary); font-size: 10px; margin-left: 5px"
margin-left: 5px; >{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span
" >
>{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span </span>
> </template>
</span> <template
</template> v-for="(list, index) in userFavoriteWorlds"
:key="`favorite-worlds-content-${index}`"
v-slot:[String(index)]>
<div <div
class="x-friend-list" class="x-friend-list"
style="margin-top: 10px; margin-bottom: 15px; min-height: 60px; max-height: none"> style="margin-top: 10px; margin-bottom: 15px; min-height: 60px; max-height: none">
@@ -1143,17 +1145,17 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
</template> </TabsUnderline>
<template v-else-if="!userDialog.isFavoriteWorldsLoading"> </template>
<div style="display: flex; justify-content: center; align-items: center; height: 100%"> <template v-else-if="!userDialog.isFavoriteWorldsLoading">
<span style="font-size: 16px">No favorite worlds found.</span> <div style="display: flex; justify-content: center; align-items: center; height: 100%">
</div> <span style="font-size: 16px">No favorite worlds found.</span>
</template> </div>
</el-tabs> </template>
</el-tab-pane> </template>
<el-tab-pane name="Avatars" :label="t('dialog.user.avatars.header')" lazy> <template #Avatars>
<div style="display: flex; align-items: center; justify-content: space-between"> <div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
<Button <Button
@@ -1250,9 +1252,9 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="JSON" :label="t('dialog.user.json.header')" lazy style="height: 50vh"> <template #JSON>
<Button <Button
class="rounded-full h-6 w-6 mr-2" class="rounded-full h-6 w-6 mr-2"
size="icon-sm" size="icon-sm"
@@ -1272,8 +1274,8 @@
:deep="2" :deep="2"
:theme="isDarkMode ? 'dark' : 'light'" :theme="isDarkMode ? 'dark' : 'light'"
show-icon /> show-icon />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
<SendInviteDialog <SendInviteDialog
v-model:sendInviteDialogVisible="sendInviteDialogVisible" v-model:sendInviteDialogVisible="sendInviteDialogVisible"
@@ -1319,6 +1321,7 @@
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';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -1392,6 +1395,26 @@
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue')); const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
const { t } = useI18n(); const { t } = useI18n();
const userDialogTabs = computed(() => {
const tabs = [
{ value: 'Info', label: t('dialog.user.info.header') },
{ value: 'Groups', label: t('dialog.user.groups.header') },
{ value: 'Worlds', label: t('dialog.user.worlds.header') },
{ value: 'favorite-worlds', label: t('dialog.user.favorite_worlds.header') },
{ value: 'Avatars', label: t('dialog.user.avatars.header') },
{ value: 'JSON', label: t('dialog.user.json.header') }
];
if (userDialog.value.id !== currentUser.value.id && !currentUser.value.hasSharedConnectionsOptOut) {
tabs.splice(1, 0, { value: 'mutual', label: t('dialog.user.mutual_friends.header') });
}
return tabs;
});
const favoriteWorldTabs = computed(() =>
(userFavoriteWorlds.value || []).map((list, index) => ({
value: String(index),
label: list?.[0] ?? ''
}))
);
const modalStore = useModalStore(); const modalStore = useModalStore();
@@ -1476,7 +1499,7 @@
remainingGroups: [] remainingGroups: []
}); });
const favoriteWorldsRef = ref(null); const favoriteWorldsTab = ref('0');
const sendInviteDialogVisible = ref(false); const sendInviteDialogVisible = ref(false);
const sendInviteDialog = ref({ const sendInviteDialog = ref({
@@ -1588,7 +1611,7 @@
if (vrchatCredit.value === null) { if (vrchatCredit.value === null) {
getVRChatCredits(); getVRChatCredits();
} }
} else if (tabName === 'Mutual Friends') { } else if (tabName === 'mutual') {
if (userId === currentUser.value.id) { if (userId === currentUser.value.id) {
userDialogLastActiveTab.value = 'Info'; userDialogLastActiveTab.value = 'Info';
return; return;
@@ -1618,7 +1641,7 @@
userDialogLastWorld.value = userId; userDialogLastWorld.value = userId;
refreshUserDialogWorlds(); refreshUserDialogWorlds();
} }
} else if (tabName === 'Favorite Worlds') { } else if (tabName === 'favorite-worlds') {
if (userDialogLastFavoriteWorld.value !== userId) { if (userDialogLastFavoriteWorld.value !== userId) {
userDialogLastFavoriteWorld.value = userId; userDialogLastFavoriteWorld.value = userId;
getUserFavoriteWorlds(userId); getUserFavoriteWorlds(userId);
@@ -1632,12 +1655,11 @@
handleUserDialogTab(userDialogLastActiveTab.value); handleUserDialogTab(userDialogLastActiveTab.value);
} }
function userDialogTabClick(obj) { function userDialogTabClick(tabName) {
if (obj.props.name === userDialogLastActiveTab.value) { if (tabName === userDialogLastActiveTab.value) {
return; return;
} }
handleUserDialogTab(obj.props.name); handleUserDialogTab(tabName);
userDialogLastActiveTab.value = obj.props.name;
} }
function showPronounsDialog() { function showPronounsDialog() {
@@ -2318,9 +2340,7 @@
async function getUserFavoriteWorlds(userId) { async function getUserFavoriteWorlds(userId) {
userDialog.value.isFavoriteWorldsLoading = true; userDialog.value.isFavoriteWorldsLoading = true;
if (favoriteWorldsRef.value) { favoriteWorldsTab.value = '0';
favoriteWorldsRef.value.currentName = '0'; // select first tab
}
userFavoriteWorlds.value = []; userFavoriteWorlds.value = [];
const worldLists = []; const worldLists = [];
let params = { let params = {
@@ -314,8 +314,12 @@
</div> </div>
</div> </div>
</div> </div>
<el-tabs v-model="worldDialogLastActiveTab" @tab-click="worldDialogTabClick"> <TabsUnderline
<el-tab-pane name="Instances" :label="t('dialog.world.instances.header')"> v-model="worldDialogLastActiveTab"
:items="worldDialogTabs"
:unmount-on-hide="false"
@update:modelValue="worldDialogTabClick">
<template #Instances>
<div class=""> <div class="">
<el-icon><User /></el-icon> <el-icon><User /></el-icon>
{{ t('dialog.world.instances.public_count', { count: worldDialog.ref.publicOccupants }) }} {{ t('dialog.world.instances.public_count', { count: worldDialog.ref.publicOccupants }) }}
@@ -429,8 +433,8 @@
</div> </div>
</template> </template>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="Info" :label="t('dialog.world.info.header')" lazy> <template #Info>
<div class="x-friend-list" style="max-height: none"> <div class="x-friend-list" style="max-height: none">
<div class="x-friend-item" style="width: 100%; cursor: default"> <div class="x-friend-item" style="width: 100%; cursor: default">
<div class="detail"> <div class="detail">
@@ -695,8 +699,8 @@
</div> </div>
</div> </div>
</div> </div>
</el-tab-pane> </template>
<el-tab-pane name="JSON" :label="t('dialog.world.json.header')" style="max-height: 50vh" lazy> <template #JSON>
<Button <Button
class="rounded-full h-6 w-6 mr-2" class="rounded-full h-6 w-6 mr-2"
size="icon-sm" size="icon-sm"
@@ -719,8 +723,8 @@
:deep="2" :deep="2"
:theme="isDarkMode ? 'dark' : 'light'" :theme="isDarkMode ? 'dark' : 'light'"
show-icon /> show-icon />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
<template v-if="isDialogVisible"> <template v-if="isDialogVisible">
@@ -768,6 +772,7 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ElMessageBox } from 'element-plus'; import { ElMessageBox } from 'element-plus';
import { InputGroupTextareaField } from '@/components/ui/input-group'; import { InputGroupTextareaField } from '@/components/ui/input-group';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -834,6 +839,11 @@
const { isGameRunning } = storeToRefs(useGameStore()); const { isGameRunning } = storeToRefs(useGameStore());
const { showFullscreenImageDialog } = useGalleryStore(); const { showFullscreenImageDialog } = useGalleryStore();
const { t } = useI18n(); const { t } = useI18n();
const worldDialogTabs = computed(() => [
{ value: 'Instances', label: t('dialog.world.instances.header') },
{ value: 'Info', label: t('dialog.world.info.header') },
{ value: 'JSON', label: t('dialog.world.json.header') }
]);
const treeData = ref({}); const treeData = ref({});
const worldAllowedDomainsDialog = ref({ const worldAllowedDomainsDialog = ref({
@@ -954,12 +964,11 @@
handleWorldDialogTab(worldDialogLastActiveTab.value); handleWorldDialogTab(worldDialogLastActiveTab.value);
} }
function worldDialogTabClick(obj) { function worldDialogTabClick(tabName) {
if (obj.props.name === worldDialogLastActiveTab.value) { if (tabName === worldDialogLastActiveTab.value) {
return; return;
} }
handleWorldDialogTab(obj.props.name); handleWorldDialogTab(tabName);
worldDialogLastActiveTab.value = obj.props.name;
} }
function handleDialogOpen() { function handleDialogOpen() {
+109
View File
@@ -0,0 +1,109 @@
<script setup>
import { TabsContent, TabsIndicator, TabsList, TabsRoot, TabsTrigger } from 'reka-ui';
import { computed, ref, toRefs, watch } from 'vue';
const props = defineProps({
modelValue: String,
defaultValue: String,
items: {
type: Array,
required: true,
validator: (value) =>
Array.isArray(value) &&
value.every(
(item) =>
item &&
(typeof item.value === 'string' || typeof item.value === 'number') &&
typeof item.label === 'string'
)
},
ariaLabel: { type: String, default: '' },
variant: { type: String, default: 'fit' },
unmountOnHide: { type: Boolean, default: false }
});
const emit = defineEmits(['update:modelValue']);
const { modelValue, defaultValue, items, ariaLabel, variant, unmountOnHide } = toRefs(props);
const resolvedDefault = computed(() => {
return defaultValue.value ?? items.value?.[0]?.value;
});
const isValueValid = (value) => items.value?.some((item) => item?.value === value);
const innerValue = ref(isValueValid(modelValue.value) ? modelValue.value : resolvedDefault.value);
watch(modelValue, (v) => {
if (isValueValid(v)) {
innerValue.value = v;
}
});
watch([items, defaultValue], () => {
if (!isValueValid(innerValue.value)) {
innerValue.value = resolvedDefault.value;
return;
}
if (!isValueValid(modelValue.value)) {
innerValue.value = resolvedDefault.value;
}
});
function onValueChange(v) {
innerValue.value = v;
emit('update:modelValue', v);
}
const triggerClass = computed(() => {
return [
'relative inline-flex h-10 items-center justify-center px-3 text-sm font-medium',
'text-muted-foreground transition-colors hover:text-foreground',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background',
'disabled:pointer-events-none disabled:opacity-50',
'data-[state=active]:text-primary',
variant.value === 'equal' ? 'flex-1' : '',
variant.value === 'pill' ? 'rounded-full' : ''
].join(' ');
});
const listClass = computed(() => {
return [
'relative flex w-full items-center gap-1 border-b border-border',
variant.value === 'pill' ? 'rounded-full bg-muted p-1' : ''
].join(' ');
});
</script>
<template>
<TabsRoot
:model-value="innerValue"
:default-value="resolvedDefault"
class="w-full"
:unmount-on-hide="unmountOnHide"
@update:modelValue="onValueChange">
<TabsList :class="listClass" :aria-label="ariaLabel || undefined">
<TabsIndicator
class="pointer-events-none absolute left-0 bottom-0 z-20 h-0.5 w-(--reka-tabs-indicator-size) translate-x-(--reka-tabs-indicator-position) transition-[width,translate] duration-200 ease-out">
<div class="h-full w-full rounded-full bg-primary" />
</TabsIndicator>
<TabsTrigger
v-for="it in items"
:key="it.value"
:value="it.value"
:disabled="it.disabled"
:class="triggerClass">
<slot :name="`label-${it.value}`">{{ it.label }}</slot>
</TabsTrigger>
</TabsList>
<TabsContent
v-for="it in items"
:key="it.value"
:value="it.value"
class="pt-4 outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background">
<slot :name="it.value" />
</TabsContent>
</TabsRoot>
</template>
+1
View File
@@ -2,3 +2,4 @@ export { default as Tabs } from './Tabs.vue';
export { default as TabsContent } from './TabsContent.vue'; export { default as TabsContent } from './TabsContent.vue';
export { default as TabsList } from './TabsList.vue'; export { default as TabsList } from './TabsList.vue';
export { default as TabsTrigger } from './TabsTrigger.vue'; export { default as TabsTrigger } from './TabsTrigger.vue';
export { default as TabsUnderline } from './TabsUnderline.vue';
+15 -12
View File
@@ -1,21 +1,20 @@
<template> <template>
<div id="chart" class="x-container"> <div id="chart" class="x-container">
<el-tabs v-model="activeTab" class="charts-tabs"> <TabsUnderline v-model="activeTab" :items="chartTabs" :unmount-on-hide="false" class="charts-tabs">
<el-tab-pane :label="t('view.charts.instance_activity.header')" name="instance"></el-tab-pane> <template #instance>
<el-tab-pane :label="t('view.charts.mutual_friend.tab_label')" name="mutual"></el-tab-pane> <InstanceActivity />
</el-tabs> </template>
<div v-show="activeTab === 'instance'"> <template #mutual>
<InstanceActivity /> <MutualFriends />
</div> </template>
<div v-show="activeTab === 'mutual'"> </TabsUnderline>
<MutualFriends />
</div>
<el-backtop target="#chart" :right="30" :bottom="30"></el-backtop> <el-backtop target="#chart" :right="30" :bottom="30"></el-backtop>
</div> </div>
</template> </template>
<script setup> <script setup>
import { defineAsyncComponent } from 'vue'; import { computed, defineAsyncComponent } from 'vue';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -27,10 +26,14 @@
const { t } = useI18n(); const { t } = useI18n();
const chartsStore = useChartsStore(); const chartsStore = useChartsStore();
const { activeTab } = storeToRefs(chartsStore); const { activeTab } = storeToRefs(chartsStore);
const chartTabs = computed(() => [
{ value: 'instance', label: t('view.charts.instance_activity.header') },
{ value: 'mutual', label: t('view.charts.mutual_friend.tab_label') }
]);
</script> </script>
<style scoped> <style scoped>
:deep(.el-tabs__header) { :deep(.charts-tabs [data-slot='tabs-list']) {
margin: 0; margin: 0;
} }
</style> </style>
@@ -1,12 +1,15 @@
<template> <template>
<div class="friend-view x-container"> <div class="friend-view x-container">
<div v-if="settingsReady" class="friend-view__toolbar"> <div v-if="settingsReady" class="friend-view__toolbar">
<el-segmented v-model="activeSegment" :options="segmentedOptions" /> <Tabs v-model="activeSegment" class="friend-view__tabs">
<TabsList>
<TabsTrigger v-for="option in segmentedOptions" :key="option.value" :value="option.value">
{{ option.label }}
</TabsTrigger>
</TabsList>
</Tabs>
<div class="friend-view__actions"> <div class="friend-view__actions">
<InputGroupSearch <InputGroupSearch v-model="searchTerm" class="friend-view__search" placeholder="Search Friend" />
v-model="searchTerm"
class="friend-view__search"
placeholder="Search Friend" />
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<div> <div>
@@ -161,9 +164,10 @@
<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 { Loading } from '@element-plus/icons-vue'; import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { InputGroupSearch } from '@/components/ui/input-group'; import { InputGroupSearch } from '@/components/ui/input-group';
import { Loading } from '@element-plus/icons-vue';
import { Settings } from 'lucide-vue-next'; import { Settings } from 'lucide-vue-next';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -724,6 +728,10 @@
padding: 6px 2px 0 2px; padding: 6px 2px 0 2px;
} }
.friend-view__tabs {
gap: 0;
}
.friend-view__toolbar--loading { .friend-view__toolbar--loading {
justify-content: flex-end; justify-content: flex-end;
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
@@ -36,8 +36,8 @@
</div> </div>
</TooltipWrapper> </TooltipWrapper>
</div> </div>
<el-tabs type="card"> <TabsUnderline default-value="current" :items="photonTabs" :unmount-on-hide="false">
<el-tab-pane :label="t('view.player_list.photon.current')"> <template #current>
<DataTableLayout <DataTableLayout
class="min-w-0 w-full" class="min-w-0 w-full"
:table="currentTable" :table="currentTable"
@@ -47,8 +47,8 @@
:total-items="currentTotal" :total-items="currentTotal"
:on-page-size-change="handleCurrentPageSizeChange" :on-page-size-change="handleCurrentPageSizeChange"
style="margin-bottom: 10px" /> style="margin-bottom: 10px" />
</el-tab-pane> </template>
<el-tab-pane :label="t('view.player_list.photon.previous')"> <template #previous>
<DataTableLayout <DataTableLayout
class="min-w-0 w-full" class="min-w-0 w-full"
:table="previousTable" :table="previousTable"
@@ -58,8 +58,8 @@
:total-items="previousTotal" :total-items="previousTotal"
:on-page-size-change="handlePreviousPageSizeChange" :on-page-size-change="handlePreviousPageSizeChange"
style="margin-bottom: 10px" /> style="margin-bottom: 10px" />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
</template> </template>
@@ -68,6 +68,7 @@
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table'; import { DataTableLayout } from '@/components/ui/data-table';
import { InputGroupField } from '@/components/ui/input-group'; import { InputGroupField } from '@/components/ui/input-group';
import { TabsUnderline } from '@/components/ui/tabs';
import { localeIncludes } from '@/shared/utils'; import { localeIncludes } from '@/shared/utils';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -91,6 +92,10 @@
const { t } = useI18n(); const { t } = useI18n();
const photonStore = usePhotonStore(); const photonStore = usePhotonStore();
const photonTabs = computed(() => [
{ value: 'current', label: t('view.player_list.photon.current') },
{ value: 'previous', label: t('view.player_list.photon.previous') }
]);
const { const {
photonEventTableTypeFilter, photonEventTableTypeFilter,
photonEventTableFilter, photonEventTableFilter,
+333 -309
View File
@@ -14,329 +14,341 @@
/></Button> /></Button>
</TooltipWrapper> </TooltipWrapper>
</div> </div>
<el-tabs ref="searchTabRef" style="margin-top: 15px" @tab-click="searchText = ''"> <TabsUnderline
<el-tab-pane v-loading="isSearchUserLoading" :label="t('view.search.user.header')" style="min-height: 60px"> v-model="activeSearchTab"
<label class="inline-flex items-center gap-2" style="margin-left: 10px"> :items="searchTabs"
<Checkbox v-model="searchUserByBio" /> aria-label="Search tabs"
<span>{{ t('view.search.user.search_by_bio') }}</span> :unmount-on-hide="false"
</label> style="margin-top: 15px">
<label class="inline-flex items-center gap-2" style="margin-left: 10px"> <template #user>
<Checkbox v-model="searchUserSortByLastLoggedIn" /> <div v-loading="isSearchUserLoading" style="min-height: 60px">
<span>{{ t('view.search.user.sort_by_last_logged_in') }}</span>
</label>
<div class="x-friend-list" style="min-height: 500px">
<div
v-for="user in searchUserResults"
:key="user.id"
class="x-friend-item"
@click="showUserDialog(user.id)">
<div class="avatar">
<img :src="userImage(user, true)" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="user.displayName"></span>
<span
v-if="randomUserColours"
class="extra"
:class="user.$trustClass"
v-text="user.$trustLevel"></span>
<span
v-else
class="extra"
:style="{ color: user.$userColour }"
v-text="user.$trustLevel"></span>
</div>
</div>
</div>
<ButtonGroup v-if="searchUserResults.length" style="margin-top: 15px">
<Button
variant="outline"
size="sm"
:disabled="!searchUserParams.offset"
@click="handleMoreSearchUser(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="searchUserResults.length < 10"
@click="handleMoreSearchUser(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</el-tab-pane>
<el-tab-pane
v-loading="isSearchWorldLoading"
:label="t('view.search.world.header')"
style="min-height: 60px">
<div class="inline-flex justify-between mb-4 w-full">
<Select
:model-value="searchWorldCategoryIndex"
@update:modelValue="handleSearchWorldCategorySelect"
style="margin-bottom: 15px">
<SelectTrigger size="sm">
<SelectValue :placeholder="t('view.search.world.category')" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="row in cachedConfig.dynamicWorldRows"
:key="row.index"
:value="row.index">
{{ row.name }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<label class="inline-flex items-center gap-2" style="margin-left: 10px"> <label class="inline-flex items-center gap-2" style="margin-left: 10px">
<Checkbox v-model="searchWorldLabs" /> <Checkbox v-model="searchUserByBio" />
<span>{{ t('view.search.world.community_lab') }}</span> <span>{{ t('view.search.user.search_by_bio') }}</span>
</label> </label>
</div> <label class="inline-flex items-center gap-2" style="margin-left: 10px">
<div class="x-friend-list" style="min-height: 500px"> <Checkbox v-model="searchUserSortByLastLoggedIn" />
<div <span>{{ t('view.search.user.sort_by_last_logged_in') }}</span>
v-for="world in searchWorldResults" </label>
:key="world.id" <div class="x-friend-list" style="min-height: 500px">
class="x-friend-item" <div
@click="showWorldDialog(world.id)"> v-for="user in searchUserResults"
<div class="avatar"> :key="user.id"
<img :src="world.thumbnailImageUrl" loading="lazy" /> class="x-friend-item"
</div> @click="showUserDialog(user.id)">
<div class="detail"> <div class="avatar">
<span class="name" v-text="world.name"></span> <img :src="userImage(user, true)" loading="lazy" />
<span v-if="world.occupants" class="extra" </div>
>{{ world.authorName }} ({{ world.occupants }})</span <div class="detail">
> <span class="name" v-text="user.displayName"></span>
<span v-else class="extra" v-text="world.authorName"></span> <span
v-if="randomUserColours"
class="extra"
:class="user.$trustClass"
v-text="user.$trustLevel"></span>
<span
v-else
class="extra"
:style="{ color: user.$userColour }"
v-text="user.$trustLevel"></span>
</div>
</div> </div>
</div> </div>
<ButtonGroup v-if="searchUserResults.length" style="margin-top: 15px">
<Button
variant="outline"
size="sm"
:disabled="!searchUserParams.offset"
@click="handleMoreSearchUser(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="searchUserResults.length < 10"
@click="handleMoreSearchUser(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</div> </div>
<ButtonGroup v-if="searchWorldResults.length" style="margin-top: 15px"> </template>
<Button <template #world>
variant="outline" <div v-loading="isSearchWorldLoading" style="min-height: 60px">
size="sm" <div class="inline-flex justify-between mb-4 w-full">
:disabled="!searchWorldParams.offset"
@click="moreSearchWorld(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="searchWorldResults.length < 10"
@click="moreSearchWorld(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</el-tab-pane>
<el-tab-pane
v-loading="isSearchAvatarLoading"
:label="t('view.search.avatar.header')"
style="min-height: 60px">
<div style="display: flex; align-items: center; justify-content: space-between">
<div style="display: flex; align-items: center">
<Select <Select
v-if="avatarRemoteDatabaseProviderList.length > 1" :model-value="searchWorldCategoryIndex"
:model-value="avatarRemoteDatabaseProvider" @update:modelValue="handleSearchWorldCategorySelect"
@update:modelValue="setAvatarProvider" style="margin-bottom: 15px">
style="margin-right: 5px">
<SelectTrigger size="sm"> <SelectTrigger size="sm">
<SelectValue :placeholder="t('view.search.avatar.search_provider')" /> <SelectValue :placeholder="t('view.search.world.category')" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectGroup> <SelectGroup>
<SelectItem <SelectItem
v-for="provider in avatarRemoteDatabaseProviderList" v-for="row in cachedConfig.dynamicWorldRows"
:key="provider" :key="row.index"
:value="provider"> :value="row.index">
{{ provider }} {{ row.name }}
</SelectItem> </SelectItem>
</SelectGroup> </SelectGroup>
</SelectContent> </SelectContent>
</Select> </Select>
<TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')"> <label class="inline-flex items-center gap-2" style="margin-left: 10px">
<Button <Checkbox v-model="searchWorldLabs" />
class="rounded-full ml-1" <span>{{ t('view.search.world.community_lab') }}</span>
variant="outline" </label>
size="icon-sm"
:disabled="userDialog.isAvatarsLoading"
@click="refreshUserDialogAvatars">
<Spinner v-if="userDialog.isAvatarsLoading" />
<Refresh v-else />
</Button>
</TooltipWrapper>
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">{{
t('view.search.avatar.result_count', {
count: searchAvatarResults.length
})
}}</span>
</div> </div>
<div style="display: flex; align-items: center"> <div class="x-friend-list" style="min-height: 500px">
<RadioGroup <div
:model-value="searchAvatarFilter" v-for="world in searchWorldResults"
class="flex items-center gap-4" :key="world.id"
style="margin: 5px" class="x-friend-item"
@update:modelValue="handleSearchAvatarFilterChange"> @click="showWorldDialog(world.id)">
<div class="flex items-center space-x-2"> <div class="avatar">
<RadioGroupItem id="searchAvatarFilter-all" value="all" /> <img :src="world.thumbnailImageUrl" loading="lazy" />
<label for="searchAvatarFilter-all">{{ t('view.search.avatar.all') }}</label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="detail">
<RadioGroupItem id="searchAvatarFilter-public" value="public" /> <span class="name" v-text="world.name"></span>
<label for="searchAvatarFilter-public">{{ t('view.search.avatar.public') }}</label> <span v-if="world.occupants" class="extra"
</div> >{{ world.authorName }} ({{ world.occupants }})</span
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilter-private" value="private" />
<label for="searchAvatarFilter-private">{{ t('view.search.avatar.private') }}</label>
</div>
</RadioGroup>
<el-divider direction="vertical"></el-divider>
<RadioGroup
:model-value="searchAvatarFilterRemote"
class="flex items-center gap-4"
style="margin: 5px"
@update:modelValue="handleSearchAvatarFilterRemoteChange">
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilterRemote-all" value="all" />
<label for="searchAvatarFilterRemote-all">{{ t('view.search.avatar.all') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilterRemote-local" value="local" />
<label for="searchAvatarFilterRemote-local">{{ t('view.search.avatar.local') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem
id="searchAvatarFilterRemote-remote"
value="remote"
:disabled="!avatarRemoteDatabase" />
<label for="searchAvatarFilterRemote-remote">{{
t('view.search.avatar.remote')
}}</label>
</div>
</RadioGroup>
</div>
</div>
<div style="display: flex; justify-content: end">
<RadioGroup
:model-value="searchAvatarSort"
:disabled="searchAvatarFilterRemote !== 'local'"
class="flex items-center gap-4"
style="margin: 5px"
@update:modelValue="handleSearchAvatarSortChange">
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarSort-name" value="name" />
<label for="searchAvatarSort-name">{{ t('view.search.avatar.sort_name') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarSort-update" value="update" />
<label for="searchAvatarSort-update">{{ t('view.search.avatar.sort_update') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarSort-created" value="created" />
<label for="searchAvatarSort-created">{{ t('view.search.avatar.sort_created') }}</label>
</div>
</RadioGroup>
</div>
<div class="x-friend-list" style="margin-top: 20px; min-height: 500px">
<div
v-for="avatar in searchAvatarPage"
:key="avatar.id"
class="x-friend-item"
@click="showAvatarDialog(avatar.id)">
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
<img v-else-if="avatar.imageUrl" :src="avatar.imageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
<span
v-if="avatar.releaseStatus === 'public'"
class="extra"
style="color: var(--el-color-success)"
v-text="avatar.releaseStatus"></span>
<span
v-else-if="avatar.releaseStatus === 'private'"
class="extra"
style="color: var(--el-color-danger)"
v-text="avatar.releaseStatus"></span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatar.authorName"></span>
</div>
</div>
</div>
<ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px">
<Button variant="outline" size="sm" :disabled="!searchAvatarPageNum" @click="moreSearchAvatar(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="
searchAvatarResults.length < 10 ||
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
"
@click="moreSearchAvatar(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</el-tab-pane>
<el-tab-pane
v-loading="isSearchGroupLoading"
:label="t('view.search.group.header')"
style="min-height: 60px">
<div class="x-friend-list" style="min-height: 500px">
<div
v-for="group in searchGroupResults"
:key="group.id"
class="x-friend-item"
@click="showGroupDialog(group.id)">
<div class="avatar">
<img :src="getSmallThumbnailUrl(group.iconUrl)" loading="lazy" />
</div>
<div class="detail">
<span class="name">
<span v-text="group.name"></span>
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
<span
style="
margin-left: 5px;
color: #909399;
font-weight: normal;
font-family: monospace;
font-size: 12px;
"
>{{ group.shortCode }}.{{ group.discriminator }}</span
> >
</span> <span v-else class="extra" v-text="world.authorName"></span>
<span class="extra" v-text="group.description"></span> </div>
</div> </div>
</div> </div>
<ButtonGroup v-if="searchWorldResults.length" style="margin-top: 15px">
<Button
variant="outline"
size="sm"
:disabled="!searchWorldParams.offset"
@click="moreSearchWorld(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="searchWorldResults.length < 10"
@click="moreSearchWorld(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</div> </div>
<ButtonGroup v-if="searchGroupResults.length" style="margin-top: 15px"> </template>
<Button <template #avatar>
variant="outline" <div v-loading="isSearchAvatarLoading" style="min-height: 60px">
size="sm" <div style="display: flex; align-items: center; justify-content: space-between">
:disabled="!searchGroupParams.offset" <div style="display: flex; align-items: center">
@click="moreSearchGroup(-1)"> <Select
<Back /> v-if="avatarRemoteDatabaseProviderList.length > 1"
{{ t('view.search.prev_page') }} :model-value="avatarRemoteDatabaseProvider"
</Button> @update:modelValue="setAvatarProvider"
<Button style="margin-right: 5px">
variant="outline" <SelectTrigger size="sm">
size="sm" <SelectValue :placeholder="t('view.search.avatar.search_provider')" />
:disabled="searchGroupResults.length < 10" </SelectTrigger>
@click="moreSearchGroup(1)"> <SelectContent>
<Right /> <SelectGroup>
{{ t('view.search.next_page') }} <SelectItem
</Button> v-for="provider in avatarRemoteDatabaseProviderList"
</ButtonGroup> :key="provider"
</el-tab-pane> :value="provider">
</el-tabs> {{ provider }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<TooltipWrapper side="bottom" :content="t('view.search.avatar.refresh_tooltip')">
<Button
class="rounded-full ml-1"
variant="outline"
size="icon-sm"
:disabled="userDialog.isAvatarsLoading"
@click="refreshUserDialogAvatars">
<Spinner v-if="userDialog.isAvatarsLoading" />
<Refresh v-else />
</Button>
</TooltipWrapper>
<span style="font-size: 14px; margin-left: 5px; margin-right: 5px">{{
t('view.search.avatar.result_count', {
count: searchAvatarResults.length
})
}}</span>
</div>
<div style="display: flex; align-items: center">
<RadioGroup
:model-value="searchAvatarFilter"
class="flex items-center gap-4"
style="margin: 5px"
@update:modelValue="handleSearchAvatarFilterChange">
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilter-all" value="all" />
<label for="searchAvatarFilter-all">{{ t('view.search.avatar.all') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilter-public" value="public" />
<label for="searchAvatarFilter-public">{{ t('view.search.avatar.public') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilter-private" value="private" />
<label for="searchAvatarFilter-private">{{
t('view.search.avatar.private')
}}</label>
</div>
</RadioGroup>
<el-divider direction="vertical"></el-divider>
<RadioGroup
:model-value="searchAvatarFilterRemote"
class="flex items-center gap-4"
style="margin: 5px"
@update:modelValue="handleSearchAvatarFilterRemoteChange">
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilterRemote-all" value="all" />
<label for="searchAvatarFilterRemote-all">{{ t('view.search.avatar.all') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilterRemote-local" value="local" />
<label for="searchAvatarFilterRemote-local">{{
t('view.search.avatar.local')
}}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem
id="searchAvatarFilterRemote-remote"
value="remote"
:disabled="!avatarRemoteDatabase" />
<label for="searchAvatarFilterRemote-remote">{{
t('view.search.avatar.remote')
}}</label>
</div>
</RadioGroup>
</div>
</div>
<div style="display: flex; justify-content: end">
<RadioGroup
:model-value="searchAvatarSort"
:disabled="searchAvatarFilterRemote !== 'local'"
class="flex items-center gap-4"
style="margin: 5px"
@update:modelValue="handleSearchAvatarSortChange">
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarSort-name" value="name" />
<label for="searchAvatarSort-name">{{ t('view.search.avatar.sort_name') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarSort-update" value="update" />
<label for="searchAvatarSort-update">{{ t('view.search.avatar.sort_update') }}</label>
</div>
<div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarSort-created" value="created" />
<label for="searchAvatarSort-created">{{ t('view.search.avatar.sort_created') }}</label>
</div>
</RadioGroup>
</div>
<div class="x-friend-list" style="margin-top: 20px; min-height: 500px">
<div
v-for="avatar in searchAvatarPage"
:key="avatar.id"
class="x-friend-item"
@click="showAvatarDialog(avatar.id)">
<div class="avatar">
<img v-if="avatar.thumbnailImageUrl" :src="avatar.thumbnailImageUrl" loading="lazy" />
<img v-else-if="avatar.imageUrl" :src="avatar.imageUrl" loading="lazy" />
</div>
<div class="detail">
<span class="name" v-text="avatar.name"></span>
<span
v-if="avatar.releaseStatus === 'public'"
class="extra"
style="color: var(--el-color-success)"
v-text="avatar.releaseStatus"></span>
<span
v-else-if="avatar.releaseStatus === 'private'"
class="extra"
style="color: var(--el-color-danger)"
v-text="avatar.releaseStatus"></span>
<span v-else class="extra" v-text="avatar.releaseStatus"></span>
<span class="extra" v-text="avatar.authorName"></span>
</div>
</div>
</div>
<ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px">
<Button
variant="outline"
size="sm"
:disabled="!searchAvatarPageNum"
@click="moreSearchAvatar(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="
searchAvatarResults.length < 10 ||
(searchAvatarPageNum + 1) * 10 >= searchAvatarResults.length
"
@click="moreSearchAvatar(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</div>
</template>
<template #group>
<div v-loading="isSearchGroupLoading" style="min-height: 60px">
<div class="x-friend-list" style="min-height: 500px">
<div
v-for="group in searchGroupResults"
:key="group.id"
class="x-friend-item"
@click="showGroupDialog(group.id)">
<div class="avatar">
<img :src="getSmallThumbnailUrl(group.iconUrl)" loading="lazy" />
</div>
<div class="detail">
<span class="name">
<span v-text="group.name"></span>
<span style="margin-left: 5px; font-weight: normal">({{ group.memberCount }})</span>
<span
style="
margin-left: 5px;
color: #909399;
font-weight: normal;
font-family: monospace;
font-size: 12px;
"
>{{ group.shortCode }}.{{ group.discriminator }}</span
>
</span>
<span class="extra" v-text="group.description"></span>
</div>
</div>
</div>
<ButtonGroup v-if="searchGroupResults.length" style="margin-top: 15px">
<Button
variant="outline"
size="sm"
:disabled="!searchGroupParams.offset"
@click="moreSearchGroup(-1)">
<Back />
{{ t('view.search.prev_page') }}
</Button>
<Button
variant="outline"
size="sm"
:disabled="searchGroupResults.length < 10"
@click="moreSearchGroup(1)">
<Right />
{{ t('view.search.next_page') }}
</Button>
</ButtonGroup>
</div>
</template>
</TabsUnderline>
</div> </div>
</template> </template>
@@ -344,13 +356,14 @@
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Back, Refresh, Right } from '@element-plus/icons-vue'; import { Back, Refresh, Right } from '@element-plus/icons-vue';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { computed, ref } from 'vue';
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 { 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 { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { TabsUnderline } from '@/components/ui/tabs';
import { Trash2 } from 'lucide-vue-next'; import { Trash2 } from 'lucide-vue-next';
import { ref } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -390,7 +403,13 @@
const { t } = useI18n(); const { t } = useI18n();
const searchTabRef = ref(null); const activeSearchTab = ref('user');
const searchTabs = computed(() => [
{ value: 'user', label: t('view.search.user.header') },
{ value: 'world', label: t('view.search.world.header') },
{ value: 'avatar', label: t('view.search.avatar.header') },
{ value: 'group', label: t('view.search.group.header') }
]);
const searchUserParams = ref({}); const searchUserParams = ref({});
const searchUserByBio = ref(false); const searchUserByBio = ref(false);
@@ -453,18 +472,23 @@
searchText.value = text; searchText.value = text;
} }
function handleSearchTabChange(tabName) {
searchText.value = '';
activeSearchTab.value = tabName;
}
function search() { function search() {
switch (searchTabRef.value.currentName) { switch (activeSearchTab.value) {
case '0': case 'user':
searchUser(); searchUser();
break; break;
case '1': case 'world':
searchWorld({}); searchWorld({});
break; break;
case '2': case 'avatar':
searchAvatar(); searchAvatar();
break; break;
case '3': case 'group':
searchGroup(); searchGroup();
break; break;
} }
+31 -17
View File
@@ -3,34 +3,39 @@
<div class="options-container" style="margin-top: 0; padding: 5px"> <div class="options-container" style="margin-top: 0; padding: 5px">
<span class="header">{{ t('view.settings.header') }}</span> <span class="header">{{ t('view.settings.header') }}</span>
</div> </div>
<el-tabs style="height: calc(100% - 51px)"> <TabsUnderline
<el-tab-pane :label="t('view.settings.category.general')"> default-value="general"
:items="settingsTabs"
:unmount-on-hide="false"
style="height: calc(100% - 51px)">
<template #general>
<GeneralTab /> <GeneralTab />
</el-tab-pane> </template>
<el-tab-pane lazy :label="t('view.settings.category.appearance')"> <template #appearance>
<AppearanceTab /> <AppearanceTab />
</el-tab-pane> </template>
<el-tab-pane lazy :label="t('view.settings.category.notifications')"> <template #notifications>
<NotificationsTab /> <NotificationsTab />
</el-tab-pane> </template>
<el-tab-pane lazy :label="t('view.settings.category.wrist_overlay')"> <template #wrist-overlay>
<WristOverlayTab /> <WristOverlayTab />
</el-tab-pane> </template>
<el-tab-pane lazy :label="t('view.settings.category.discord_presence')"> <template #discord>
<DiscordPresenceTab /> <DiscordPresenceTab />
</el-tab-pane> </template>
<el-tab-pane lazy :label="t('view.settings.category.pictures')"> <template #pictures>
<PicturesTab /> <PicturesTab />
</el-tab-pane> </template>
<el-tab-pane lazy :label="t('view.settings.category.advanced')"> <template #advanced>
<AdvancedTab /> <AdvancedTab />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onBeforeMount } from 'vue'; import { computed, onBeforeMount } from 'vue';
import { TabsUnderline } from '@/components/ui/tabs';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import AdvancedTab from './components/Tabs/AdvancedTab.vue'; import AdvancedTab from './components/Tabs/AdvancedTab.vue';
@@ -42,6 +47,15 @@
import WristOverlayTab from './components/Tabs/WristOverlayTab.vue'; import WristOverlayTab from './components/Tabs/WristOverlayTab.vue';
const { t } = useI18n(); const { t } = useI18n();
const settingsTabs = computed(() => [
{ value: 'general', label: t('view.settings.category.general') },
{ value: 'appearance', label: t('view.settings.category.appearance') },
{ value: 'notifications', label: t('view.settings.category.notifications') },
{ value: 'wrist-overlay', label: t('view.settings.category.wrist_overlay') },
{ value: 'discord', label: t('view.settings.category.discord_presence') },
{ value: 'pictures', label: t('view.settings.category.pictures') },
{ value: 'advanced', label: t('view.settings.category.advanced') }
]);
onBeforeMount(() => { onBeforeMount(() => {
const menuItem = document.querySelector('li[role="menuitem"].is-active'); const menuItem = document.querySelector('li[role="menuitem"].is-active');
+33 -18
View File
@@ -73,33 +73,44 @@
</TooltipWrapper> </TooltipWrapper>
</div> </div>
</div> </div>
<el-tabs class="zero-margin-tabs" stretch style="height: calc(100% - 70px); margin-top: 5px"> <TabsUnderline
<el-tab-pane> default-value="friends"
<template #label> :items="sidebarTabs"
<span>{{ t('side_panel.friends') }}</span> :unmount-on-hide="false"
<span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span> variant="equal"
</template> class="zero-margin-tabs"
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop> style="height: calc(100% - 70px); margin-top: 5px">
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" /> <template #label-friends>
</el-tab-pane> <span>{{ t('side_panel.friends') }}</span>
<el-tab-pane lazy> <span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span>
<template #label> </template>
<span>{{ t('side_panel.groups') }}</span> <template #label-groups>
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span> <span>{{ t('side_panel.groups') }}</span>
</template> <span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" /> </template>
</el-tab-pane> <template #friends>
</el-tabs> <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>
</template>
<template #groups>
<div class="el-tabs__content">
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
</div>
</template>
</TabsUnderline>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Refresh } from '@element-plus/icons-vue'; import { Refresh } from '@element-plus/icons-vue';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -115,6 +126,10 @@
const { quickSearchItems } = storeToRefs(useSearchStore()); const { quickSearchItems } = storeToRefs(useSearchStore());
const { inGameGroupOrder, groupInstances } = storeToRefs(useGroupStore()); const { inGameGroupOrder, groupInstances } = storeToRefs(useGroupStore());
const { t } = useI18n(); const { t } = useI18n();
const sidebarTabs = computed(() => [
{ value: 'friends', label: t('side_panel.friends') },
{ value: 'groups', label: t('side_panel.groups') }
]);
const quickSearchQuery = ref(''); const quickSearchQuery = ref('');
const isQuickSearchOpen = ref(false); const isQuickSearchOpen = ref(false);
File diff suppressed because it is too large Load Diff
@@ -5,40 +5,40 @@
:title="t('dialog.edit_invite_messages.header')" :title="t('dialog.edit_invite_messages.header')"
width="1000px" width="1000px"
@close="closeDialog"> @close="closeDialog">
<el-tabs v-model="activeTab" style="margin-top: 10px"> <TabsUnderline v-model="activeTab" :items="editInviteTabs" :unmount-on-hide="false" class="mt-2.5">
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_message_tab')" name="message"> <template #message>
<DataTableLayout <DataTableLayout
style="margin-top: 10px; cursor: pointer" style="margin-top: 10px; cursor: pointer"
:table="inviteMessageTanstackTable" :table="inviteMessageTanstackTable"
:loading="false" :loading="false"
:show-pagination="false" :show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" /> :on-row-click="handleEditInviteMessageRowClick" />
</el-tab-pane> </template>
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_request_tab')" name="request"> <template #request>
<DataTableLayout <DataTableLayout
style="margin-top: 10px; cursor: pointer" style="margin-top: 10px; cursor: pointer"
:table="inviteRequestTanstackTable" :table="inviteRequestTanstackTable"
:loading="false" :loading="false"
:show-pagination="false" :show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" /> :on-row-click="handleEditInviteMessageRowClick" />
</el-tab-pane> </template>
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_request_response_tab')" name="requestResponse"> <template #requestResponse>
<DataTableLayout <DataTableLayout
style="margin-top: 10px; cursor: pointer" style="margin-top: 10px; cursor: pointer"
:table="inviteRequestResponseTanstackTable" :table="inviteRequestResponseTanstackTable"
:loading="false" :loading="false"
:show-pagination="false" :show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" /> :on-row-click="handleEditInviteMessageRowClick" />
</el-tab-pane> </template>
<el-tab-pane :label="t('dialog.edit_invite_messages.invite_response_tab')" name="response"> <template #response>
<DataTableLayout <DataTableLayout
style="margin-top: 10px; cursor: pointer" style="margin-top: 10px; cursor: pointer"
:table="inviteResponseTanstackTable" :table="inviteResponseTanstackTable"
:loading="false" :loading="false"
:show-pagination="false" :show-pagination="false"
:on-row-click="handleEditInviteMessageRowClick" /> :on-row-click="handleEditInviteMessageRowClick" />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</el-dialog> </el-dialog>
<template v-if="isEditInviteMessagesDialogVisible"> <template v-if="isEditInviteMessagesDialogVisible">
<EditInviteMessageDialog <EditInviteMessageDialog
@@ -52,6 +52,7 @@
<script setup> <script setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { DataTableLayout } from '@/components/ui/data-table'; import { DataTableLayout } from '@/components/ui/data-table';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@@ -82,6 +83,12 @@
}); });
const activeTab = ref('message'); const activeTab = ref('message');
const editInviteTabs = computed(() => [
{ value: 'message', label: t('dialog.edit_invite_messages.invite_message_tab') },
{ value: 'request', label: t('dialog.edit_invite_messages.invite_request_tab') },
{ value: 'requestResponse', label: t('dialog.edit_invite_messages.invite_request_response_tab') },
{ value: 'response', label: t('dialog.edit_invite_messages.invite_response_tab') }
]);
const isEditInviteMessageDialogVisible = ref(false); const isEditInviteMessageDialogVisible = ref(false);
const inviteMessage = ref({}); const inviteMessage = ref({});
@@ -1,7 +1,7 @@
<template> <template>
<el-dialog :title="t('dialog.export_friends_list.header')" v-model="isVisible" width="650px"> <el-dialog :title="t('dialog.export_friends_list.header')" v-model="isVisible" width="650px">
<el-tabs> <TabsUnderline default-value="csv" :items="exportFriendsTabs" :unmount-on-hide="false" class="mt-2.5">
<el-tab-pane :label="t('dialog.export_friends_list.csv')"> <template #csv>
<InputGroupTextareaField <InputGroupTextareaField
v-model="exportFriendsListCsv" v-model="exportFriendsListCsv"
:rows="15" :rows="15"
@@ -9,8 +9,8 @@
style="margin-top: 15px" style="margin-top: 15px"
input-class="resize-none" input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" /> @click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-tab-pane> </template>
<el-tab-pane :label="t('dialog.export_friends_list.json')"> <template #json>
<InputGroupTextareaField <InputGroupTextareaField
v-model="exportFriendsListJson" v-model="exportFriendsListJson"
:rows="15" :rows="15"
@@ -18,15 +18,16 @@
style="margin-top: 15px" style="margin-top: 15px"
input-class="resize-none" input-class="resize-none"
@click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" /> @click="$event.target.tagName === 'TEXTAREA' && $event.target.select()" />
</el-tab-pane> </template>
</el-tabs> </TabsUnderline>
</el-dialog> </el-dialog>
</template> </template>
<script setup> <script setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { InputGroupTextareaField } from '@/components/ui/input-group'; import { InputGroupTextareaField } from '@/components/ui/input-group';
import { TabsUnderline } from '@/components/ui/tabs';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useUserStore } from '../../../stores'; import { useUserStore } from '../../../stores';
@@ -49,6 +50,10 @@
const exportFriendsListCsv = ref(''); const exportFriendsListCsv = ref('');
const exportFriendsListJson = ref(''); const exportFriendsListJson = ref('');
const exportFriendsTabs = computed(() => [
{ value: 'csv', label: t('dialog.export_friends_list.csv') },
{ value: 'json', label: t('dialog.export_friends_list.json') }
]);
const isVisible = computed({ const isVisible = computed({
get() { get() {