rewrite tables

This commit is contained in:
pa
2026-01-13 17:49:38 +09:00
committed by Natsumi
parent 7649390939
commit 6e3aa44710
7 changed files with 746 additions and 435 deletions

View File

@@ -38,361 +38,41 @@
</div>
<el-tabs type="card">
<el-tab-pane :label="t('view.player_list.photon.current')">
<DataTable v-bind="photonEventTable" style="margin-bottom: 10px">
<el-table-column :label="t('table.playerList.date')" prop="created_at" width="130">
<template #default="scope">
<TooltipWrapper side="right">
<template #content>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
</TooltipWrapper>
</template>
</el-table-column>
<el-table-column :label="t('table.playerList.user')" prop="photonId" width="160">
<template #default="scope">
<span
class="x-link"
style="padding-right: 10px"
@click="showUserFromPhotonId(scope.row.photonId)"
v-text="scope.row.displayName"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.playerList.type')" prop="type" width="140"></el-table-column>
<el-table-column :label="t('table.playerList.detail')" prop="text">
<template #default="scope">
<template v-if="scope.row.type === 'ChangeAvatar'">
<span
class="x-link"
@click="showAvatarDialog(scope.row.avatar.id)"
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><el-icon><Download /></el-icon>&nbsp;</span
>
<span v-if="scope.row.avatar.releaseStatus === 'public'" class="avatar-info-public">{{
t('dialog.avatar.labels.public')
}}</span>
<span
v-else-if="scope.row.avatar.releaseStatus === 'private'"
class="avatar-info-own"
>{{ t('dialog.avatar.labels.private') }}</span
>
</template>
<template v-else-if="scope.row.type === 'ChangeStatus'">
<template v-if="scope.row.status !== scope.row.previousStatus">
<TooltipWrapper side="top">
<template #content>
<span v-if="scope.row.previousStatus === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.previousStatus === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
</TooltipWrapper>
<span>
<el-icon><ArrowRight /></el-icon>
</span>
<TooltipWrapper side="top">
<template #content>
<span v-if="scope.row.status === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.status === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.status === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.status === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i
class="x-user-status"
:class="statusClass(scope.row.status)"
style="margin-right: 5px"></i>
</TooltipWrapper>
</template>
<span
v-if="scope.row.statusDescription !== scope.row.previousStatusDescription"
v-text="scope.row.statusDescription"></span>
</template>
<template v-else-if="scope.row.type === 'ChangeGroup'">
<span
v-if="scope.row.previousGroupName"
class="x-link"
style="margin-right: 5px"
@click="showGroupDialog(scope.row.previousGroupId)"
v-text="scope.row.previousGroupName"></span>
<span
v-else
class="x-link"
style="margin-right: 5px"
@click="showGroupDialog(scope.row.previousGroupId)"
v-text="scope.row.previousGroupId"></span>
<span>
<el-icon><ArrowRight /></el-icon>
</span>
<span
v-if="scope.row.groupName"
class="x-link"
style="margin-left: 5px"
@click="showGroupDialog(scope.row.groupId)"
v-text="scope.row.groupName"></span>
<span
v-else
class="x-link"
style="margin-left: 5px"
@click="showGroupDialog(scope.row.groupId)"
v-text="scope.row.groupId"></span>
</template>
<span
v-else-if="scope.row.type === 'PortalSpawn'"
class="x-link"
@click="showWorldDialog(scope.row.location, scope.row.shortName)">
<Location
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"
:link="false" />
</span>
<span v-else-if="scope.row.type === 'ChatBoxMessage'" v-text="scope.row.text"></span>
<span v-else-if="scope.row.type === 'OnPlayerJoined'">
<span v-if="scope.row.platform === 'Desktop'" style="color: var(--el-color-primary)"
>Desktop&nbsp;</span
>
<span v-else-if="scope.row.platform === 'VR'" style="color: var(--el-color-primary)"
>VR&nbsp;</span
>
<span v-else-if="scope.row.platform === 'Quest'" style="color: var(--el-color-success)"
>Android&nbsp;</span
>
<span
class="x-link"
@click="showAvatarDialog(scope.row.avatar.id)"
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><el-icon><Download /></el-icon>&nbsp;</span
>
<span v-if="scope.row.avatar.releaseStatus === 'public'" class="avatar-info-public">{{
t('dialog.avatar.labels.public')
}}</span>
<span
v-else-if="scope.row.avatar.releaseStatus === 'private'"
class="avatar-info-own"
>{{ t('dialog.avatar.labels.private') }}</span
>
</span>
<span v-else-if="scope.row.type === 'SpawnEmoji'">
<span v-if="scope.row.imageUrl">
<TooltipWrapper side="right">
<template #content>
<img
:src="scope.row.imageUrl"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(scope.row.imageUrl)"
loading="lazy" />
</template>
<span v-text="scope.row.fileId"></span>
</TooltipWrapper>
</span>
<span v-else v-text="scope.row.text"></span>
</span>
<span
v-else-if="scope.row.color === 'yellow'"
style="color: yellow"
v-text="scope.row.text"></span>
<span v-else v-text="scope.row.text"></span>
</template>
</el-table-column>
</DataTable>
<DataTableLayout
class="min-w-0 w-full"
:table="currentTable"
:loading="false"
:table-style="tableStyle"
:page-sizes="pageSizes"
:total-items="currentTotal"
:on-page-size-change="handleCurrentPageSizeChange"
style="margin-bottom: 10px" />
</el-tab-pane>
<el-tab-pane :label="t('view.player_list.photon.previous')">
<DataTable v-bind="photonEventTablePrevious" style="margin-bottom: 10px">
<el-table-column :label="t('table.playerList.date')" prop="created_at" width="130">
<template #default="scope">
<TooltipWrapper side="right">
<template #content>
<span>{{ formatDateFilter(scope.row.created_at, 'long') }}</span>
</template>
<span>{{ formatDateFilter(scope.row.created_at, 'short') }}</span>
</TooltipWrapper>
</template>
</el-table-column>
<el-table-column :label="t('table.playerList.user')" prop="photonId" width="160">
<template #default="scope">
<span
class="x-link"
style="padding-right: 10px"
@click="lookupUser(scope.row)"
v-text="scope.row.displayName"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.playerList.type')" prop="type" width="140"></el-table-column>
<el-table-column :label="t('table.playerList.detail')" prop="text">
<template #default="scope">
<template v-if="scope.row.type === 'ChangeAvatar'">
<span
class="x-link"
@click="showAvatarDialog(scope.row.avatar.id)"
v-text="scope.row.avatar.name"></span>
&nbsp;
<span v-if="!scope.row.inCache" style="color: #aaa"
><el-icon><Download /></el-icon>&nbsp;</span
>
<span v-if="scope.row.avatar.releaseStatus === 'public'" class="avatar-info-public">{{
t('dialog.avatar.labels.public')
}}</span>
<span
v-else-if="scope.row.avatar.releaseStatus === 'private'"
class="avatar-info-own"
>{{ t('dialog.avatar.labels.private') }}</span
>
<template
v-if="
scope.row.avatar.description &&
scope.row.avatar.name !== scope.row.avatar.description
">
| - {{ scope.row.avatar.description }}
</template>
</template>
<template v-else-if="scope.row.type === 'ChangeStatus'">
<template v-if="scope.row.status !== scope.row.previousStatus">
<TooltipWrapper side="top">
<template #content>
<span v-if="scope.row.previousStatus === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.previousStatus === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.previousStatus === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i class="x-user-status" :class="statusClass(scope.row.previousStatus)"></i>
</TooltipWrapper>
<span>
<el-icon><ArrowRight /></el-icon>
</span>
<TooltipWrapper side="top">
<template #content>
<span v-if="scope.row.status === 'active'">{{
t('dialog.user.status.active')
}}</span>
<span v-else-if="scope.row.status === 'join me'">{{
t('dialog.user.status.join_me')
}}</span>
<span v-else-if="scope.row.status === 'ask me'">{{
t('dialog.user.status.ask_me')
}}</span>
<span v-else-if="scope.row.status === 'busy'">{{
t('dialog.user.status.busy')
}}</span>
<span v-else>{{ t('dialog.user.status.offline') }}</span>
</template>
<i
class="x-user-status"
:class="statusClass(scope.row.status)"
style="margin-right: 5px"></i>
</TooltipWrapper>
</template>
<span
v-if="scope.row.statusDescription !== scope.row.previousStatusDescription"
v-text="scope.row.statusDescription"></span>
</template>
<template v-else-if="scope.row.type === 'ChangeGroup'">
<span
v-if="scope.row.previousGroupName"
class="x-link"
style="margin-right: 5px"
@click="showGroupDialog(scope.row.previousGroupId)"
v-text="scope.row.previousGroupName"></span>
<span
v-else
class="x-link"
style="margin-right: 5px"
@click="showGroupDialog(scope.row.previousGroupId)"
v-text="scope.row.previousGroupId"></span>
<span>
<el-icon><ArrowRight /></el-icon>
</span>
<span
v-if="scope.row.groupName"
class="x-link"
style="margin-left: 5px"
@click="showGroupDialog(scope.row.groupId)"
v-text="scope.row.groupName"></span>
<span
v-else
class="x-link"
style="margin-left: 5px"
@click="showGroupDialog(scope.row.groupId)"
v-text="scope.row.groupId"></span>
</template>
<span
v-else-if="scope.row.type === 'PortalSpawn'"
class="x-link"
@click="showWorldDialog(scope.row.location, scope.row.shortName)">
<Location
:location="scope.row.location"
:hint="scope.row.worldName"
:grouphint="scope.row.groupName"
:link="false" />
</span>
<span v-else-if="scope.row.type === 'ChatBoxMessage'" v-text="scope.row.text"></span>
<span v-else-if="scope.row.type === 'SpawnEmoji'">
<span v-if="scope.row.imageUrl">
<TooltipWrapper side="right">
<template #content>
<img
:src="scope.row.imageUrl"
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
@click="showFullscreenImageDialog(scope.row.imageUrl)"
loading="lazy" />
</template>
<span v-text="scope.row.fileId"></span>
</TooltipWrapper>
</span>
<span v-else v-text="scope.row.text"></span>
</span>
<span
v-else-if="scope.row.color === 'yellow'"
style="color: yellow"
v-text="scope.row.text"></span>
<span v-else v-text="scope.row.text"></span>
</template>
</el-table-column>
</DataTable>
<DataTableLayout
class="min-w-0 w-full"
:table="previousTable"
:loading="false"
:table-style="tableStyle"
:page-sizes="pageSizes"
:total-items="previousTotal"
:on-page-size-change="handlePreviousPageSizeChange"
style="margin-bottom: 10px" />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { ArrowRight, Download } from '@element-plus/icons-vue';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
import { InputGroupField } from '@/components/ui/input-group';
import { localeIncludes } from '@/shared/utils';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useSearchStore } from '@/stores';
import { useVrcxVueTable } from '@/lib/table/useVrcxVueTable';
import {
useAvatarStore,
@@ -404,7 +84,7 @@
useWorldStore
} from '../../../stores';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../components/ui/select';
import { formatDateFilter, statusClass } from '../../../shared/utils';
import { createColumns } from './photonEventColumns.jsx';
import { photonEventTableTypeFilterList } from '../../../shared/constants/photon';
const emit = defineEmits(['show-chatbox-blacklist']);
@@ -420,6 +100,8 @@
} = storeToRefs(photonStore);
const { photonEventTableFilterChange, showUserFromPhotonId } = photonStore;
const { stringComparer } = storeToRefs(useSearchStore());
const { lookupUser } = useUserStore();
const { showAvatarDialog } = useAvatarStore();
const { showWorldDialog } = useWorldStore();
@@ -427,6 +109,104 @@
const { showFullscreenImageDialog } = useGalleryStore();
const { ipcEnabled } = storeToRefs(useVrcxStore());
const pageSizes = [10, 25, 50, 100];
const tableStyle = { maxHeight: '320px' };
const q = computed(() =>
String(photonEventTableFilter.value ?? '')
.trim()
.toLowerCase()
);
const typeFilterSet = computed(() => new Set(photonEventTableTypeFilter.value ?? []));
const filterRows = (rows) => {
const query = q.value;
const types = typeFilterSet.value;
const comparer = stringComparer.value;
const src = Array.isArray(rows) ? rows : [];
return src.filter((row) => {
if (types.size && !types.has(row?.type)) return false;
if (!query) return true;
return (
localeIncludes(row?.displayName ?? '', query, comparer) ||
localeIncludes(row?.text ?? '', query, comparer)
);
});
};
const currentRawRows = computed(() =>
Array.isArray(photonEventTable.value?.data) ? photonEventTable.value.data.slice() : []
);
const previousRawRows = computed(() =>
Array.isArray(photonEventTablePrevious.value?.data) ? photonEventTablePrevious.value.data.slice() : []
);
const currentRows = computed(() => filterRows(currentRawRows.value));
const previousRows = computed(() => filterRows(previousRawRows.value));
const currentTotal = computed(() => currentRows.value.length);
const previousTotal = computed(() => previousRows.value.length);
const currentPageSize = ref(photonEventTable.value?.pageSize ?? 10);
const previousPageSize = ref(photonEventTablePrevious.value?.pageSize ?? 10);
const currentColumns = computed(() =>
createColumns({
isPrevious: false,
onShowUser: (row) => showUserFromPhotonId(row?.photonId),
onShowAvatar: showAvatarDialog,
onShowGroup: showGroupDialog,
onShowWorld: showWorldDialog,
onShowImage: showFullscreenImageDialog
})
);
const previousColumns = computed(() =>
createColumns({
isPrevious: true,
onShowUser: (row) => lookupUser(row),
onShowAvatar: showAvatarDialog,
onShowGroup: showGroupDialog,
onShowWorld: showWorldDialog,
onShowImage: showFullscreenImageDialog
})
);
const { table: currentTable } = useVrcxVueTable({
persistKey: 'photonEventTable:current',
data: currentRows,
columns: currentColumns.value,
getRowId: (row) => `${row?.photonId ?? ''}:${row?.created_at ?? ''}:${row?.type ?? ''}`,
initialSorting: [{ id: 'created_at', desc: true }],
initialPagination: { pageIndex: 0, pageSize: currentPageSize.value }
});
const { table: previousTable } = useVrcxVueTable({
persistKey: 'photonEventTable:previous',
data: previousRows,
columns: previousColumns.value,
getRowId: (row) => `${row?.photonId ?? ''}:${row?.created_at ?? ''}:${row?.type ?? ''}`,
initialSorting: [{ id: 'created_at', desc: true }],
initialPagination: { pageIndex: 0, pageSize: previousPageSize.value }
});
const handleCurrentPageSizeChange = (size) => {
currentPageSize.value = size;
};
const handlePreviousPageSizeChange = (size) => {
previousPageSize.value = size;
};
watch(currentColumns, (next) => {
currentTable.setOptions((prev) => ({ ...prev, columns: next }));
});
watch(previousColumns, (next) => {
previousTable.setOptions((prev) => ({ ...prev, columns: next }));
});
function emitShowChatboxBlacklist() {
emit('show-chatbox-blacklist');
}

