修改网盘显示等细节,登陆验证更加严格,同时允许一台设备在线

This commit is contained in:
yoyuzh
2026-03-20 18:08:59 +08:00
parent 43358e29d7
commit f8ea5a6f85
37 changed files with 1541 additions and 100 deletions

View File

@@ -0,0 +1,32 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { getInviteCodePanelState } from './dashboard-state';
test('getInviteCodePanelState returns a copyable invite code when summary contains one', () => {
assert.deepEqual(
getInviteCodePanelState({
totalUsers: 12,
totalFiles: 34,
inviteCode: ' AbCd1234 ',
}),
{
inviteCode: 'AbCd1234',
canCopy: true,
},
);
});
test('getInviteCodePanelState falls back to a placeholder when summary has no invite code', () => {
assert.deepEqual(
getInviteCodePanelState({
totalUsers: 12,
totalFiles: 34,
inviteCode: ' ',
}),
{
inviteCode: '未生成',
canCopy: false,
},
);
});

View File

@@ -0,0 +1,21 @@
import type { AdminSummary } from '@/src/lib/types';
export interface InviteCodePanelState {
inviteCode: string;
canCopy: boolean;
}
export function getInviteCodePanelState(summary: AdminSummary | null | undefined): InviteCodePanelState {
const inviteCode = summary?.inviteCode?.trim() ?? '';
if (!inviteCode) {
return {
inviteCode: '未生成',
canCopy: false,
};
}
return {
inviteCode,
canCopy: true,
};
}

View File

@@ -1,9 +1,13 @@
import { useEffect, useState } from 'react';
import { Alert, Card, CardContent, Chip, CircularProgress, Grid, Stack, Typography } from '@mui/material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Alert, Button, Card, CardContent, Chip, CircularProgress, Grid, Stack, Typography } from '@mui/material';
import { useNavigate } from 'react-router-dom';
import { apiRequest } from '@/src/lib/api';
import { readStoredSession } from '@/src/lib/session';
import type { AdminSummary } from '@/src/lib/types';
import { getInviteCodePanelState } from './dashboard-state';
interface DashboardState {
summary: AdminSummary | null;
@@ -33,12 +37,31 @@ export function PortalAdminDashboard() {
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [copyMessage, setCopyMessage] = useState('');
const navigate = useNavigate();
const session = readStoredSession();
async function loadDashboardData() {
setLoading(true);
setError('');
try {
const summary = await apiRequest<AdminSummary>('/admin/summary');
setState({
summary,
});
} catch (requestError) {
setError(requestError instanceof Error ? requestError.message : '后台首页数据加载失败');
} finally {
setLoading(false);
}
}
useEffect(() => {
let active = true;
async function loadDashboardData() {
void (async () => {
setLoading(true);
setError('');
@@ -63,24 +86,52 @@ export function PortalAdminDashboard() {
setLoading(false);
}
}
}
loadDashboardData();
})();
return () => {
active = false;
};
}, []);
const inviteCodePanel = getInviteCodePanelState(state.summary);
async function handleRefreshInviteCode() {
setCopyMessage('');
await loadDashboardData();
}
async function handleCopyInviteCode() {
if (!inviteCodePanel.canCopy) {
return;
}
if (!navigator.clipboard?.writeText) {
setError('当前浏览器不支持复制邀请码');
return;
}
try {
await navigator.clipboard.writeText(inviteCodePanel.inviteCode);
setCopyMessage('邀请码已复制到剪贴板');
} catch (requestError) {
setError(requestError instanceof Error ? requestError.message : '复制邀请码失败');
}
}
return (
<Stack spacing={3} sx={{ p: 2 }}>
<Stack spacing={1}>
<Typography variant="h4" fontWeight={700}>
YOYUZH Admin
</Typography>
<Typography color="text.secondary">
react-admin `/api/admin/**`
</Typography>
<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} justifyContent="space-between" alignItems={{ xs: 'flex-start', sm: 'center' }}>
<Stack spacing={1}>
<Typography variant="h4" fontWeight={700}>
YOYUZH Admin
</Typography>
<Typography color="text.secondary">
react-admin `/api/admin/**`
</Typography>
</Stack>
<Button variant="outlined" onClick={() => navigate('/overview')}>
</Button>
</Stack>
{loading && (
@@ -113,7 +164,7 @@ export function PortalAdminDashboard() {
</Grid>
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>
<Grid size={{ xs: 12, md: 4 }}>
<Card variant="outlined">
<CardContent>
<Stack spacing={1}>
@@ -134,7 +185,7 @@ export function PortalAdminDashboard() {
</Card>
</Grid>
<Grid size={{ xs: 12, md: 6 }}>
<Grid size={{ xs: 12, md: 4 }}>
<Card variant="outlined">
<CardContent>
<Stack spacing={1}>
@@ -151,6 +202,57 @@ export function PortalAdminDashboard() {
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<Card variant="outlined">
<CardContent>
<Stack spacing={1.5}>
<Typography variant="h6" fontWeight={600}>
</Typography>
<Typography color="text.secondary">
</Typography>
<Typography
component="code"
sx={{
display: 'inline-block',
width: 'fit-content',
px: 1.5,
py: 1,
borderRadius: 1,
backgroundColor: 'action.hover',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: '0.95rem',
}}
>
{inviteCodePanel.inviteCode}
</Typography>
<Stack direction="row" spacing={1}>
<Button
variant="contained"
size="small"
startIcon={<ContentCopyIcon />}
onClick={() => void handleCopyInviteCode()}
disabled={!inviteCodePanel.canCopy}
>
</Button>
<Button
variant="outlined"
size="small"
startIcon={<RefreshIcon />}
onClick={() => void handleRefreshInviteCode()}
disabled={loading}
>
</Button>
</Stack>
{copyMessage && <Alert severity="success">{copyMessage}</Alert>}
</Stack>
</CardContent>
</Card>
</Grid>
</Grid>
</Stack>
);