From 2b30744d2ed0fa8ef965de6448621cfd1366082f Mon Sep 17 00:00:00 2001 From: alanpaine Date: Wed, 15 Apr 2026 17:47:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E7=BB=9F=E4=B8=80=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E5=88=86=E9=A1=B5=E7=BB=84=E4=BB=B6=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=B8=83=E5=B1=80=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AppPagination 组件,统一分页样式与行为 - 将多个页面的自定义分页替换为标准分页组件 - 修复布局容器高度计算问题,确保内容区域正确滚动 - 调整表格容器样式,支持响应式布局和水平滚动 - 优化国际化配置,支持 Ant Design 组件多语言 - 统一分页边距和边框样式,提升视觉一致性 --- frontend/src/App.tsx | 11 +- .../components/shared/AppPagination/index.css | 17 ++ .../components/shared/AppPagination/index.tsx | 27 +++ .../components/shared/ListTable/ListTable.tsx | 1 + frontend/src/index.css | 78 ++++++++- frontend/src/layouts/AppLayout.tsx | 5 +- frontend/src/locales/zh-CN.json | 2 +- frontend/src/pages/access/roles/index.less | 7 +- frontend/src/pages/access/roles/index.tsx | 13 +- frontend/src/pages/access/users/index.tsx | 7 +- .../pages/bindings/role-permission/index.tsx | 10 +- .../src/pages/bindings/user-role/index.tsx | 13 +- frontend/src/pages/business/AiModels.tsx | 42 +++-- .../src/pages/business/ClientManagement.tsx | 12 +- .../pages/business/ExternalAppManagement.tsx | 8 +- frontend/src/pages/business/HotWords.tsx | 32 ++-- frontend/src/pages/business/Meetings.tsx | 9 +- .../src/pages/business/PromptTemplates.tsx | 59 +++---- frontend/src/pages/business/SpeakerReg.tsx | 10 +- frontend/src/pages/devices/index.tsx | 156 +++++++++--------- .../src/pages/organization/tenants/index.tsx | 39 +++-- .../src/pages/system/dictionaries/index.tsx | 9 +- frontend/src/pages/system/logs/index.tsx | 14 +- .../src/pages/system/sys-params/index.tsx | 25 +-- frontend/src/utils/pagination.ts | 13 +- 25 files changed, 387 insertions(+), 232 deletions(-) create mode 100644 frontend/src/components/shared/AppPagination/index.css create mode 100644 frontend/src/components/shared/AppPagination/index.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 32ac777..e45e729 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,13 +1,19 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { ConfigProvider, theme, App as AntdApp } from "antd"; +import zhCN from "antd/locale/zh_CN"; +import enUS from "antd/locale/en_US"; +import { useTranslation } from "react-i18next"; import AppRoutes from "./routes"; import { getOpenPlatformConfig } from "./api"; -import {useThemeStore} from "./store/themeStore"; +import { useThemeStore } from "./store/themeStore"; import type { SysPlatformConfig } from "./types"; export default function App() { const [config, setConfig] = useState(null); const { colorPrimary, themeMode, initTheme } = useThemeStore(); + const { i18n } = useTranslation(); + const antdLocale = useMemo(() => (i18n.language === "en-US" ? enUS : zhCN), [i18n.language]); + useEffect(() => { initTheme(); const fetchConfig = async () => { @@ -37,6 +43,7 @@ export default function App() { return ( + t('common.total', { total })} + pageSizeOptions={['10', '20', '50', '100']} + size="default" + {...props} + /> + + ); +} diff --git a/frontend/src/components/shared/ListTable/ListTable.tsx b/frontend/src/components/shared/ListTable/ListTable.tsx index fa57b3b..94b2cd7 100644 --- a/frontend/src/components/shared/ListTable/ListTable.tsx +++ b/frontend/src/components/shared/ListTable/ListTable.tsx @@ -62,6 +62,7 @@ function ListTable>({ ? false : { ...pagination, + className: ["app-global-pagination", pagination.className].filter(Boolean).join(" "), showTotal: (total: number) => (
{isAllPagesSelected ? ( diff --git a/frontend/src/index.css b/frontend/src/index.css index 754080c..74bfbb9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -182,10 +182,11 @@ body::after { .app-page { height: 100%; + min-height: 0; padding: 24px; display: flex; flex-direction: column; - overflow: auto; + overflow: hidden; } .app-page--contained { @@ -210,12 +211,37 @@ body::after { } .app-page__content-card { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.app-page__panel-card { + display: flex; + flex-direction: column; min-height: 0; } .app-page__content-card .ant-card-body, .app-page__panel-card .ant-card-body { + flex: 1; min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.app-page__table-wrap { + flex: 1; + min-height: 0; + min-width: 0; + overflow: hidden; +} + +.app-page__table-wrap .ant-table-wrapper { + min-width: 0; } .app-page__toolbar { @@ -796,3 +822,53 @@ body::after { } + +/* Global Pagination Style */ +.ant-table-wrapper { + height: 100%; +} +.ant-table-wrapper .ant-spin-nested-loading { + height: 100%; +} +.ant-table-wrapper .ant-spin-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.ant-table-wrapper .ant-table { + flex: 1; + min-height: 0; +} + +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination, +.app-global-pagination.ant-pagination { + margin: auto 0 0 0 !important; + flex-shrink: 0; + flex: 1 1 100%; + box-sizing: border-box; + padding: 12px 24px; + border-top: 1px solid var(--app-border-color); + background: var(--app-bg-card); + border-radius: 0 0 16px 16px; + display: flex; + align-items: center; + width: 100%; +} + +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-total-text, +.app-global-pagination.ant-pagination .ant-pagination-total-text { + margin-right: auto; + color: var(--app-text-muted); + white-space: nowrap; +} + +.app-global-pagination.ant-pagination .ant-pagination-options, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options { + margin-inline-start: 12px; +} + +.app-global-pagination.ant-pagination .ant-pagination-options-quick-jumper, +.ant-table-wrapper .ant-table-pagination.ant-pagination.app-global-pagination .ant-pagination-options-quick-jumper { + margin-inline-start: 12px; +} diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index 26cebfb..01b8cf6 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -396,7 +396,7 @@ export default function AppLayout() { )} - +
-
@@ -589,7 +588,7 @@ export default function Roles() { size="small" loading={loadingUsers} dataSource={roleUsers} - pagination={{ pageSize: 10, size: "small" }} + pagination={getStandardPagination(roleUsers.length, 1, 10)} columns={[ { title: "用户信息", @@ -634,7 +633,7 @@ export default function Roles() {
} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
- setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} /> +
setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} /> setDrawerOpen(false)} width={420} destroyOnHidden forceRender footer={
}> diff --git a/frontend/src/pages/access/users/index.tsx b/frontend/src/pages/access/users/index.tsx index 216f420..4bb2c0c 100644 --- a/frontend/src/pages/access/users/index.tsx +++ b/frontend/src/pages/access/users/index.tsx @@ -4,10 +4,10 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ApartmentOutlined, DeleteOutlined, EditOutlined, MinusCircleOutlined, PlusOutlined, SearchOutlined, ShopOutlined, UploadOutlined, UserOutlined } from "@ant-design/icons"; import { createUser, deleteUser, getUserDetail, listOrgs, listRoles, listTenants, listUserRoles, listUsers, saveUserRoles, updateUser, uploadPlatformAsset } from "@/api"; +import AppPagination from "@/components/shared/AppPagination"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; -import { getStandardPagination } from "@/utils/pagination"; import type { SysOrg, SysRole, SysTenant, SysUser } from "@/types"; import "./index.less"; @@ -399,9 +399,10 @@ export default function Users() { -
-
{ setCurrent(page); setPageSize(size); })} /> +
+
+ { setCurrent(page); setPageSize(size); }} /> -
setSelectedRoleId(record.roleId), className: "cursor-pointer" })} - pagination={{ pageSize: 10, showTotal: (total) => t("common.total", { total }) }} + pagination={getStandardPagination(filteredRoles.length, 1, 10)} columns={[ { title: t("roles.roleName"), @@ -235,6 +237,7 @@ export default function RolePermissionBinding() { -
setSelectedUserId(record.userId), className: "cursor-pointer" })} - pagination={{ pageSize: 10, showTotal: (total) => t("common.total", { total }) }} + pagination={getStandardPagination(filteredUsers.length, 1, 10)} columns={[ { title: t("users.userInfo"), @@ -155,10 +157,11 @@ export default function UserRoleBinding() {
{ - setCurrent(page); - setSize(pageSize); - }, + /> + +
+
+ + { + setCurrent(page); + setSize(pageSize); }} /> diff --git a/frontend/src/pages/business/ClientManagement.tsx b/frontend/src/pages/business/ClientManagement.tsx index 0715d03..38fe0d0 100644 --- a/frontend/src/pages/business/ClientManagement.tsx +++ b/frontend/src/pages/business/ClientManagement.tsx @@ -410,7 +410,17 @@ export default function ClientManagement() { -
} : undefined} scroll={{ x: 960, y: "calc(100vh - 360px)" }} pagination={getStandardPagination(filteredRecords.length, page, pageSize, (nextPage, nextSize) => { setPage(nextPage); setPageSize(nextSize); })} /> +
+
} : undefined} + scroll={{ x: "max-content", y: "calc(100vh - 360px)" }} + pagination={getStandardPagination(filteredRecords.length, page, pageSize, (nextPage: number, nextSize: number) => { setPage(nextPage); setPageSize(nextSize); })} + /> + setDrawerOpen(false)} width={680} destroyOnHidden forceRender footer={
}> diff --git a/frontend/src/pages/business/ExternalAppManagement.tsx b/frontend/src/pages/business/ExternalAppManagement.tsx index 37ad703..e52d56e 100644 --- a/frontend/src/pages/business/ExternalAppManagement.tsx +++ b/frontend/src/pages/business/ExternalAppManagement.tsx @@ -1,4 +1,4 @@ -import { App, Avatar, Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tag, Typography, Upload } from "antd"; +import { App, Avatar, Button, Card, Col, Drawer, Form, Input, InputNumber, Popconfirm, Row, Select, Space, Switch, Table, Tag, Typography, Upload } from "antd"; import type { ColumnsType } from "antd/es/table"; import { AppstoreOutlined, DeleteOutlined, EditOutlined, GlobalOutlined, LinkOutlined, PictureOutlined, PlusOutlined, ReloadOutlined, RobotOutlined, SaveOutlined, SearchOutlined, UploadOutlined } from "@ant-design/icons"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -31,7 +31,7 @@ const STATUS_OPTIONS = [ { label: "已停用", value: "disabled" }, ] as const; -function normalizeAppInfo(value: ExternalAppVO["appInfo"]): ExternalAppFormValues["appInfo"] { +function normalizeAppInfo(value: ExternalAppVO["appInfo"]): NonNullable { if (!value || typeof value !== "object") { return {}; } @@ -320,7 +320,9 @@ export default function ExternalAppManagement() { -
{ setPage(nextPage); setPageSize(nextSize); })} /> +
+
{ setPage(nextPage); setPageSize(nextSize); })} /> + setDrawerOpen(false)} width={700} destroyOnHidden forceRender footer={
}> diff --git a/frontend/src/pages/business/HotWords.tsx b/frontend/src/pages/business/HotWords.tsx index 2ed5883..1991a19 100644 --- a/frontend/src/pages/business/HotWords.tsx +++ b/frontend/src/pages/business/HotWords.tsx @@ -18,6 +18,7 @@ import { updateHotWord, type HotWordVO, } from "../../api/business/hotword"; +import AppPagination from "../../components/shared/AppPagination"; const { Option } = Select; const { Text } = Typography; @@ -246,10 +247,11 @@ const HotWords: React.FC = () => { ]; return ( -
+
@@ -296,25 +298,25 @@ const HotWords: React.FC = () => { } > -
+
`共 ${value} 条`, - onChange: (page, pageSize) => { - setCurrent(page); - setSize(pageSize); - }, - }} + scroll={{ x: "max-content" }} + pagination={false} /> + { + setCurrent(page); + setSize(pageSize); + }} + /> { const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [current, setCurrent] = useState(1); - const [size, setSize] = useState(8); + const [size, setSize] = useState(10); const [searchTitle, setSearchTitle] = useState(''); const [viewType, setViewType] = useState<'all' | 'created' | 'involved'>('all'); const [createDrawerVisible, setCreateDrawerVisible] = useState(false); @@ -364,11 +365,7 @@ const Meetings: React.FC = () => { - {total > 0 && ( -
- { setCurrent(p); setSize(s); }} showTotal={(total) => 为您找到 {total} 场会议} size="small" /> -
- )} + {total > 0 && { setCurrent(p); setSize(s); }} />} { }; return ( -
-
+
+
提示词模板
{ )}
-
- + { setCurrent(page); setPageSize(size); }} - showTotal={count => `共 ${count} 条`} />
diff --git a/frontend/src/pages/devices/index.tsx b/frontend/src/pages/devices/index.tsx index eff2d80..734a125 100644 --- a/frontend/src/pages/devices/index.tsx +++ b/frontend/src/pages/devices/index.tsx @@ -155,86 +155,88 @@ export default function Devices() { +
- rowKey="deviceId" - dataSource={filteredData} - loading={loading} - size="middle" - scroll={{ y: "calc(100vh - 350px)" }} - pagination={getStandardPagination(filteredData.length, 1, 1000)} - columns={[ - { - title: t("devicesExt.device"), - key: "device", - render: (_value: unknown, record) => ( - -
-
-
-
{record.deviceName || t("devicesExt.unnamedDevice")}
-
{record.deviceCode}
-
-
- ) - }, - { - title: t("devices.owner"), - key: "user", - render: (_value: unknown, record) => { - const owner = userMap[record.userId]; - return owner ? ( + rowKey="deviceId" + dataSource={filteredData} + loading={loading} + size="middle" + scroll={{ x: "max-content", y: "calc(100vh - 350px)" }} + pagination={getStandardPagination(filteredData.length, 1, 1000)} + columns={[ + { + title: t("devicesExt.device"), + key: "device", + render: (_value: unknown, record) => ( - - ) : ( - {t("devicesExt.ownerId")}: {record.userId} - ); + ) + }, + { + title: t("devices.owner"), + key: "user", + render: (_value: unknown, record) => { + const owner = userMap[record.userId]; + return owner ? ( + + + ) : ( + {t("devicesExt.ownerId")}: {record.userId} + ); + } + }, + { + title: t("common.status"), + dataIndex: "status", + width: 100, + render: (status: number) => { + const item = statusDict.find((dictItem) => dictItem.itemValue === String(status)); + return {item?.itemLabel || (status === 1 ? t("devicesExt.enabled") : t("devicesExt.disabled"))}; + } + }, + { + title: t("devices.updateTime"), + dataIndex: "updatedAt", + width: 180, + render: (text: string) => ( + + {text?.replace("T", " ").substring(0, 19)} + + ) + }, + { + title: t("common.action"), + key: "action", + width: 120, + fixed: "right", + render: (_value: unknown, record) => ( + + {can("device:update") ? ( +
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/organization/tenants/index.tsx b/frontend/src/pages/organization/tenants/index.tsx index b2b6126..27b4a9c 100644 --- a/frontend/src/pages/organization/tenants/index.tsx +++ b/frontend/src/pages/organization/tenants/index.tsx @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"; import { DeleteOutlined, EditOutlined, PhoneOutlined, PlusOutlined, ReloadOutlined, SearchOutlined, ShopOutlined, UserOutlined } from "@ant-design/icons"; import dayjs from "dayjs"; import { createTenant, deleteTenant, listTenants, updateTenant } from "@/api"; +import AppPagination from "@/components/shared/AppPagination"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; @@ -146,26 +147,24 @@ export default function Tenants() { -
- setParams({ ...params, current: page, size: size || params.size }), - showSizeChanger: true, - showQuickJumper: true, - showTotal: (count) => t("common.total", { total: count }), - pageSizeOptions: ["10", "20", "50", "100"], - style: { marginTop: "24px", marginBottom: "24px" } - }} - locale={{ emptyText: }} + +
+ }} + /> +
+ setParams({ ...params, current: page, size: size || params.size })} /> -
+
); -} \ No newline at end of file +} diff --git a/frontend/src/pages/system/dictionaries/index.tsx b/frontend/src/pages/system/dictionaries/index.tsx index 59e984d..0450d30 100644 --- a/frontend/src/pages/system/dictionaries/index.tsx +++ b/frontend/src/pages/system/dictionaries/index.tsx @@ -174,12 +174,7 @@ export default function Dictionaries() { rowKey="dictTypeId" loading={loadingTypes} dataSource={types} - pagination={{ - ...getStandardPagination(typeTotal, typeParams.current, typeParams.size, (page, size) => setTypeParams({ ...typeParams, current: page, size })), - simple: true, - size: "small", - position: ["bottomCenter"] - }} + pagination={getStandardPagination(typeTotal, typeParams.current, typeParams.size, (page, size) => setTypeParams({ ...typeParams, current: page, size }))} size="small" showHeader={false} scroll={{ y: "calc(100vh - 480px)" }} @@ -276,4 +271,4 @@ export default function Dictionaries() { ); -} \ No newline at end of file +} diff --git a/frontend/src/pages/system/logs/index.tsx b/frontend/src/pages/system/logs/index.tsx index 38977a6..6abdf53 100644 --- a/frontend/src/pages/system/logs/index.tsx +++ b/frontend/src/pages/system/logs/index.tsx @@ -3,10 +3,10 @@ import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { EyeOutlined, InfoCircleOutlined, ReloadOutlined, SearchOutlined, UserOutlined } from "@ant-design/icons"; import { fetchLogModules, fetchLogs } from "@/api"; +import AppPagination from "@/components/shared/AppPagination"; import { useDict } from "@/hooks/useDict"; import PageHeader from "@/components/shared/PageHeader"; import ListTable from "@/components/shared/ListTable/ListTable"; -import { getStandardPagination } from "@/utils/pagination"; import type { SysLog, UserProfile } from "@/types"; const { RangePicker } = DatePicker; @@ -71,11 +71,10 @@ export default function Logs() { fetchLogModules().then((items) => setModuleOptions(items || [])).catch(() => setModuleOptions([])); }, [activeTab]); - const handleTableChange = (pagination: any, _filters: any, sorter: any) => { + const handleTableChange = (_pagination: any, _filters: any, sorter: any) => { setParams({ ...params, - current: pagination.current, - size: pagination.pageSize, + current: 1, sortField: sorter.field || "createdAt", sortOrder: sorter.order || "descend" }); @@ -275,7 +274,7 @@ export default function Logs() { } /> -
+
+ setParams((prev) => ({ ...prev, current: page, size }))} /> setDetailModalVisible(false)} footer={[]} width={700}> diff --git a/frontend/src/pages/system/sys-params/index.tsx b/frontend/src/pages/system/sys-params/index.tsx index 4c8b32e..22950fc 100644 --- a/frontend/src/pages/system/sys-params/index.tsx +++ b/frontend/src/pages/system/sys-params/index.tsx @@ -3,10 +3,10 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { DeleteOutlined, EditOutlined, InfoCircleOutlined, PlusOutlined, SearchOutlined, SettingOutlined } from "@ant-design/icons"; import { createParam, deleteParam, pageParams, updateParam } from "@/api"; +import AppPagination from "@/components/shared/AppPagination"; import { useDict } from "@/hooks/useDict"; import { usePermission } from "@/hooks/usePermission"; import PageHeader from "@/components/shared/PageHeader"; -import { getStandardPagination } from "@/utils/pagination"; import type { SysParamQuery, SysParamVO } from "@/types"; import "./index.less"; @@ -173,15 +173,18 @@ export default function SysParams() { -
+
+
+ + ); -} \ No newline at end of file +} diff --git a/frontend/src/utils/pagination.ts b/frontend/src/utils/pagination.ts index 882dac8..963278c 100644 --- a/frontend/src/utils/pagination.ts +++ b/frontend/src/utils/pagination.ts @@ -5,11 +5,14 @@ import i18n from '../i18n'; * Returns a standardized Ant Design pagination configuration. */ export const getStandardPagination = ( - total: number, - current: number, + total: number, + current: number, pageSize: number, - onChange?: (page: number, size: number) => void + onChange?: (page: number, size: number) => void, + overrides: TablePaginationConfig = {} ): TablePaginationConfig => { + const mergedClassName = ['app-global-pagination', overrides.className].filter(Boolean).join(' '); + return { total, current, @@ -20,6 +23,8 @@ export const getStandardPagination = ( showTotal: (totalCount) => i18n.t('common.total', { total: totalCount }), pageSizeOptions: ['10', '20', '50', '100'], size: 'default', - position: ['bottomRight'] + position: ['bottomRight'], + ...overrides, + className: mergedClassName }; };