View File

@@ -0,0 +1,313 @@
import { ArrowRight, Download } from '@element-plus/icons-vue';
import Location from '@/components/Location.vue';
import { TooltipWrapper } from '@/components/ui/tooltip';
import { i18n } from '@/plugin';
import { formatDateFilter, statusClass } from '@/shared/utils';
const { t } = i18n.global;
const statusLabel = (key) => {
if (key === 'active') return t('dialog.user.status.active');
if (key === 'join me') return t('dialog.user.status.join_me');
if (key === 'ask me') return t('dialog.user.status.ask_me');
if (key === 'busy') return t('dialog.user.status.busy');
return t('dialog.user.status.offline');
};
const avatarStatusLabel = (status) => {
if (status === 'public') return t('dialog.avatar.labels.public');
if (status === 'private') return t('dialog.avatar.labels.private');
return '';
};
const avatarStatusClass = (status) => {
if (status === 'public') return 'avatar-info-public';
if (status === 'private') return 'avatar-info-own';
return null;
};
function DetailCell({ row, isPrevious, onShowAvatar, onShowGroup, onShowWorld, onShowUser, onShowImage }) {
const r = row;
if (!r) return null;
if (r.type === 'ChangeAvatar') {
return (
<>
<span
class="x-link"
onClick={(e) => {
e.stopPropagation();
onShowAvatar?.(r.avatar?.id);
}}
>
{r.avatar?.name}
</span>
&nbsp;
{!r.inCache ? (
<span style="color: #aaa">
<el-icon>
<Download />
</el-icon>
&nbsp;
</span>
) : null}
{r.avatar?.releaseStatus ? (
<span class={avatarStatusClass(r.avatar.releaseStatus)}>
{avatarStatusLabel(r.avatar.releaseStatus)}
</span>
) : null}
{isPrevious &&
r.avatar?.description &&
r.avatar?.name !== r.avatar?.description ? (
<>
{' | - '}
{r.avatar.description}
</>
) : null}
</>
);
}
if (r.type === 'ChangeStatus') {
return (
<>
{r.status !== r.previousStatus ? (
<>
<TooltipWrapper
side="top"
v-slots={{
content: () => (
<span>{statusLabel(r.previousStatus)}</span>
)
}}
>
<i
class={[
'x-user-status',
statusClass(r.previousStatus)
]}
></i>
</TooltipWrapper>
<span>
<el-icon>
<ArrowRight />
</el-icon>
</span>
<TooltipWrapper
side="top"
v-slots={{
content: () => (
<span>{statusLabel(r.status)}</span>
)
}}
>
<i
class={['x-user-status', statusClass(r.status)]}
style="margin-right: 5px"
></i>
</TooltipWrapper>
</>
) : null}
{r.statusDescription !== r.previousStatusDescription ? (
<span>{r.statusDescription}</span>
) : null}
</>
);
}
if (r.type === 'ChangeGroup') {
return (
<>
<span
class="x-link"
style="margin-right: 5px"
onClick={(e) => {
e.stopPropagation();
onShowGroup?.(r.previousGroupId);
}}
>
{r.previousGroupName || r.previousGroupId}
</span>
<span>
<el-icon>
<ArrowRight />
</el-icon>
</span>
<span
class="x-link"
style="margin-left: 5px"
onClick={(e) => {
e.stopPropagation();
onShowGroup?.(r.groupId);
}}
>
{r.groupName || r.groupId}
</span>
</>
);
}
if (r.type === 'PortalSpawn') {
return (
<span
class="x-link"
onClick={(e) => {
e.stopPropagation();
onShowWorld?.(r.location, r.shortName);
}}
>
<Location
location={r.location}
hint={r.worldName}
grouphint={r.groupName}
link={false}
/>
</span>
);
}
if (r.type === 'ChatBoxMessage') {
return <span>{r.text}</span>;
}
if (r.type === 'OnPlayerJoined') {
return (
<>
{r.platform === 'Desktop' ? (
<span style="color: var(--el-color-primary)">
Desktop&nbsp;
</span>
) : r.platform === 'VR' ? (
<span style="color: var(--el-color-primary)">VR&nbsp;</span>
) : r.platform === 'Quest' ? (
<span style="color: var(--el-color-success)">
Android&nbsp;
</span>
) : null}
<span
class="x-link"
onClick={(e) => {
e.stopPropagation();
onShowAvatar?.(r.avatar?.id);
}}
>
{r.avatar?.name}
</span>
&nbsp;
{!r.inCache ? (
<span style="color: #aaa">
<el-icon>
<Download />
</el-icon>
&nbsp;
</span>
) : null}
{r.avatar?.releaseStatus ? (
<span class={avatarStatusClass(r.avatar.releaseStatus)}>
{avatarStatusLabel(r.avatar.releaseStatus)}
</span>
) : null}
</>
);
}
if (r.type === 'SpawnEmoji') {
return r.imageUrl ? (
<TooltipWrapper
side="right"
v-slots={{
content: () => (
<img
src={r.imageUrl}
class="friends-list-avatar"
style="height: 500px; cursor: pointer"
loading="lazy"
onClick={(e) => {
e.stopPropagation();
onShowImage?.(r.imageUrl);
}}
/>
)
}}
>
<span>{r.fileId}</span>
</TooltipWrapper>
) : (
<span>{r.text}</span>
);
}
if (r.color === 'yellow') {
return <span style="color: yellow">{r.text}</span>;
}
return <span>{r.text}</span>;
}
export const createColumns = ({ isPrevious, onShowUser, onShowAvatar, onShowGroup, onShowWorld, onShowImage }) => [
{
id: 'created_at',
accessorFn: (row) => (row?.created_at ? Date.parse(row.created_at) : 0),
header: () => t('table.playerList.date'),
size: 130,
cell: ({ row }) => (
<TooltipWrapper
side="right"
v-slots={{
content: () => (
<span>
{formatDateFilter(row.original?.created_at, 'long')}
</span>
)
}}
>
<span>{formatDateFilter(row.original?.created_at, 'short')}</span>
</TooltipWrapper>
)
},
{
id: 'user',
header: () => t('table.playerList.user'),
size: 160,
enableSorting: false,
cell: ({ row }) => (
<span
class="x-link"
style="padding-right: 10px"
onClick={(e) => {
e.stopPropagation();
onShowUser?.(row.original);
}}
>
{row.original?.displayName}
</span>
)
},
{
id: 'type',
accessorKey: 'type',
header: () => t('table.playerList.type'),
size: 140
},
{
id: 'detail',
accessorKey: 'text',
header: () => t('table.playerList.detail'),
enableSorting: false,
meta: {
stretch: true
},
cell: ({ row }) => (
<DetailCell
row={row.original}
isPrevious={!!isPrevious}
onShowAvatar={onShowAvatar}
onShowGroup={onShowGroup}
onShowWorld={onShowWorld}
onShowImage={onShowImage}
/>
)
}
];

