refactor(ui): 统一页面头部组件并集成字典数据

- 引入统一的 PageHeader 组件替换各页面自定义头部结构
- 集成 useDict 钩子实现状态标签的动态字典映射
- 更新设备、字典、日志、组织、权限、租户等页面的状态渲染逻辑
- 替换硬编码的选择框选项为字典数据驱动
- 优化日志页面的标签页结构支持动态字典配置
- 统一各页面标题区域的样式和布局结构
master
chenhao 2026-02-27 10:27:57 +08:00
parent 351e56a059
commit 1ae81909c2
13 changed files with 295 additions and 221 deletions

View File

@ -10,6 +10,7 @@ import {
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import StatCard from "../components/shared/StatCard/StatCard"; import StatCard from "../components/shared/StatCard/StatCard";
import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -65,13 +66,11 @@ export default function Dashboard() {
return ( return (
<div className="dashboard-page p-6"> <div className="dashboard-page p-6">
<div className="mb-6 flex justify-between items-end"> <PageHeader
<div> title={t('dashboard.title')}
<Title level={4} className="mb-1">{t('dashboard.title')}</Title> subtitle={t('dashboard.subtitle')}
<Text type="secondary">{t('dashboard.subtitle')}</Text> extra={<Button icon={<SyncOutlined aria-hidden="true" />} size="small">{t('common.refresh')}</Button>}
</div> />
<Button icon={<SyncOutlined aria-hidden="true" />} size="small">{t('common.refresh')}</Button>
</div>
<Row gutter={[24, 24]}> <Row gutter={[24, 24]}>
<Col xs={24} sm={12} lg={6}> <Col xs={24} sm={12} lg={6}>

View File

@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
import { createDevice, deleteDevice, listDevices, updateDevice, listUsers } from "../api"; import { createDevice, deleteDevice, listDevices, updateDevice, listUsers } from "../api";
import type { DeviceInfo, SysUser } from "../types"; import type { DeviceInfo, SysUser } from "../types";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@ -25,7 +26,7 @@ import {
DesktopOutlined, DesktopOutlined,
UserOutlined UserOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import "./Devices.css"; import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -37,6 +38,9 @@ export default function Devices() {
const [data, setData] = useState<DeviceInfo[]>([]); const [data, setData] = useState<DeviceInfo[]>([]);
const [users, setUsers] = useState<SysUser[]>([]); const [users, setUsers] = useState<SysUser[]>([]);
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
// Search state // Search state
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
@ -165,11 +169,14 @@ export default function Devices() {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 100, width: 100,
render: (status: number) => ( render: (status: number) => {
const item = statusDict.find(i => i.itemValue === String(status));
return (
<Tag color={status === 1 ? "green" : "red"}> <Tag color={status === 1 ? "green" : "red"}>
{status === 1 ? "启用" : "禁用"} {item ? item.itemLabel : (status === 1 ? "启用" : "禁用")}
</Tag> </Tag>
), );
},
}, },
{ {
title: t('devices.updateTime'), title: t('devices.updateTime'),
@ -209,17 +216,15 @@ export default function Devices() {
return ( return (
<div className="devices-page p-6"> <div className="devices-page p-6">
<div className="devices-header flex justify-between items-end mb-6"> <PageHeader
<div> title={t('devices.title')}
<Title level={4} className="mb-1">{t('devices.title')}</Title> subtitle={t('devices.subtitle')}
<Text type="secondary">{t('devices.subtitle')}</Text> extra={can("device:create") && (
</div>
{can("device:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}> <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
{t('devices.drawerTitleCreate')} {t('devices.drawerTitleCreate')}
</Button> </Button>
)} )}
</div> />
<Card className="devices-table-card shadow-sm"> <Card className="devices-table-card shadow-sm">
<div className="devices-table-toolbar mb-4"> <div className="devices-table-toolbar mb-4">
@ -283,7 +288,7 @@ export default function Devices() {
<Input placeholder="例如:会议室 A 转录仪" /> <Input placeholder="例如:会议室 A 转录仪" />
</Form.Item> </Form.Item>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ value: 1, label: "正常启用" }, { value: 0, label: "禁用接入" }]} /> <Select options={statusDict.map(i => ({ value: Number(i.itemValue), label: i.itemLabel }))} />
</Form.Item> </Form.Item>
</Form> </Form>
</Drawer> </Drawer>

View File

@ -30,7 +30,9 @@ import {
} from "../api"; } from "../api";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, ProfileOutlined } from "@ant-design/icons"; import { PlusOutlined, EditOutlined, DeleteOutlined, BookOutlined, ProfileOutlined } from "@ant-design/icons";
import { useDict } from "../hooks/useDict";
import type { SysDictItem, SysDictType } from "../types"; import type { SysDictItem, SysDictType } from "../types";
import PageHeader from "../components/shared/PageHeader";
import "./Dictionaries.css"; import "./Dictionaries.css";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -44,6 +46,9 @@ export default function Dictionaries() {
const [loadingTypes, setLoadingTypes] = useState(false); const [loadingTypes, setLoadingTypes] = useState(false);
const [loadingItems, setLoadingItems] = useState(false); const [loadingItems, setLoadingItems] = useState(false);
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
// Type Drawer // Type Drawer
const [typeDrawerVisible, setTypeDrawerVisible] = useState(false); const [typeDrawerVisible, setTypeDrawerVisible] = useState(false);
const [editingType, setEditingType] = useState<SysDictType | null>(null); const [editingType, setEditingType] = useState<SysDictType | null>(null);
@ -158,12 +163,19 @@ export default function Dictionaries() {
return ( return (
<div className="dictionaries-page p-6"> <div className="dictionaries-page p-6">
<div className="dictionaries-header mb-6"> <PageHeader
<div> title={t('dicts.title')}
<Title level={4} className="mb-1">{t('dicts.title')}</Title> subtitle={t('dicts.subtitle')}
<Text type="secondary">{t('dicts.subtitle')}</Text> extra={can("sys_dict:type:create") && (
</div> <Button
</div> type="primary"
icon={<PlusOutlined aria-hidden="true" />}
onClick={handleAddType}
>
{t('common.create')}
</Button>
)}
/>
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}> <Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
<Col span={8} style={{ height: '100%' }}> <Col span={8} style={{ height: '100%' }}>
@ -175,18 +187,6 @@ export default function Dictionaries() {
</Space> </Space>
} }
className="full-height-card shadow-sm" className="full-height-card shadow-sm"
extra={
can("sys_dict:type:create") && (
<Button
type="primary"
size="small"
icon={<PlusOutlined aria-hidden="true" />}
onClick={handleAddType}
>
{t('common.create')}
</Button>
)
}
> >
<div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}> <div style={{ height: 'calc(100% - 10px)', overflowY: 'auto' }}>
<Table <Table
@ -299,11 +299,14 @@ export default function Dictionaries() {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 100, width: 100,
render: (v) => ( render: (v) => {
const item = statusDict.find(i => i.itemValue === String(v));
return (
<Tag color={v === 1 ? "green" : "red"}> <Tag color={v === 1 ? "green" : "red"}>
{v === 1 ? "启用" : "禁用"} {item ? item.itemLabel : (v === 1 ? "启用" : "禁用")}
</Tag> </Tag>
) );
}
}, },
{ {
title: t('common.action'), title: t('common.action'),
@ -412,10 +415,7 @@ export default function Dictionaries() {
</Form.Item> </Form.Item>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select <Select
options={[ options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))}
{ label: "启用", value: 1 },
{ label: "禁用", value: 0 }
]}
/> />
</Form.Item> </Form.Item>
<Form.Item label={t('common.remark')} name="remark"> <Form.Item label={t('common.remark')} name="remark">

View File

@ -2,8 +2,10 @@ import { Card, Table, Tabs, Tag, Input, Space, Button, DatePicker, Select, Typog
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { fetchLogs } from "../api"; import { fetchLogs } from "../api";
import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOutlined } from "@ant-design/icons"; import { SearchOutlined, ReloadOutlined, InfoCircleOutlined, EyeOutlined, UserOutlined, FileTextOutlined } from "@ant-design/icons";
import { SysLog, UserProfile } from "../types"; import { SysLog, UserProfile } from "../types";
import { useDict } from "../hooks/useDict";
import PageHeader from "../components/shared/PageHeader";
const { RangePicker } = DatePicker; const { RangePicker } = DatePicker;
const { Text, Title } = Typography; const { Text, Title } = Typography;
@ -26,6 +28,10 @@ export default function Logs() {
sortOrder: "descend" as any sortOrder: "descend" as any
}); });
// Dictionaries
const { items: logTypeDict } = useDict("sys_log_type");
const { items: logStatusDict } = useDict("sys_log_status");
// Get user profile to check platform admin // Get user profile to check platform admin
const userProfile = useMemo(() => { const userProfile = useMemo(() => {
const stored = sessionStorage.getItem("userProfile"); const stored = sessionStorage.getItem("userProfile");
@ -151,11 +157,14 @@ export default function Logs() {
dataIndex: "status", dataIndex: "status",
key: "status", key: "status",
width: 90, width: 90,
render: (status: number) => ( render: (status: number) => {
const item = logStatusDict.find(i => i.itemValue === String(status));
return (
<Tag color={status === 1 ? "green" : "red"} className="m-0"> <Tag color={status === 1 ? "green" : "red"} className="m-0">
{status === 1 ? "成功" : "失败"} {item ? item.itemLabel : (status === 1 ? "成功" : "失败")}
</Tag> </Tag>
) );
}
}, },
{ {
title: t('logs.time'), title: t('logs.time'),
@ -196,10 +205,10 @@ export default function Logs() {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mb-6"> <PageHeader
<Title level={4} className="mb-1">{t('logs.title')}</Title> title={t('logs.title')}
<Text type="secondary">{t('logs.subtitle')}</Text> subtitle={t('logs.subtitle')}
</div> />
<Card className="mb-4 shadow-sm"> <Card className="mb-4 shadow-sm">
<Space wrap size="middle"> <Space wrap size="middle">
@ -217,10 +226,7 @@ export default function Logs() {
allowClear allowClear
value={params.status} value={params.status}
onChange={v => setParams({ ...params, status: v })} onChange={v => setParams({ ...params, status: v })}
options={[ options={logStatusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))}
{ label: "成功", value: 1 },
{ label: "失败", value: 0 }
]}
aria-label={t('common.status')} aria-label={t('common.status')}
/> />
<RangePicker <RangePicker
@ -251,6 +257,20 @@ export default function Logs() {
<Card className="shadow-sm" styles={{ body: { paddingTop: 0 } }}> <Card className="shadow-sm" styles={{ body: { paddingTop: 0 } }}>
<Tabs activeKey={activeTab} onChange={setActiveTab} size="large"> <Tabs activeKey={activeTab} onChange={setActiveTab} size="large">
{logTypeDict.length > 0 ? (
logTypeDict.map(item => (
<Tabs.TabPane
tab={
<span>
{item.itemValue === 'OPERATION' ? <InfoCircleOutlined aria-hidden="true" /> : <UserOutlined aria-hidden="true" />}
{item.itemLabel}
</span>
}
key={item.itemValue}
/>
))
) : (
<>
<Tabs.TabPane <Tabs.TabPane
tab={<span><InfoCircleOutlined aria-hidden="true" />{t('logs.opLog')}</span>} tab={<span><InfoCircleOutlined aria-hidden="true" />{t('logs.opLog')}</span>}
key="OPERATION" key="OPERATION"
@ -259,6 +279,8 @@ export default function Logs() {
tab={<span><UserOutlined aria-hidden="true" />{t('logs.loginLog')}</span>} tab={<span><UserOutlined aria-hidden="true" />{t('logs.loginLog')}</span>}
key="LOGIN" key="LOGIN"
/> />
</>
)}
</Tabs> </Tabs>
<Table <Table
@ -305,7 +327,7 @@ export default function Logs() {
<Descriptions.Item label={t('logs.duration')}>{selectedLog.duration ? `${selectedLog.duration}ms` : "-"}</Descriptions.Item> <Descriptions.Item label={t('logs.duration')}>{selectedLog.duration ? `${selectedLog.duration}ms` : "-"}</Descriptions.Item>
<Descriptions.Item label={t('common.status')}> <Descriptions.Item label={t('common.status')}>
<Tag color={selectedLog.status === 1 ? "green" : "red"}> <Tag color={selectedLog.status === 1 ? "green" : "red"}>
{selectedLog.status === 1 ? "成功" : "失败"} {logStatusDict.find(i => i.itemValue === String(selectedLog.status))?.itemLabel || (selectedLog.status === 1 ? "成功" : "失败")}
</Tag> </Tag>
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={t('logs.time')} className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item> <Descriptions.Item label={t('logs.time')} className="tabular-nums">{selectedLog.createdAt?.replace('T', ' ')}</Descriptions.Item>

View File

@ -20,6 +20,7 @@ import { useEffect, useState, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createOrg, deleteOrg, listOrgs, updateOrg, listTenants } from "../api"; import { createOrg, deleteOrg, listOrgs, updateOrg, listTenants } from "../api";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@ -29,6 +30,7 @@ import {
ShopOutlined ShopOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import type { SysOrg, SysTenant, OrgNode } from "../types"; import type { SysOrg, SysTenant, OrgNode } from "../types";
import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -59,6 +61,10 @@ function buildOrgTree(list: SysOrg[]): OrgNode[] {
export default function Orgs() { export default function Orgs() {
const { t } = useTranslation(); const { t } = useTranslation();
const { can } = usePermission(); const { can } = usePermission();
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysOrg[]>([]); const [data, setData] = useState<SysOrg[]>([]);
@ -200,7 +206,14 @@ export default function Orgs() {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 100, width: 100,
render: (s: number) => <Tag color={s === 1 ? "green" : "red"}>{s === 1 ? "启用" : "禁用"}</Tag> render: (s: number) => {
const item = statusDict.find(i => i.itemValue === String(s));
return (
<Tag color={s === 1 ? "green" : "red"}>
{item ? item.itemLabel : (s === 1 ? "启用" : "禁用")}
</Tag>
);
}
}, },
{ {
title: t('common.action'), title: t('common.action'),
@ -226,17 +239,15 @@ export default function Orgs() {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mb-6 flex justify-between items-end"> <PageHeader
<div> title={t('orgs.title')}
<Title level={4} className="mb-1">{t('orgs.title')}</Title> subtitle={t('orgs.subtitle')}
<Text type="secondary">{t('orgs.subtitle')}</Text> extra={can("sys:org:create") && (
</div>
{can("sys:org:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}> <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={() => openCreate()}>
{t('orgs.createRoot')} {t('orgs.createRoot')}
</Button> </Button>
)} )}
</div> />
{isPlatformMode && ( {isPlatformMode && (
<Card className="shadow-sm mb-4"> <Card className="shadow-sm mb-4">
@ -332,7 +343,7 @@ export default function Orgs() {
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} /> <Select options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))} />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>

View File

@ -21,6 +21,7 @@ import { useTranslation } from "react-i18next";
import { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api"; import { createPermission, deletePermission, listMyPermissions, updatePermission } from "../api";
import type { SysPermission } from "../types"; import type { SysPermission } from "../types";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@ -32,6 +33,7 @@ import {
CheckSquareOutlined, CheckSquareOutlined,
InfoCircleOutlined InfoCircleOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -75,6 +77,12 @@ export default function Permissions() {
const { can } = usePermission(); const { can } = usePermission();
const level = Form.useWatch("level", form); const level = Form.useWatch("level", form);
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
const { items: typeDict } = useDict("sys_permission_type");
const { items: visibleDict } = useDict("sys_common_visibility");
const { items: levelDict } = useDict("sys_permission_level");
const load = async () => { const load = async () => {
setLoading(true); setLoading(true);
try { try {
@ -206,11 +214,14 @@ export default function Permissions() {
title: t('permissions.permType'), title: t('permissions.permType'),
dataIndex: "permType", dataIndex: "permType",
width: 90, width: 90,
render: (type: string) => ( render: (type: string) => {
const item = typeDict.find(i => i.itemValue === type);
return (
<Tag color={type === 'menu' ? 'processing' : 'warning'}> <Tag color={type === 'menu' ? 'processing' : 'warning'}>
{type === 'menu' ? '菜单' : '按钮'} {item ? item.itemLabel : type}
</Tag> </Tag>
) );
}
}, },
{ {
title: t('permissions.sort'), title: t('permissions.sort'),
@ -233,13 +244,19 @@ export default function Permissions() {
title: t('permissions.visible'), title: t('permissions.visible'),
dataIndex: "isVisible", dataIndex: "isVisible",
width: 80, width: 80,
render: (v: number) => (v === 1 ? <Tag color="blue"></Tag> : <Tag></Tag>) render: (v: number) => {
const item = visibleDict.find(i => i.itemValue === String(v));
return (v === 1 ? <Tag color="blue">{item?.itemLabel || '可见'}</Tag> : <Tag>{item?.itemLabel || '隐藏'}</Tag>);
}
}, },
{ {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 80, width: 80,
render: (v: number) => (v === 1 ? <Tag color="green"></Tag> : <Tag color="red"></Tag>) render: (v: number) => {
const item = statusDict.find(i => i.itemValue === String(v));
return (v === 1 ? <Tag color="green">{item?.itemLabel || '启用'}</Tag> : <Tag color="red">{item?.itemLabel || '禁用'}</Tag>);
}
}, },
{ {
title: t('common.action'), title: t('common.action'),
@ -284,12 +301,10 @@ export default function Permissions() {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mb-6 flex justify-between items-end"> <PageHeader
<div> title={t('permissions.title')}
<Title level={4} className="mb-1">{t('permissions.title')}</Title> subtitle={t('permissions.subtitle')}
<Text type="secondary">{t('permissions.subtitle')}</Text> extra={can("sys:permission:create") && (
</div>
{can("sys:permission:create") && (
<Button <Button
type="primary" type="primary"
icon={<PlusOutlined aria-hidden="true" />} icon={<PlusOutlined aria-hidden="true" />}
@ -298,7 +313,7 @@ export default function Permissions() {
{t('common.create')} {t('common.create')}
</Button> </Button>
)} )}
</div> />
<Card className="mb-4 shadow-sm"> <Card className="mb-4 shadow-sm">
<Space wrap size="middle"> <Space wrap size="middle">
@ -324,10 +339,7 @@ export default function Permissions() {
allowClear allowClear
value={query.permType || undefined} value={query.permType || undefined}
onChange={(v) => setQuery({ ...query, permType: v || "" })} onChange={(v) => setQuery({ ...query, permType: v || "" })}
options={[ options={typeDict.map(i => ({ value: i.itemValue, label: i.itemLabel }))}
{ value: "menu", label: "菜单" },
{ value: "button", label: "按钮" }
]}
style={{ width: 120 }} style={{ width: 120 }}
aria-label={t('permissions.permType')} aria-label={t('permissions.permType')}
/> />
@ -395,16 +407,18 @@ export default function Permissions() {
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item label={t('permissions.level')} name="level" rules={[{ required: true }]}> <Form.Item label={t('permissions.level')} name="level" rules={[{ required: true }]}>
<Select aria-label={t('permissions.level')}> <Select
<Select.Option value={1}></Select.Option> options={levelDict.map(i => ({ value: Number(i.itemValue), label: i.itemLabel }))}
<Select.Option value={2}></Select.Option> aria-label={t('permissions.level')}
<Select.Option value={3}></Select.Option> />
</Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item label={t('permissions.permType')} name="permType" rules={[{ required: true }]}> <Form.Item label={t('permissions.permType')} name="permType" rules={[{ required: true }]}>
<Select options={[{ value: "menu", label: "菜单" }, { value: "button", label: "按钮" }]} aria-label={t('permissions.permType')} /> <Select
options={typeDict.map(i => ({ value: i.itemValue, label: i.itemLabel }))}
aria-label={t('permissions.permType')}
/>
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
@ -484,12 +498,12 @@ export default function Permissions() {
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item label={t('permissions.isVisible')} name="isVisible" initialValue={1}> <Form.Item label={t('permissions.isVisible')} name="isVisible" initialValue={1}>
<Select options={[{ value: 1, label: "显示" }, { value: 0, label: "隐藏" }]} /> <Select options={visibleDict.map(i => ({ value: Number(i.itemValue), label: i.itemLabel }))} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col span={12}>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ value: 1, label: "启用" }, { value: 0, label: "禁用" }]} /> <Select options={statusDict.map(i => ({ value: Number(i.itemValue), label: i.itemLabel }))} />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>

View File

@ -22,6 +22,7 @@ import {
FileTextOutlined FileTextOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import type { SysPlatformConfig } from "../types"; import type { SysPlatformConfig } from "../types";
import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -85,11 +86,10 @@ export default function PlatformSettings() {
return ( return (
<div className="p-6 max-w-4xl mx-auto"> <div className="p-6 max-w-4xl mx-auto">
<div className="flex justify-between items-end mb-6"> <PageHeader
<div> title={t('platformSettings.title')}
<Title level={4} className="mb-1">{t('platformSettings.title')}</Title> subtitle={t('platformSettings.subtitle')}
<Text type="secondary">{t('platformSettings.subtitle')}</Text> extra={(
</div>
<Button <Button
type="primary" type="primary"
icon={<SaveOutlined />} icon={<SaveOutlined />}
@ -98,7 +98,8 @@ export default function PlatformSettings() {
> >
{t('common.save')} {t('common.save')}
</Button> </Button>
</div> )}
/>
<Form <Form
form={form} form={form}

View File

@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "../api"; import { listPermissions, listRolePermissions, listRoles, saveRolePermissions } from "../api";
import { SearchOutlined, SafetyCertificateOutlined, SaveOutlined, KeyOutlined, ClusterOutlined } from "@ant-design/icons"; import { SearchOutlined, SafetyCertificateOutlined, SaveOutlined, KeyOutlined, ClusterOutlined } from "@ant-design/icons";
import type { SysPermission, SysRole } from "../types"; import type { SysPermission, SysRole } from "../types";
import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -178,11 +179,10 @@ export default function RolePermissionBinding() {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mb-6 flex justify-between items-end"> <PageHeader
<div> title={t('rolePerm.title')}
<Title level={4} className="mb-1">{t('rolePerm.title')}</Title> subtitle={t('rolePerm.subtitle')}
<Text type="secondary">{t('rolePerm.subtitle')}</Text> extra={(
</div>
<Button <Button
type="primary" type="primary"
icon={<SaveOutlined aria-hidden="true" />} icon={<SaveOutlined aria-hidden="true" />}
@ -192,7 +192,8 @@ export default function RolePermissionBinding() {
> >
{saving ? t('common.loading') : t('rolePerm.savePolicy')} {saving ? t('common.loading') : t('rolePerm.savePolicy')}
</Button> </Button>
</div> )}
/>
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}> <Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
<Col xs={24} lg={10} style={{ height: '100%' }}> <Col xs={24} lg={10} style={{ height: '100%' }}>

View File

@ -34,8 +34,9 @@ import {
unbindUserFromRole, unbindUserFromRole,
listUsers listUsers
} from "../api"; } from "../api";
import type {SysPermission, SysRole, SysTenant, SysUser} from "../types"; import { SysPermission, SysRole, SysTenant, SysUser } from "../types";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
EditOutlined, EditOutlined,
PlusOutlined, PlusOutlined,
@ -47,6 +48,7 @@ import {
SaveOutlined, SaveOutlined,
UserAddOutlined UserAddOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import PageHeader from "../components/shared/PageHeader";
import "./Roles.css"; import "./Roles.css";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -106,6 +108,9 @@ export default function Roles() {
const [permissions, setPermissions] = useState<SysPermission[]>([]); const [permissions, setPermissions] = useState<SysPermission[]>([]);
const [selectedRole, setSelectedRole] = useState<SysRole | null>(null); const [selectedRole, setSelectedRole] = useState<SysRole | null>(null);
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
// Platform admin check // Platform admin check
const isPlatformMode = useMemo(() => { const isPlatformMode = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile"); const profileStr = sessionStorage.getItem("userProfile");
@ -364,22 +369,25 @@ export default function Roles() {
return ( return (
<div className="roles-page-v2 p-6"> <div className="roles-page-v2 p-6">
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}> <PageHeader
{/* Left: Role List */}
<Col span={8} style={{ height: '100%' }}>
<Card
title={t('roles.title')} title={t('roles.title')}
className="full-height-card shadow-sm" subtitle={t('roles.subtitle')}
extra={can("sys:role:create") && ( extra={can("sys:role:create") && (
<Button <Button
type="primary" type="primary"
size="small"
icon={<PlusOutlined aria-hidden="true" />} icon={<PlusOutlined aria-hidden="true" />}
onClick={openCreate} onClick={openCreate}
> >
{t('common.create')} {t('common.create')}
</Button> </Button>
)} )}
/>
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
{/* Left: Role List */}
<Col span={8} style={{ height: '100%' }}>
<Card
title="角色列表"
className="full-height-card shadow-sm"
> >
<div className="mb-4 flex gap-2"> <div className="mb-4 flex gap-2">
{isPlatformMode && ( {isPlatformMode && (
@ -540,7 +548,14 @@ export default function Roles() {
title: t('common.status'), title: t('common.status'),
dataIndex: 'status', dataIndex: 'status',
width: 80, width: 80,
render: s => <Tag color={s === 1 ? 'green' : 'red'}>{s === 1 ? '正常' : '禁用'}</Tag> render: (s: number) => {
const item = statusDict.find(i => i.itemValue === String(s));
return (
<Tag color={s === 1 ? 'green' : 'red'}>
{item ? item.itemLabel : (s === 1 ? '正常' : '禁用')}
</Tag>
);
}
}, },
{ {
title: t('common.action'), title: t('common.action'),
@ -648,7 +663,7 @@ export default function Roles() {
<Input placeholder={t('roles.roleCode')} disabled={!!editing} /> <Input placeholder={t('roles.roleCode')} disabled={!!editing} />
</Form.Item> </Form.Item>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{label: '启用', value: 1}, {label: '禁用', value: 0}]} /> <Select options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))} />
</Form.Item> </Form.Item>
<Form.Item label={t('common.remark')} name="remark"> <Form.Item label={t('common.remark')} name="remark">
<Input.TextArea rows={3} /> <Input.TextArea rows={3} />

View File

@ -25,6 +25,7 @@ import {
deleteParam deleteParam
} from "../api"; } from "../api";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@ -35,6 +36,7 @@ import {
InfoCircleOutlined InfoCircleOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import type { SysParamVO, SysParamQuery } from "../types"; import type { SysParamVO, SysParamQuery } from "../types";
import PageHeader from "../components/shared/PageHeader";
import "./SysParams.css"; import "./SysParams.css";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -43,6 +45,10 @@ export default function SysParams() {
const { t } = useTranslation(); const { t } = useTranslation();
const { can } = usePermission(); const { can } = usePermission();
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
const { items: paramTypeDict } = useDict("sys_param_type");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysParamVO[]>([]); const [data, setData] = useState<SysParamVO[]>([]);
@ -176,11 +182,14 @@ export default function SysParams() {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 80, width: 80,
render: (status: number) => ( render: (status: number) => {
const item = statusDict.find(i => i.itemValue === String(status));
return (
<Tag color={status === 1 ? "green" : "red"}> <Tag color={status === 1 ? "green" : "red"}>
{status === 1 ? "正常" : "禁用"} {item ? item.itemLabel : (status === 1 ? "正常" : "禁用")}
</Tag> </Tag>
), );
},
}, },
{ {
title: t('common.action'), title: t('common.action'),
@ -208,17 +217,15 @@ export default function SysParams() {
return ( return (
<div className="sys-params-page p-6"> <div className="sys-params-page p-6">
<div className="sys-params-header mb-6"> <PageHeader
<div> title={t('sysParams.title')}
<Title level={4} className="mb-1">{t('sysParams.title')}</Title> subtitle={t('sysParams.subtitle')}
<Text type="secondary">{t('sysParams.subtitle')}</Text> extra={can("sys_param:create") && (
</div>
{can("sys_param:create") && (
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}> <Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
{t('common.create')} {t('common.create')}
</Button> </Button>
)} )}
</div> />
<Card className="sys-params-table-card shadow-sm mb-4"> <Card className="sys-params-table-card shadow-sm mb-4">
<Form <Form
@ -239,12 +246,7 @@ export default function SysParams() {
placeholder={t('sysParams.paramType')} placeholder={t('sysParams.paramType')}
allowClear allowClear
style={{ width: 150 }} style={{ width: 150 }}
options={[ options={paramTypeDict.map(i => ({ label: i.itemLabel, value: i.itemValue }))}
{ label: 'String', value: 'String' },
{ label: 'Number', value: 'Number' },
{ label: 'Boolean', value: 'Boolean' },
{ label: 'JSON', value: 'JSON' }
]}
/> />
</Form.Item> </Form.Item>
<Form.Item> <Form.Item>
@ -321,12 +323,7 @@ export default function SysParams() {
rules={[{ required: true, message: t('sysParams.paramType') }]} rules={[{ required: true, message: t('sysParams.paramType') }]}
> >
<Select <Select
options={[ options={paramTypeDict.map(i => ({ label: i.itemLabel, value: i.itemValue }))}
{ label: 'String', value: 'String' },
{ label: 'Number', value: 'Number' },
{ label: 'Boolean', value: 'Boolean' },
{ label: 'JSON', value: 'JSON' }
]}
/> />
</Form.Item> </Form.Item>
</Col> </Col>
@ -337,10 +334,7 @@ export default function SysParams() {
initialValue={1} initialValue={1}
> >
<Select <Select
options={[ options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))}
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]}
/> />
</Form.Item> </Form.Item>
</Col> </Col>

View File

@ -19,6 +19,7 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { createTenant, deleteTenant, listTenants, updateTenant } from "../api"; import { createTenant, deleteTenant, listTenants, updateTenant } from "../api";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@ -31,6 +32,7 @@ import {
UserOutlined UserOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import type { SysTenant } from "../types"; import type { SysTenant } from "../types";
import PageHeader from "../components/shared/PageHeader";
import dayjs from "dayjs"; import dayjs from "dayjs";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -38,6 +40,10 @@ const { Title, Text } = Typography;
export default function Tenants() { export default function Tenants() {
const { t } = useTranslation(); const { t } = useTranslation();
const { can } = usePermission(); const { can } = usePermission();
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [data, setData] = useState<SysTenant[]>([]); const [data, setData] = useState<SysTenant[]>([]);
@ -165,11 +171,14 @@ export default function Tenants() {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 100, width: 100,
render: (status: number) => ( render: (status: number) => {
const item = statusDict.find(i => i.itemValue === String(status));
return (
<Tag color={status === 1 ? "green" : "red"}> <Tag color={status === 1 ? "green" : "red"}>
{status === 1 ? "正常" : "禁用"} {item ? item.itemLabel : (status === 1 ? "正常" : "禁用")}
</Tag> </Tag>
), );
},
}, },
{ {
title: t('tenants.expireTime'), title: t('tenants.expireTime'),
@ -209,17 +218,15 @@ export default function Tenants() {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mb-6 flex justify-between items-end"> <PageHeader
<div> title={t('tenants.title')}
<Title level={4} className="mb-1">{t('tenants.title')}</Title> subtitle={t('tenants.subtitle')}
<Text type="secondary">{t('tenants.subtitle')}</Text> extra={can("sys_tenant:create") && (
</div>
{can("sys_tenant:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}> <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
{t('tenants.drawerTitleCreate')} {t('tenants.drawerTitleCreate')}
</Button> </Button>
)} )}
</div> />
<Card className="shadow-sm mb-4"> <Card className="shadow-sm mb-4">
<Space wrap size="middle"> <Space wrap size="middle">
@ -311,7 +318,7 @@ export default function Tenants() {
</Form.Item> </Form.Item>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ label: "正常启用", value: 1 }, { label: "禁止访问", value: 0 }]} /> <Select options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))} />
</Form.Item> </Form.Item>
<Form.Item label={t('common.remark')} name="remark"> <Form.Item label={t('common.remark')} name="remark">

View File

@ -17,6 +17,7 @@ import { useTranslation } from "react-i18next";
import { listRoles, listUserRoles, listUsers, saveUserRoles } from "../api"; import { listRoles, listUserRoles, listUsers, saveUserRoles } from "../api";
import { SearchOutlined, UserOutlined, SaveOutlined, TeamOutlined } from "@ant-design/icons"; import { SearchOutlined, UserOutlined, SaveOutlined, TeamOutlined } from "@ant-design/icons";
import type { SysRole, SysUser } from "../types"; import type { SysRole, SysUser } from "../types";
import PageHeader from "../components/shared/PageHeader";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -107,11 +108,10 @@ export default function UserRoleBinding() {
return ( return (
<div className="p-6"> <div className="p-6">
<div className="mb-6 flex justify-between items-end"> <PageHeader
<div> title={t('userRole.title')}
<Title level={4} className="mb-1">{t('userRole.title')}</Title> subtitle={t('userRole.subtitle')}
<Text type="secondary">{t('userRole.subtitle')}</Text> extra={(
</div>
<Button <Button
type="primary" type="primary"
icon={<SaveOutlined aria-hidden="true" />} icon={<SaveOutlined aria-hidden="true" />}
@ -121,7 +121,8 @@ export default function UserRoleBinding() {
> >
{saving ? t('common.loading') : t('common.save')} {saving ? t('common.loading') : t('common.save')}
</Button> </Button>
</div> )}
/>
<Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}> <Row gutter={24} style={{ height: 'calc(100vh - 180px)' }}>
<Col xs={24} lg={12} style={{ height: '100%' }}> <Col xs={24} lg={12} style={{ height: '100%' }}>

View File

@ -30,6 +30,7 @@ import {
listOrgs listOrgs
} from "../api"; } from "../api";
import { usePermission } from "../hooks/usePermission"; import { usePermission } from "../hooks/usePermission";
import { useDict } from "../hooks/useDict";
import { import {
PlusOutlined, PlusOutlined,
EditOutlined, EditOutlined,
@ -41,8 +42,7 @@ import {
ReloadOutlined, ReloadOutlined,
MinusCircleOutlined MinusCircleOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import type { SysRole, SysUser, SysTenant, SysOrg } from "../types"; import PageHeader from "../components/shared/PageHeader";
import "./Users.css";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -109,6 +109,9 @@ export default function Users() {
const [tenants, setTenants] = useState<SysTenant[]>([]); const [tenants, setTenants] = useState<SysTenant[]>([]);
const [orgs, setOrgs] = useState<SysOrg[]>([]); const [orgs, setOrgs] = useState<SysOrg[]>([]);
// Dictionaries
const { items: statusDict } = useDict("sys_common_status");
// Platform admin check // Platform admin check
const isPlatformMode = useMemo(() => { const isPlatformMode = useMemo(() => {
const profileStr = sessionStorage.getItem("userProfile"); const profileStr = sessionStorage.getItem("userProfile");
@ -373,11 +376,14 @@ export default function Users() {
title: t('common.status'), title: t('common.status'),
dataIndex: "status", dataIndex: "status",
width: 80, width: 80,
render: (status: number) => ( render: (status: number) => {
const item = statusDict.find(i => i.itemValue === String(status));
return (
<Tag color={status === 1 ? "green" : "red"} className="m-0"> <Tag color={status === 1 ? "green" : "red"} className="m-0">
{status === 1 ? "正常" : "禁用"} {item ? item.itemLabel : (status === 1 ? "正常" : "禁用")}
</Tag> </Tag>
), );
},
}, },
{ {
title: t('common.action'), title: t('common.action'),
@ -406,17 +412,15 @@ export default function Users() {
return ( return (
<div className="users-page p-6"> <div className="users-page p-6">
<div className="users-header flex justify-between items-end mb-6"> <PageHeader
<div> title={t('users.title')}
<Title level={4} className="mb-1">{t('users.title')}</Title> subtitle={t('users.subtitle')}
<Text type="secondary">{t('users.subtitle')}</Text> extra={can("sys:user:create") && (
</div>
{can("sys:user:create") && (
<Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}> <Button type="primary" icon={<PlusOutlined aria-hidden="true" />} onClick={openCreate}>
{t('users.drawerTitleCreate')} {t('users.drawerTitleCreate')}
</Button> </Button>
)} )}
</div> />
<Card className="users-table-card shadow-sm"> <Card className="users-table-card shadow-sm">
<div className="users-table-toolbar mb-4"> <div className="users-table-toolbar mb-4">
@ -536,7 +540,7 @@ export default function Users() {
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Form.Item label={t('common.status')} name="status" initialValue={1}> <Form.Item label={t('common.status')} name="status" initialValue={1}>
<Select options={[{ label: "启用", value: 1 }, { label: "禁用", value: 0 }]} /> <Select options={statusDict.map(i => ({ label: i.itemLabel, value: Number(i.itemValue) }))} />
</Form.Item> </Form.Item>
</Col> </Col>
{isPlatformMode && ( {isPlatformMode && (