rewrite moderation table

This commit is contained in:
pa
2026-01-06 20:46:55 +09:00
committed by Natsumi
parent 7e8485a5d3
commit 25c4ad8d2f
3 changed files with 298 additions and 68 deletions

View File

@@ -32,7 +32,7 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
accessorKey: 'created_at',
meta: {
class: 'w-[200px]'
class: 'w-[140px]'
},
header: ({ column }) => (
<Button
@@ -67,7 +67,7 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
{
accessorKey: 'type',
meta: {
class: 'w-[200px]'
class: 'w-[180px]'
},
header: () => t('table.friendLog.type'),
cell: ({ row }) => {
@@ -81,21 +81,24 @@ export const createColumns = ({ onDelete, onDeletePrompt }) => {
},
{
accessorKey: 'displayName',
meta: {
class: 'min-w-0 overflow-hidden'
},
header: () => t('table.friendLog.user'),
cell: ({ row }) => {
const original = row.original;
const displayName =
original.displayName || original.userId || '';
return (
<span>
<span class="block w-full min-w-0 truncate">
{original.type === 'DisplayName' ? (
<span class="mr-1 text-muted-foreground">
<span class="mr-1">
{original.previousDisplayName}
<ArrowRight class="mx-1 inline h-3 w-3" />
</span>
) : null}
<span
class="x-link table-user pr-2.5"
class="x-link pr-2.5"
onClick={() => showUserDialog(original.userId)}
>
{displayName}

View File

@@ -28,86 +28,52 @@
</el-tooltip>
</div>
<DataTable v-bind="playerModerationTable">
<el-table-column width="20"></el-table-column>
<el-table-column :label="t('table.moderation.date')" prop="created" :sortable="true" width="150">
<template #default="scope">
<el-tooltip placement="right">
<template #content>
<span>{{ formatDateFilter(scope.row.created, 'long') }}</span>
</template>
<span>{{ formatDateFilter(scope.row.created, 'short') }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.type')" prop="type" width="150">
<template #default="scope">
<el-tag type="info" effect="plain" size="small">
<span v-text="t('view.moderation.filters.' + scope.row.type)"></span
></el-tag>
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.source')" prop="sourceDisplayName" width="200">
<template #default="scope">
<span
class="x-link"
v-text="scope.row.sourceDisplayName"
@click="showUserDialog(scope.row.sourceUserId)"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.target')" prop="targetDisplayName">
<template #default="scope">
<span
class="x-link"
v-text="scope.row.targetDisplayName"
@click="showUserDialog(scope.row.targetUserId)"></span>
</template>
</el-table-column>
<el-table-column :label="t('table.moderation.action')" width="80" align="right">
<template #default="scope">
<template v-if="scope.row.sourceUserId === currentUser.id">
<el-button
v-if="shiftHeld"
style="color: var(--el-color-danger)"
text
:icon="Close"
size="small"
@click="deletePlayerModeration(scope.row)"></el-button>
<el-button
v-else
text
:icon="Close"
size="small"
@click="deletePlayerModerationPrompt(scope.row)"></el-button>
</template>
</template>
</el-table-column>
</DataTable>
<DataTableLayout
:table="table"
:loading="playerModerationTable.loading"
:table-style="tableHeightStyle"
:page-sizes="pageSizes"
:total-items="totalItems"
:on-page-size-change="handlePageSizeChange" />
</div>
</template>
<script setup>
import { Close, Refresh } from '@element-plus/icons-vue';
import { Refresh } from '@element-plus/icons-vue';
import { ElMessageBox } from 'element-plus';
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable
} from '@tanstack/vue-table';
import { computed, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useModerationStore, useUiStore, useUserStore } from '../../stores';
import { formatDateFilter } from '../../shared/utils';
import { useAppearanceSettingsStore, useModerationStore, useVrcxStore } from '../../stores';
import { DataTableLayout } from '../../components/ui/data-table';
import { createColumns } from './columns.jsx';
import { moderationTypes } from '../../shared/constants';
import { playerModerationRequest } from '../../api';
import { useTableHeight } from '../../composables/useTableHeight';
import { useDataTableScrollHeight } from '../../composables/useDataTableScrollHeight';
import { valueUpdater } from '../../components/ui/table/utils';
import configRepository from '../../service/config.js';
const { t } = useI18n();
const { showUserDialog } = useUserStore();
const { playerModerationTable } = storeToRefs(useModerationStore());
const { refreshPlayerModerations, handlePlayerModerationDelete } = useModerationStore();
const { shiftHeld } = storeToRefs(useUiStore());
const { currentUser } = storeToRefs(useUserStore());
const appearanceSettingsStore = useAppearanceSettingsStore();
const vrcxStore = useVrcxStore();
const { containerRef: moderationRef } = useTableHeight(playerModerationTable);
const moderationRef = ref(null);
const { tableStyle: tableHeightStyle } = useDataTableScrollHeight(moderationRef, {
offset: 30,
toolbarHeight: 54,
paginationHeight: 52
});
async function init() {
playerModerationTable.value.filters[0].value = JSON.parse(
@@ -145,6 +111,97 @@
})
.catch(() => {});
}
const moderationDisplayData = computed(() => {
const data = playerModerationTable.value.data;
const typeFilter = playerModerationTable.value.filters?.[0]?.value ?? [];
const searchFilter = playerModerationTable.value.filters?.[1]?.value ?? '';
const typeSet = Array.isArray(typeFilter)
? new Set(typeFilter.map((value) => String(value).toLowerCase()))
: null;
const searchValue = String(searchFilter).trim().toLowerCase();
return data.filter((row) => {
if (typeSet && typeSet.size > 0) {
const rowType = String(row.type ?? '').toLowerCase();
if (!typeSet.has(rowType)) {
return false;
}
}
if (searchValue) {
const source = String(row.sourceDisplayName ?? '').toLowerCase();
const target = String(row.targetDisplayName ?? '').toLowerCase();
if (!source.includes(searchValue) && !target.includes(searchValue)) {
return false;
}
}
return true;
});
});
const columns = createColumns({
onDelete: deletePlayerModeration,
onDeletePrompt: deletePlayerModerationPrompt
});
const pageSizes = computed(() => appearanceSettingsStore.tablePageSizes);
const pageSize = computed(() =>
playerModerationTable.value.pageSizeLinked
? appearanceSettingsStore.tablePageSize
: playerModerationTable.value.pageSize
);
const sorting = ref([{ id: 'created', desc: true }]);
const pagination = ref({
pageIndex: 0,
pageSize: pageSize.value
});
const table = useVueTable({
data: moderationDisplayData,
columns,
getRowId: (row) => row.id ?? `${row.type}:${row.sourceUserId}:${row.targetUserId}:${row.created ?? ''}`,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: (updaterOrValue) => valueUpdater(updaterOrValue, sorting),
onPaginationChange: (updaterOrValue) => valueUpdater(updaterOrValue, pagination),
state: {
get sorting() {
return sorting.value;
},
get pagination() {
return pagination.value;
}
}
});
const totalItems = computed(() => {
const length = table.getFilteredRowModel().rows.length;
const max = vrcxStore.maxTableSize;
return length > max && length < max + 51 ? max : length;
});
const handlePageSizeChange = (size) => {
if (playerModerationTable.value.pageSizeLinked) {
appearanceSettingsStore.setTablePageSize(size);
} else {
playerModerationTable.value.pageSize = size;
}
};
watch(pageSize, (size) => {
if (pagination.value.pageSize === size) {
return;
}
pagination.value = {
...pagination.value,
pageIndex: 0,
pageSize: size
};
table.setPageSize(size);
});
</script>
<style scoped>

View File

@@ -0,0 +1,170 @@
import { Badge } from '../../components/ui/badge';
import { Button } from '../../components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from '../../components/ui/tooltip';
import { ArrowUpDown, X } from 'lucide-vue-next';
import { storeToRefs } from 'pinia';
import { formatDateFilter } from '../../shared/utils';
import { i18n } from '../../plugin';
import { useUiStore, useUserStore } from '../../stores';
const { t } = i18n.global;
export const createColumns = ({ onDelete, onDeletePrompt }) => {
const { showUserDialog } = useUserStore();
const { shiftHeld } = storeToRefs(useUiStore());
const { currentUser } = storeToRefs(useUserStore());
return [
{
id: 'spacer',
header: () => null,
enableSorting: false,
meta: {
class: 'w-[20px]'
},
cell: () => null
},
{
accessorKey: 'created',
meta: {
class: 'w-[140px]'
},
header: ({ column }) => (
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === 'asc')
}
>
{t('table.moderation.date')}
<ArrowUpDown class="ml-1 h-4 w-4" />
</Button>
),
cell: ({ row }) => {
const createdAt = row.getValue('created');
const shortText = formatDateFilter(createdAt, 'short');
const longText = formatDateFilter(createdAt, 'long');
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>{shortText}</span>
</TooltipTrigger>
<TooltipContent side="right">
<span>{longText}</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
},
{
accessorKey: 'type',
meta: {
class: 'w-[160px]'
},
header: () => t('table.moderation.type'),
cell: ({ row }) => {
const type = row.getValue('type');
return (
<Badge variant="outline" class="text-muted-foreground">
{t(`view.moderation.filters.${type}`)}
</Badge>
);
}
},
{
accessorKey: 'sourceDisplayName',
meta: {
class: 'w-[200px] min-w-0 overflow-hidden'
},
header: () => t('table.moderation.source'),
cell: ({ row }) => {
const original = row.original;
return (
<span
class="x-link block w-full min-w-0 truncate pr-2.5"
onClick={() => showUserDialog(original.sourceUserId)}
>
{original.sourceDisplayName}
</span>
);
}
},
{
accessorKey: 'targetDisplayName',
meta: {
class: 'min-w-0 overflow-hidden'
},
header: () => t('table.moderation.target'),
cell: ({ row }) => {
const original = row.original;
return (
<span
class="x-link block w-full min-w-0 truncate pr-2.5"
onClick={() => showUserDialog(original.targetUserId)}
>
{original.targetDisplayName}
</span>
);
}
},
{
id: 'action',
meta: {
class: 'w-[80px] max-w-[80px] text-right'
},
header: () => t('table.moderation.action'),
enableSorting: false,
cell: ({ row }) => {
const original = row.original;
if (original.sourceUserId !== currentUser.value?.id) {
return null;
}
if (shiftHeld.value) {
return (
<div class="flex justify-end">
<Button
variant="ghost"
size="sm"
class="h-6 text-destructive"
onClick={() => onDelete(original)}
>
<X />
</Button>
</div>
);
}
return (
<div class="flex justify-end">
<button
type="button"
class="inline-flex h-6 items-center justify-center text-muted-foreground hover:text-foreground"
onClick={() => onDeletePrompt(original)}
>
<i class="ri-delete-bin-line" />
</button>
</div>
);
}
},
{
id: 'trailing',
header: () => null,
enableSorting: false,
meta: {
class: 'w-[5px]'
},
cell: () => null
}
];
};