View File

@@ -22,50 +22,18 @@
<span class="name" style="margin-right: 24px">{{ t('dialog.registry_backup.ask_to_restore') }}</span>
<Switch :model-value="vrcRegistryAskRestore" @update:modelValue="setVrcRegistryAskRestore" />
</div>
<DataTable v-bind="registryBackupTable" style="margin-top: 10px">
<el-table-column :label="t('dialog.registry_backup.name')" prop="name"></el-table-column>
<el-table-column :label="t('dialog.registry_backup.date')" prop="date">
<template #default="scope">
<span>{{ formatDateFilter(scope.row.date, 'long') }}</span>
</template>
</el-table-column>
<el-table-column :label="t('dialog.registry_backup.action')" width="90" align="right">
<template #default="scope">
<TooltipWrapper side="top" :content="t('dialog.registry_backup.restore')">
<Button
size="sm"
variant="ghost"
class="button-pd-0"
@click="restoreVrcRegistryBackup(scope.row)">
<RotateCcw
/></Button>
</TooltipWrapper>
<TooltipWrapper side="top" :content="t('dialog.registry_backup.save_to_file')">
<Button
size="icon-sm"
variant="ghost"
class="button-pd-0"
@click="saveVrcRegistryBackupToFile(scope.row)">
<Download
/></Button>
</TooltipWrapper>
<TooltipWrapper side="top" :content="t('dialog.registry_backup.delete')">
<Button
size="icon-sm"
variant="ghost"
class="button-pd-0"
@click="deleteVrcRegistryBackup(scope.row)"
><Trash2
/></Button>
</TooltipWrapper>
</template>
</el-table-column>
</DataTable>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="false"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 10px">
<Button size="sm" variant="destructive" @click="deleteVrcRegistry">{{
t('dialog.registry_backup.reset')
}}</Button>
<div>
<div class="flex gap-2">
<Button size="sm" variant="outline" @click="promptVrcRegistryBackupName">{{
t('dialog.registry_backup.backup')
}}</Button>
@@ -79,17 +47,19 @@
</template>
<script setup>
import { Download, RotateCcw, Trash2 } from 'lucide-vue-next';
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { DataTableLayout } from '@/components/ui/data-table';
import { ElMessageBox } from 'element-plus';
import { storeToRefs } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';
import { downloadAndSaveJson, formatDateFilter, removeFromArray } from '../../../shared/utils';
import { downloadAndSaveJson, removeFromArray } from '../../../shared/utils';
import { useAdvancedSettingsStore, useVrcxStore } from '../../../stores';
import { Switch } from '../../../components/ui/switch';
import { createColumns } from './registryBackupColumns.jsx';
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
import configRepository from '../../../service/config';
@@ -113,6 +83,29 @@
layout: 'table'
});
const tableStyle = { maxHeight: '320px' };
const rows = computed(() =>
Array.isArray(registryBackupTable.value?.data) ? registryBackupTable.value.data.slice() : []
);
const columns = computed(() =>
createColumns({
onRestore: restoreVrcRegistryBackup,
onSaveToFile: saveVrcRegistryBackupToFile,
onDelete: deleteVrcRegistryBackup
})
);
const { table } = useVrcxVueTable({
persistKey: 'registryBackupDialog',
data: rows,
columns: columns.value,
getRowId: (row) => String(row?.name ?? ''),
enablePagination: false,
initialSorting: [{ id: 'date', desc: true }]
});
watch(
() => isRegistryBackupDialogVisible.value,
(newVal) => {

View File

@@ -0,0 +1,110 @@
import { Download, RotateCcw, Trash2 } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { TooltipWrapper } from '@/components/ui/tooltip';
import { i18n } from '@/plugin';
import { formatDateFilter } from '@/shared/utils';
const { t } = i18n.global;
export const createColumns = ({ onRestore, onSaveToFile, onDelete }) => [
{
id: 'name',
accessorKey: 'name',
header: () => t('dialog.registry_backup.name'),
meta: {
stretch: true
},
cell: ({ row }) => <span>{row.original?.name}</span>
},
{
id: 'date',
accessorFn: (row) => {
const v = row?.date;
if (typeof v === 'number') return v;
const ts = Date.parse(String(v ?? ''));
return Number.isFinite(ts) ? ts : 0;
},
header: ({ column }) => (
<button
class="inline-flex items-center"
onClick={() => {
const sorted = column.getIsSorted();
column.toggleSorting(sorted === 'asc');
}}
>
{t('dialog.registry_backup.date')}
</button>
),
size: 170,
cell: ({ row }) => (
<span>{formatDateFilter(row.original?.date, 'long')}</span>
)
},
{
id: 'action',
header: () => t('dialog.registry_backup.action'),
enableSorting: false,
size: 90,
meta: {
tdClass: 'text-right'
},
cell: ({ row }) => {
const original = row.original;
return (
<div class="inline-flex items-center justify-end gap-1">
<TooltipWrapper
side="top"
content={t('dialog.registry_backup.restore')}
>
<Button
size="icon-sm"
variant="ghost"
class="button-pd-0"
onClick={(e) => {
e.stopPropagation();
onRestore?.(original);
}}
>
<RotateCcw />
</Button>
</TooltipWrapper>
<TooltipWrapper
side="top"
content={t('dialog.registry_backup.save_to_file')}
>
<Button
size="icon-sm"
variant="ghost"
class="button-pd-0"
onClick={(e) => {
e.stopPropagation();
onSaveToFile?.(original);
}}
>
<Download />
</Button>
</TooltipWrapper>
<TooltipWrapper
side="top"
content={t('dialog.registry_backup.delete')}
>
<Button
size="icon-sm"
variant="ghost"
class="button-pd-0"
onClick={(e) => {
e.stopPropagation();
onDelete?.(original);
}}
>
<Trash2 />
</Button>
</TooltipWrapper>
</div>
);
}
}
];

View File

@@ -222,6 +222,7 @@
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
const { showGalleryPage } = useGalleryStore();
const { friends } = storeToRefs(useFriendStore());
@@ -239,11 +240,7 @@
const isExportFriendsListDialogVisible = ref(false);
const isExportAvatarsListDialogVisible = ref(false);
const isEditInviteMessagesDialogVisible = ref(false);
const isToolsTabVisible = computed(() => {
const route = useRoute();
if (!route) return false;
return route.name === 'tools';
});
const isToolsTabVisible = computed(() => route.name === 'tools');
const showGroupCalendarDialog = () => {
isGroupCalendarDialogVisible.value = true;

View File

@@ -18,13 +18,20 @@
<Button
size="sm"
class="mr-2"
variant="outline"
:disabled="loading"
style="margin-top: 10px"
@click="updateNoteExportDialog">
{{ t('dialog.note_export.refresh') }}
</Button>
<Button size="sm" variant="outline" :disabled="loading" style="margin-top: 10px" @click="exportNoteExport">
<Button
size="sm"
class="mr-2"
variant="outline"
:disabled="loading"
style="margin-top: 10px"
@click="exportNoteExport">
{{ t('dialog.note_export.export') }}
</Button>
<Button v-if="loading" size="sm" variant="outline" style="margin-top: 10px" @click="cancelNoteExport">
@@ -45,60 +52,29 @@
<pre style="white-space: pre-wrap; font-size: 12px" v-text="errors"></pre>
</template>
<DataTable :loading="loading" v-bind="noteExportTable" style="margin-top: 10px">
<el-table-column :label="t('table.import.image')" width="70" prop="currentAvatarThumbnailImageUrl">
<template #default="{ row }">
<el-popover placement="right" :width="500" trigger="hover">
<template #reference>
<img :src="userImage(row.ref)" class="friends-list-avatar" loading="lazy" />
</template>
<img
:src="userImageFull(row.ref)"
:class="['friends-list-avatar', 'x-popover-image']"
style="cursor: pointer"
loading="lazy"
@click="showFullscreenImageDialog(userImageFull(row.ref))" />
</el-popover>
</template>
</el-table-column>
<el-table-column :label="t('table.import.name')" width="170" prop="name">
<template #default="{ row }">
<span class="x-link" @click="showUserDialog(row.id)" v-text="row.name"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.import.note')" prop="memo">
<template #default="{ row }">
<InputGroupTextareaField
v-model="row.memo"
:maxlength="256"
:rows="2"
input-class="min-h-0 py-1 resize-none"
show-count />
</template>
</el-table-column>
<el-table-column :label="t('table.import.skip_export')" width="90" align="right">
<template #default="{ row }">
<Button size="sm" variant="ghost" @click="removeFromNoteExportTable(row)"></Button>
</template>
</el-table-column>
</DataTable>
<DataTableLayout
class="min-w-0 w-full"
:table="table"
:loading="loading"
:table-style="tableStyle"
:show-pagination="false"
style="margin-top: 10px" />
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { DataTableLayout } from '@/components/ui/data-table';
import { Loading } from '@element-plus/icons-vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { removeFromArray, userImage, userImageFull } from '../../../shared/utils';
import { useFriendStore, useGalleryStore, useUserStore } from '../../../stores';
import { createColumns } from './noteExportColumns.jsx';
import { miscRequest } from '../../../api';
import { useVrcxVueTable } from '../../../lib/table/useVrcxVueTable';
import * as workerTimers from 'worker-timers';
@@ -123,6 +99,29 @@
layout: 'table'
});
const tableStyle = { maxHeight: '500px' };
const rows = computed(() => (Array.isArray(noteExportTable.value?.data) ? noteExportTable.value.data.slice() : []));
const columns = computed(() =>
createColumns({
userImage,
userImageFull,
onShowFullscreenImage: showFullscreenImageDialog,
onShowUser: showUserDialog,
onRemove: removeFromNoteExportTable
})
);
const { table } = useVrcxVueTable({
persistKey: 'noteExportDialog',
data: rows,
columns: columns.value,
getRowId: (row) => String(row?.id ?? ''),
enablePagination: false,
enableSorting: false
});
const progress = ref(0);
const progressTotal = ref(0);
const loading = ref(false);

View File

@@ -0,0 +1,119 @@
import { Trash2 } from 'lucide-vue-next';
import { Button } from '@/components/ui/button';
import { InputGroupTextareaField } from '@/components/ui/input-group';
import { i18n } from '@/plugin';
const { t } = i18n.global;
export const createColumns = ({ userImage, userImageFull, onShowFullscreenImage, onShowUser, onRemove }) => [
{
id: 'image',
header: () => t('table.import.image'),
enableSorting: false,
size: 70,
cell: ({ row }) => {
const original = row.original;
const ref = original?.ref;
const thumb = userImage?.(ref);
const full = userImageFull?.(ref);
return (
<el-popover
placement="right"
width={500}
trigger="hover"
v-slots={{
reference: () => (
<img
src={thumb}
class="friends-list-avatar"
loading="lazy"
/>
)
}}
>
<img
src={full}
class={['friends-list-avatar', 'x-popover-image']}
style="cursor: pointer"
loading="lazy"
onClick={(e) => {
e.stopPropagation();
if (full) {
onShowFullscreenImage?.(full);
}
}}
/>
</el-popover>
);
}
},
{
id: 'name',
header: () => t('table.import.name'),
size: 170,
cell: ({ row }) => {
const original = row.original;
return (
<span
class="x-link"
onClick={(e) => {
e.stopPropagation();
onShowUser?.(original?.id);
}}
>
{original?.name}
</span>
);
}
},
{
id: 'memo',
accessorKey: 'memo',
header: () => t('table.import.note'),
enableSorting: false,
meta: {
stretch: true
},
cell: ({ row }) => {
const original = row.original;
return (
<InputGroupTextareaField
modelValue={original?.memo ?? ''}
onUpdate:modelValue={(value) => {
original.memo = value;
}}
maxlength={256}
rows={2}
input-class="min-h-0 py-1 resize-none"
show-count
/>
);
}
},
{
id: 'skip',
header: () => t('table.import.skip_export'),
size: 90,
enableSorting: false,
meta: {
tdClass: 'text-right'
},
cell: ({ row }) => {
const original = row.original;
return (
<Button
size="icon-sm"
variant="ghost"
onClick={(e) => {
e.stopPropagation();
onRemove?.(original);
}}
>
<Trash2 />
</Button>
);
}
}
];