import {Eye, EyeOff, RefreshCcw} from 'lucide-react'; import {useEffect, useMemo, useState} from 'react'; import { listAdminWorkVisibility, updateAdminWorkVisibility, } from '../api/adminApiClient'; import type {AdminWorkVisibilityEntryPayload} from '../api/adminApiTypes'; import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm'; import {handlePageError} from './pageUtils'; interface AdminWorkVisibilityPageProps { token: string; onUnauthorized: (message?: string) => void; } const sourceLabels: Record = { puzzle: '拼图', 'custom-world': '自定义世界', 'jump-hop': '跳一跳', 'wooden-fish': '敲木鱼', match3d: '抓大鹅', 'square-hole': '方洞挑战', 'visual-novel': '视觉小说', 'big-fish': '大鱼吃小鱼', 'bark-battle': '汪汪声浪', }; export function AdminWorkVisibilityPage({ token, onUnauthorized, }: AdminWorkVisibilityPageProps) { const [entries, setEntries] = useState([]); const [keyword, setKeyword] = useState(''); const [isLoading, setIsLoading] = useState(false); const [savingKey, setSavingKey] = useState(''); const [errorMessage, setErrorMessage] = useState(''); const {confirmWrite, confirmDialog} = useAdminWriteConfirm(); useEffect(() => { void refreshEntries(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); const filteredEntries = useMemo(() => { const normalizedKeyword = keyword.trim().toLowerCase(); if (!normalizedKeyword) { return entries; } return entries.filter((entry) => [ entry.sourceType, sourceLabels[entry.sourceType] ?? '', entry.title, entry.subtitle, entry.authorDisplayName, entry.publicWorkCode, entry.profileId, entry.workId, ] .join(' ') .toLowerCase() .includes(normalizedKeyword), ); }, [entries, keyword]); async function refreshEntries() { setIsLoading(true); setErrorMessage(''); try { const response = await listAdminWorkVisibility(token); setEntries(sortEntries(response.entries)); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { setIsLoading(false); } } async function handleToggle(entry: AdminWorkVisibilityEntryPayload) { const nextVisible = !entry.visible; const target = entry.title.trim() || entry.publicWorkCode || entry.profileId; const confirmed = await confirmWrite({ action: nextVisible ? '显示作品' : '隐藏作品', target, }); if (!confirmed) { return; } const rowKey = buildEntryKey(entry); setSavingKey(rowKey); setErrorMessage(''); try { const response = await updateAdminWorkVisibility(token, { sourceType: entry.sourceType, profileId: entry.profileId, visible: nextVisible, }); upsertEntry(response.entry); } catch (error: unknown) { handlePageError(error, onUnauthorized, setErrorMessage); } finally { setSavingKey(''); } } function upsertEntry(next: AdminWorkVisibilityEntryPayload) { setEntries((current) => sortEntries([ ...current.filter((entry) => buildEntryKey(entry) !== buildEntryKey(next)), next, ]), ); } return (

作品可见性

{errorMessage ? (
{errorMessage}
) : null}
{filteredEntries.map((entry) => { const rowKey = buildEntryKey(entry); const isSaving = savingKey === rowKey; return ( ); })}
玩法 作品 作者 公开码 更新时间 状态 操作
{sourceLabels[entry.sourceType] ?? entry.sourceType} {entry.title || entry.profileId} {entry.subtitle || entry.profileId} {entry.authorDisplayName || '玩家'} {entry.ownerUserId} {entry.publicWorkCode} {entry.profileId} {formatMicros(entry.updatedAtMicros)} {entry.visible ? '显示' : '隐藏'}
{!isLoading && filteredEntries.length === 0 ? (
暂无作品
) : null}
{confirmDialog}
); } function sortEntries(entries: AdminWorkVisibilityEntryPayload[]) { return [...entries].sort((left, right) => { const timeCompare = right.updatedAtMicros - left.updatedAtMicros; if (timeCompare !== 0) { return timeCompare; } const sourceCompare = left.sourceType.localeCompare(right.sourceType); if (sourceCompare !== 0) { return sourceCompare; } return left.profileId.localeCompare(right.profileId); }); } function buildEntryKey(entry: AdminWorkVisibilityEntryPayload) { return `${entry.sourceType}:${entry.profileId}`; } function formatMicros(value: number) { if (!Number.isFinite(value)) { return '-'; } const date = new Date(Math.floor(value / 1000)); if (!Number.isFinite(date.getTime())) { return '-'; } return date.toLocaleString('zh-CN', {hour12: false}); }