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,22 +1054,26 @@
<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}`"
v-slot:[`label-${index}`]>
<span style="font-weight: bold; font-size: 16px" v-text="gallery.name" /> <span style="font-weight: bold; font-size: 16px" v-text="gallery.name" />
<i <i class="x-status-icon" style="margin-left: 5px" :class="groupGalleryStatus(gallery)" />
class="x-status-icon"
style="margin-left: 5px"
:class="groupGalleryStatus(gallery)" />
<span style="color: #909399; font-size: 12px; margin-left: 5px">{{ <span style="color: #909399; font-size: 12px; margin-left: 5px">{{
groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0 groupDialog.galleries[gallery.id] ? groupDialog.galleries[gallery.id].length : 0
}}</span> }}</span>
</template> </template>
<template
v-for="(gallery, index) in groupDialog.ref.galleries"
:key="`content-${index}`"
v-slot:[String(index)]>
<span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" /> <span style="color: #c7c7c7; padding: 10px" v-text="gallery.description" />
<div <div
style=" style="
@@ -1087,11 +1095,10 @@
loading="lazy" /> loading="lazy" />
</Card> </Card>
</div> </div>
</el-tab-pane>
</template> </template>
</el-tabs> </TabsUnderline>
</el-tab-pane> </template>
<el-tab-pane name="JSON" :label="t('dialog.group.json.header')" 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"
@@ -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,9 +175,8 @@
<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>
@@ -185,6 +184,23 @@
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>
<el-tab-pane>
<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> </template>
<template #join>
<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>
<el-tab-pane>
<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> </template>
<template #blocked>
<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,16 +1099,18 @@
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-model="favoriteWorldsTab"
:items="favoriteWorldTabs"
:unmount-on-hide="false"
v-loading="userDialog.isFavoriteWorldsLoading" v-loading="userDialog.isFavoriteWorldsLoading"
class="zero-margin-tabs" class="zero-margin-tabs"
type="card"
stretch
style="margin-top: 10px; height: 50vh"> style="margin-top: 10px; height: 50vh">
<template v-if="userFavoriteWorlds && userFavoriteWorlds.length > 0"> <template
<el-tab-pane v-for="(list, index) in userFavoriteWorlds" :key="index" lazy> v-for="(list, index) in userFavoriteWorlds"
<template #label> :key="`favorite-worlds-label-${index}`"
v-slot:[`label-${index}`]>
<span> <span>
<i <i
class="x-status-icon" class="x-status-icon"
@@ -1117,15 +1119,15 @@
</i> </i>
<span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span> <span style="font-weight: bold; font-size: 14px" v-text="list[0]"></span>
<span <span
style=" style="color: var(--el-text-color-secondary); font-size: 10px; margin-left: 5px"
color: var(--el-text-color-secondary);
font-size: 10px;
margin-left: 5px;
"
>{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span >{{ list[2].length }}/{{ favoriteLimits.maxFavoritesPerGroup.world }}</span
> >
</span> </span>
</template> </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>
</TabsUnderline>
</template> </template>
<template v-else-if="!userDialog.isFavoriteWorldsLoading"> <template v-else-if="!userDialog.isFavoriteWorldsLoading">
<div style="display: flex; justify-content: center; align-items: center; height: 100%"> <div style="display: flex; justify-content: center; align-items: center; height: 100%">
<span style="font-size: 16px">No favorite worlds found.</span> <span style="font-size: 16px">No favorite worlds found.</span>
</div> </div>
</template> </template>
</el-tabs> </template>
</el-tab-pane>
<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';
+13 -10
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>
</el-tabs>
<div v-show="activeTab === 'instance'">
<InstanceActivity /> <InstanceActivity />
</div> </template>
<div v-show="activeTab === 'mutual'"> <template #mutual>
<MutualFriends /> <MutualFriends />
</div> </template>
</TabsUnderline>
<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,
+53 -29
View File
@@ -14,8 +14,14 @@
/></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"
:items="searchTabs"
aria-label="Search tabs"
:unmount-on-hide="false"
style="margin-top: 15px">
<template #user>
<div v-loading="isSearchUserLoading" style="min-height: 60px">
<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="searchUserByBio" /> <Checkbox v-model="searchUserByBio" />
<span>{{ t('view.search.user.search_by_bio') }}</span> <span>{{ t('view.search.user.search_by_bio') }}</span>
@@ -66,11 +72,10 @@
{{ t('view.search.next_page') }} {{ t('view.search.next_page') }}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</el-tab-pane> </div>
<el-tab-pane </template>
v-loading="isSearchWorldLoading" <template #world>
:label="t('view.search.world.header')" <div v-loading="isSearchWorldLoading" style="min-height: 60px">
style="min-height: 60px">
<div class="inline-flex justify-between mb-4 w-full"> <div class="inline-flex justify-between mb-4 w-full">
<Select <Select
:model-value="searchWorldCategoryIndex" :model-value="searchWorldCategoryIndex"
@@ -131,11 +136,10 @@
{{ t('view.search.next_page') }} {{ t('view.search.next_page') }}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</el-tab-pane> </div>
<el-tab-pane </template>
v-loading="isSearchAvatarLoading" <template #avatar>
:label="t('view.search.avatar.header')" <div v-loading="isSearchAvatarLoading" style="min-height: 60px">
style="min-height: 60px">
<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">
<Select <Select
@@ -190,7 +194,9 @@
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilter-private" value="private" /> <RadioGroupItem id="searchAvatarFilter-private" value="private" />
<label for="searchAvatarFilter-private">{{ t('view.search.avatar.private') }}</label> <label for="searchAvatarFilter-private">{{
t('view.search.avatar.private')
}}</label>
</div> </div>
</RadioGroup> </RadioGroup>
<el-divider direction="vertical"></el-divider> <el-divider direction="vertical"></el-divider>
@@ -205,7 +211,9 @@
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroupItem id="searchAvatarFilterRemote-local" value="local" /> <RadioGroupItem id="searchAvatarFilterRemote-local" value="local" />
<label for="searchAvatarFilterRemote-local">{{ t('view.search.avatar.local') }}</label> <label for="searchAvatarFilterRemote-local">{{
t('view.search.avatar.local')
}}</label>
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<RadioGroupItem <RadioGroupItem
@@ -268,7 +276,11 @@
</div> </div>
</div> </div>
<ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px"> <ButtonGroup v-if="searchAvatarPage.length" style="margin-top: 15px">
<Button variant="outline" size="sm" :disabled="!searchAvatarPageNum" @click="moreSearchAvatar(-1)"> <Button
variant="outline"
size="sm"
:disabled="!searchAvatarPageNum"
@click="moreSearchAvatar(-1)">
<Back /> <Back />
{{ t('view.search.prev_page') }} {{ t('view.search.prev_page') }}
</Button> </Button>
@@ -284,11 +296,10 @@
{{ t('view.search.next_page') }} {{ t('view.search.next_page') }}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</el-tab-pane> </div>
<el-tab-pane </template>
v-loading="isSearchGroupLoading" <template #group>
:label="t('view.search.group.header')" <div v-loading="isSearchGroupLoading" style="min-height: 60px">
style="min-height: 60px">
<div class="x-friend-list" style="min-height: 500px"> <div class="x-friend-list" style="min-height: 500px">
<div <div
v-for="group in searchGroupResults" v-for="group in searchGroupResults"
@@ -335,8 +346,9 @@
{{ t('view.search.next_page') }} {{ t('view.search.next_page') }}
</Button> </Button>
</ButtonGroup> </ButtonGroup>
</el-tab-pane> </div>
</el-tabs> </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');
+26 -11
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"
:unmount-on-hide="false"
variant="equal"
class="zero-margin-tabs"
style="height: calc(100% - 70px); margin-top: 5px">
<template #label-friends>
<span>{{ t('side_panel.friends') }}</span> <span>{{ t('side_panel.friends') }}</span>
<span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span> <span class="sidebar-tab-count"> ({{ onlineFriendCount }}/{{ friends.size }}) </span>
</template> </template>
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop> <template #label-groups>
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
</el-tab-pane>
<el-tab-pane lazy>
<template #label>
<span>{{ t('side_panel.groups') }}</span> <span>{{ t('side_panel.groups') }}</span>
<span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span> <span class="sidebar-tab-count"> ({{ groupInstances.length }}) </span>
</template> </template>
<template #friends>
<div class="el-tabs__content">
<el-backtop target=".zero-margin-tabs .el-tabs__content" :bottom="20" :right="20"></el-backtop>
<FriendsSidebar @confirm-delete-friend="confirmDeleteFriend" />
</div>
</template>
<template #groups>
<div class="el-tabs__content">
<GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" /> <GroupsSidebar :group-instances="groupInstances" :group-order="inGameGroupOrder" />
</el-tab-pane> </div>
</el-tabs> </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);
+99 -58
View File
@@ -6,14 +6,51 @@
</Button> </Button>
<span class="header">{{ t('dialog.gallery_icons.header') }}</span> <span class="header">{{ t('dialog.gallery_icons.header') }}</span>
</div> </div>
<el-tabs> <TabsUnderline default-value="gallery" :items="galleryTabs" :unmount-on-hide="false">
<el-tab-pane v-loading="galleryDialogGalleryLoading"> <template #label-gallery>
<template #label>
<span> <span>
{{ t('dialog.gallery_icons.gallery') }} {{ t('dialog.gallery_icons.gallery') }}
<span class="gallery-tab-count"> {{ galleryTable.length }}/64 </span> <span class="gallery-tab-count"> {{ galleryTable.length }}/64 </span>
</span> </span>
</template> </template>
<template #label-icons>
<span>
{{ t('dialog.gallery_icons.icons') }}
<span class="gallery-tab-count"> {{ VRCPlusIconsTable.length }}/64 </span>
</span>
</template>
<template #label-emojis>
<span>
{{ t('dialog.gallery_icons.emojis') }}
<span class="gallery-tab-count">
{{ emojiTable.length }}/{{ cachedConfigTyped.maxUserEmoji }}
</span>
</span>
</template>
<template #label-stickers>
<span>
{{ t('dialog.gallery_icons.stickers') }}
<span class="gallery-tab-count">
{{ stickerTable.length }}/{{ cachedConfigTyped.maxUserStickers }}
</span>
</span>
</template>
<template #label-prints>
<span>
{{ t('dialog.gallery_icons.prints') }}
<span class="gallery-tab-count"> {{ printTable.length }}/64 </span>
</span>
</template>
<template #label-inventory>
<span>
{{ t('dialog.gallery_icons.inventory') }}
<span class="gallery-tab-count">
{{ inventoryTable.length }}
</span>
</span>
</template>
<template #gallery>
<div v-loading="galleryDialogGalleryLoading">
<input <input
id="GalleryUploadButton" id="GalleryUploadButton"
type="file" type="file"
@@ -67,7 +104,9 @@
class="rounded-full mr-2" class="rounded-full mr-2"
size="icon-sm" size="icon-sm"
variant="outline" variant="outline"
@click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)"> @click="
showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)
">
<Maximize2 /> <Maximize2 />
</Button> </Button>
<Button <Button
@@ -80,15 +119,11 @@
</div> </div>
</template> </template>
</div> </div>
</el-tab-pane> </div>
<el-tab-pane v-loading="galleryDialogIconsLoading" lazy>
<template #label>
<span>
{{ t('dialog.gallery_icons.icons') }}
<span class="gallery-tab-count"> {{ VRCPlusIconsTable.length }}/64 </span>
</span>
</template> </template>
<template #icons>
<div v-loading="galleryDialogIconsLoading">
<input <input
id="VRCPlusIconUploadButton" id="VRCPlusIconUploadButton"
type="file" type="file"
@@ -111,7 +146,11 @@
<Upload /> <Upload />
{{ t('dialog.gallery_icons.upload') }} {{ t('dialog.gallery_icons.upload') }}
</Button> </Button>
<Button variant="outline" size="sm" :disabled="!currentUser.userIcon" @click="setVRCPlusIcon('')"> <Button
variant="outline"
size="sm"
:disabled="!currentUser.userIcon"
@click="setVRCPlusIcon('')">
<Close /> <Close />
{{ t('dialog.gallery_icons.clear') }} {{ t('dialog.gallery_icons.clear') }}
</Button> </Button>
@@ -138,7 +177,9 @@
class="rounded-full mr-2" class="rounded-full mr-2"
size="icon-sm" size="icon-sm"
variant="outline" variant="outline"
@click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)"> @click="
showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)
">
<Maximize2 /> <Maximize2 />
</Button> </Button>
<Button <Button
@@ -150,17 +191,11 @@
/></Button></div /></Button></div
></template> ></template>
</div> </div>
</el-tab-pane> </div>
<el-tab-pane v-loading="galleryDialogEmojisLoading" lazy>
<template #label>
<span>
{{ t('dialog.gallery_icons.emojis') }}
<span class="gallery-tab-count">
{{ emojiTable.length }}/{{ cachedConfigTyped.maxUserEmoji }}
</span>
</span>
</template> </template>
<template #emojis>
<div v-loading="galleryDialogEmojisLoading">
<input <input
id="EmojiUploadButton" id="EmojiUploadButton"
type="file" type="file"
@@ -275,7 +310,9 @@
getEmojiFileName(image) getEmojiFileName(image)
) )
"> ">
<Emoji :imageUrl="image.versions[image.versions.length - 1].file.url" :size="200"></Emoji> <Emoji
:imageUrl="image.versions[image.versions.length - 1].file.url"
:size="200"></Emoji>
</div> </div>
<div style="display: inline-block; margin: 5px"> <div style="display: inline-block; margin: 5px">
<span v-if="image.loopStyle === 'pingpong'"> <span v-if="image.loopStyle === 'pingpong'">
@@ -310,17 +347,11 @@
/></Button></div /></Button></div
></template> ></template>
</div> </div>
</el-tab-pane> </div>
<el-tab-pane v-loading="galleryDialogStickersLoading" lazy>
<template #label>
<span>
{{ t('dialog.gallery_icons.stickers') }}
<span class="gallery-tab-count">
{{ stickerTable.length }}/{{ cachedConfigTyped.maxUserStickers }}
</span>
</span>
</template> </template>
<template #stickers>
<div v-loading="galleryDialogStickersLoading">
<input <input
id="StickerUploadButton" id="StickerUploadButton"
type="file" type="file"
@@ -366,7 +397,9 @@
class="rounded-full mr-2" class="rounded-full mr-2"
size="icon-sm" size="icon-sm"
variant="outline" variant="outline"
@click="showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)" @click="
showFullscreenImageDialog(image.versions[image.versions.length - 1].file.url)
"
><Maximize2 ><Maximize2
/></Button> /></Button>
<Button <Button
@@ -378,15 +411,11 @@
/></Button></div /></Button></div
></template> ></template>
</div> </div>
</el-tab-pane> </div>
<el-tab-pane v-loading="galleryDialogPrintsLoading" lazy>
<template #label>
<span>
{{ t('dialog.gallery_icons.prints') }}
<span class="gallery-tab-count"> {{ printTable.length }}/64 </span>
</span>
</template> </template>
<template #prints>
<div v-loading="galleryDialogPrintsLoading">
<input <input
id="PrintUploadButton" id="PrintUploadButton"
type="file" type="file"
@@ -436,7 +465,11 @@
<img class="avatar" :src="image.files.image" loading="lazy" /> <img class="avatar" :src="image.files.image" loading="lazy" />
</div> </div>
<div style="margin-top: 5px; width: 208px"> <div style="margin-top: 5px; width: 208px">
<span class="x-ellipsis" v-if="image.note" v-text="image.note" style="display: block"></span> <span
class="x-ellipsis"
v-if="image.note"
v-text="image.note"
style="display: block"></span>
<span v-else style="display: block">&nbsp;</span> <span v-else style="display: block">&nbsp;</span>
<Location <Location
class="x-ellipsis" class="x-ellipsis"
@@ -464,22 +497,20 @@
@click="showFullscreenImageDialog(image.files.image, getPrintFileName(image))"> @click="showFullscreenImageDialog(image.files.image, getPrintFileName(image))">
<Maximize2 <Maximize2
/></Button> /></Button>
<Button class="rounded-full" size="icon-sm" variant="outline" @click="deletePrint(image.id)"> <Button
class="rounded-full"
size="icon-sm"
variant="outline"
@click="deletePrint(image.id)">
<Trash2 <Trash2
/></Button> /></Button>
</div> </div>
</div> </div>
</el-tab-pane> </div>
<el-tab-pane v-loading="galleryDialogInventoryLoading" lazy>
<template #label>
<span>
{{ t('dialog.gallery_icons.inventory') }}
<span class="gallery-tab-count">
{{ inventoryTable.length }}
</span>
</span>
</template> </template>
<template #inventory>
<div v-loading="galleryDialogInventoryLoading">
<br /> <br />
<br /> <br />
<div style="display: flex; align-items: center"> <div style="display: flex; align-items: center">
@@ -530,8 +561,9 @@
{{ t('dialog.gallery_icons.consume_bundle') }} {{ t('dialog.gallery_icons.consume_bundle') }}
</Button> </Button>
</div> </div>
</el-tab-pane> </div>
</el-tabs> </template>
</TabsUnderline>
</div> </div>
</template> </template>
@@ -551,6 +583,7 @@
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
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 { 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';
@@ -611,6 +644,14 @@
const cachedConfigTyped = computed( const cachedConfigTyped = computed(
() => /** @type {{ maxUserEmoji?: number, maxUserStickers?: number }} */ (cachedConfig.value ?? {}) () => /** @type {{ maxUserEmoji?: number, maxUserStickers?: number }} */ (cachedConfig.value ?? {})
); );
const galleryTabs = computed(() => [
{ value: 'gallery', label: t('dialog.gallery_icons.gallery') },
{ value: 'icons', label: t('dialog.gallery_icons.icons') },
{ value: 'emojis', label: t('dialog.gallery_icons.emojis') },
{ value: 'stickers', label: t('dialog.gallery_icons.stickers') },
{ value: 'prints', label: t('dialog.gallery_icons.prints') },
{ value: 'inventory', label: t('dialog.gallery_icons.inventory') }
]);
const emojiAnimFps = ref(15); const emojiAnimFps = ref(15);
const emojiAnimFrameCount = ref(4); const emojiAnimFrameCount = ref(4);
@@ -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() {