2026-06-30 10:52:45 +08:00

338 lines
14 KiB
JavaScript

import AddOutlined from "@mui/icons-material/AddOutlined";
import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined";
import FormatListBulletedOutlined from "@mui/icons-material/FormatListBulletedOutlined";
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
import RestartAltOutlined from "@mui/icons-material/RestartAltOutlined";
import SaveOutlined from "@mui/icons-material/SaveOutlined";
import Dialog from "@mui/material/Dialog";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { Button } from "@/shared/ui/Button.jsx";
import { DataState } from "@/shared/ui/DataState.jsx";
import { DataTable } from "@/shared/ui/DataTable.jsx";
import { IconButton } from "@/shared/ui/IconButton.jsx";
import { AdminListBody, AdminListPage, AdminListToolbar } from "@/shared/ui/AdminListLayout.jsx";
import { formatMillis } from "@/shared/utils/time.js";
import { useRegionBlockPage } from "@/features/region-blocks/hooks/useRegionBlockPage.js";
import styles from "@/features/region-blocks/region-blocks.module.css";
export function RegionBlockPage() {
const page = useRegionBlockPage();
const columns = buildColumns(page);
const whitelistColumns = buildWhitelistColumns(page);
const userWhitelistColumns = buildUserWhitelistColumns(page);
return (
<AdminListPage>
<AdminListToolbar
actions={
<>
<Button
disabled={!page.dirty || page.saving}
startIcon={<RestartAltOutlined fontSize="small" />}
onClick={page.resetDraft}
>
重置
</Button>
<Button
disabled={page.loading || page.saving}
startIcon={<RefreshOutlined fontSize="small" />}
onClick={page.reload}
>
刷新
</Button>
<Button
disabled={!page.abilities.canUpdate || !page.dirty || page.saving}
startIcon={<SaveOutlined fontSize="small" />}
variant="primary"
onClick={page.save}
>
保存
</Button>
</>
}
filters={
<div className={styles.forms}>
<form className={styles.addForm} onSubmit={page.submitInput}>
<TextField
className={styles.keywordInput}
disabled={!page.abilities.canUpdate || page.saving}
label="屏蔽词"
placeholder="国家码或国家名称"
size="small"
value={page.keywordInput}
onChange={(event) => page.setKeywordInput(event.target.value)}
/>
<Button
disabled={!page.abilities.canUpdate || page.saving}
startIcon={<AddOutlined fontSize="small" />}
type="submit"
variant="primary"
>
添加屏蔽词
</Button>
</form>
<form className={styles.addForm} onSubmit={page.submitIPInput}>
<TextField
className={styles.ipInput}
disabled={!page.abilities.canUpdate || page.saving}
label="IP白名单"
placeholder="203.0.113.10"
size="small"
value={page.ipInput}
onChange={(event) => page.setIPInput(event.target.value)}
/>
<Button
disabled={!page.abilities.canUpdate || page.saving}
startIcon={<AddOutlined fontSize="small" />}
type="submit"
variant="primary"
>
添加IP
</Button>
</form>
<Button
disabled={page.loading}
startIcon={<FormatListBulletedOutlined fontSize="small" />}
onClick={page.openWhitelistDialog}
>
白名单IP列表
</Button>
<form className={styles.addForm} onSubmit={page.submitUserIDInput}>
<TextField
className={styles.userInput}
disabled={!page.abilities.canUpdate || page.saving}
label="用户ID/短ID/靓号白名单"
placeholder="900025 或 VIP2026"
size="small"
value={page.userIDInput}
onChange={(event) => page.setUserIDInput(event.target.value)}
/>
<Button
disabled={!page.abilities.canUpdate || page.saving}
startIcon={<AddOutlined fontSize="small" />}
type="submit"
variant="primary"
>
添加用户
</Button>
</form>
</div>
}
/>
<DataState error={page.error} loading={page.loading} onRetry={page.reload}>
<AdminListBody>
<div className={styles.tableGroup}>
<h2 className={styles.tableTitle}>地区屏蔽</h2>
<DataTable
columns={columns}
items={page.draftItems}
minWidth="880px"
rowKey={(item) => item.blockId || item.keyword}
/>
</div>
<div className={styles.tableGroup}>
<h2 className={styles.tableTitle}>IP白名单</h2>
<DataTable
columns={whitelistColumns}
items={page.draftWhitelistItems}
minWidth="720px"
rowKey={(item) => item.whitelistId || item.ipAddress}
/>
</div>
<div className={styles.tableGroup}>
<h2 className={styles.tableTitle}>用户ID/短ID/靓号白名单</h2>
<DataTable
columns={userWhitelistColumns}
items={page.draftWhitelistUserItems}
minWidth="860px"
rowKey={(item) => item.whitelistId || item.userId}
/>
</div>
</AdminListBody>
</DataState>
<WhitelistDialog page={page} />
</AdminListPage>
);
}
function buildColumns(page) {
return [
{
key: "keyword",
label: "屏蔽词",
width: "minmax(220px, 1.2fr)",
render: (item) => <KeywordCell item={item} />,
},
{
key: "countryCode",
label: "国家码",
width: "minmax(120px, 0.6fr)",
render: (item) => item.countryCode || "保存后解析",
},
{
key: "updatedAtMs",
label: "更新时间",
width: "minmax(180px, 0.85fr)",
render: (item) => (item.updatedAtMs ? formatMillis(item.updatedAtMs) : "-"),
},
{
key: "actions",
label: "操作",
width: "minmax(96px, 0.4fr)",
render: (item) => (
<Tooltip arrow title="移除">
<span>
<IconButton
disabled={!page.abilities.canUpdate || page.saving}
label="移除"
tone="danger"
onClick={() => page.removeKeyword(item.keyword)}
>
<DeleteOutlineOutlined fontSize="small" />
</IconButton>
</span>
</Tooltip>
),
},
];
}
function buildWhitelistColumns(page) {
return [
{
key: "ipAddress",
label: "IP",
width: "minmax(220px, 1fr)",
render: (item) => <KeywordCell item={{ keyword: item.ipAddress, enabled: item.enabled }} />,
},
{
key: "updatedAtMs",
label: "更新时间",
width: "minmax(180px, 0.8fr)",
render: (item) => (item.updatedAtMs ? formatMillis(item.updatedAtMs) : "-"),
},
{
key: "actions",
label: "操作",
width: "minmax(96px, 0.4fr)",
render: (item) => (
<Tooltip arrow title="移除">
<span>
<IconButton
disabled={!page.abilities.canUpdate || page.saving}
label="移除"
tone="danger"
onClick={() => page.removeIP(item.ipAddress)}
>
<DeleteOutlineOutlined fontSize="small" />
</IconButton>
</span>
</Tooltip>
),
},
];
}
function buildUserWhitelistColumns(page) {
return [
{
key: "userId",
label: "内部用户ID",
width: "minmax(220px, 1fr)",
render: (item) => <KeywordCell item={{ keyword: item.userId, enabled: item.enabled }} />,
},
{
key: "displayUserId",
label: "当前短ID/靓号",
width: "minmax(180px, 0.8fr)",
render: (item) => item.displayUserId || (String(item.whitelistId || "").startsWith("draft-user-") ? "保存后解析" : "-"),
},
{
key: "updatedAtMs",
label: "更新时间",
width: "minmax(180px, 0.8fr)",
render: (item) => (item.updatedAtMs ? formatMillis(item.updatedAtMs) : "-"),
},
{
key: "actions",
label: "操作",
width: "minmax(96px, 0.4fr)",
render: (item) => (
<Tooltip arrow title="移除">
<span>
<IconButton
disabled={!page.abilities.canUpdate || page.saving}
label="移除"
tone="danger"
onClick={() => page.removeUserID(item.userId)}
>
<DeleteOutlineOutlined fontSize="small" />
</IconButton>
</span>
</Tooltip>
),
},
];
}
function buildWhitelistDialogColumns() {
return [
{
key: "ipAddress",
label: "IP",
width: "minmax(220px, 1fr)",
render: (item) => <KeywordCell item={{ keyword: item.ipAddress, enabled: item.enabled }} />,
},
{
key: "updatedAtMs",
label: "更新时间",
width: "minmax(180px, 0.8fr)",
render: (item) => (item.updatedAtMs ? formatMillis(item.updatedAtMs) : "-"),
},
];
}
function WhitelistDialog({ page }) {
const columns = buildWhitelistDialogColumns();
return (
<Dialog
maxWidth="md"
open={page.whitelistDialogOpen}
slotProps={{ paper: { className: styles.whitelistDialogPaper } }}
fullWidth
onClose={page.closeWhitelistDialog}
>
<DialogTitle className={styles.whitelistDialogTitle}>
<span>白名单IP列表</span>
<small> {page.draftWhitelistItems.length} </small>
</DialogTitle>
<DialogContent className={styles.whitelistDialogContent}>
<DataTable
columns={columns}
emptyLabel="当前无数据"
items={page.draftWhitelistItems}
minWidth="520px"
rowKey={(item) => item.whitelistId || item.ipAddress}
/>
</DialogContent>
<DialogActions className={styles.whitelistDialogActions}>
<Button onClick={page.closeWhitelistDialog}>关闭</Button>
</DialogActions>
</Dialog>
);
}
function KeywordCell({ item }) {
return (
<div className={styles.keywordCell}>
<span className={styles.keyword}>{item.keyword}</span>
<span className={styles.meta}>{item.enabled ? "启用" : "停用"}</span>
</div>
);
}