From 9d0edbfa0047b25761b205417fde641981d2934b Mon Sep 17 00:00:00 2001 From: puz <13060209078@163.com> Date: Mon, 6 Jul 2026 11:08:36 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=93=8D=E5=BA=94=E5=BC=8F?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../business/MeetingCreateDrawer.tsx | 2 +- .../shared/DataListPanel/DataListPanel.css | 27 +- .../components/shared/ListTable/ListTable.css | 4 + .../components/shared/ListTable/ListTable.tsx | 16 +- .../shared/SectionCard/SectionCard.css | 18 ++ frontend/src/index.css | 42 ++++ frontend/src/layouts/AppLayout.css | 105 ++++++++ frontend/src/layouts/AppLayout.tsx | 54 +++- .../src/pages/access/permissions/index.tsx | 22 +- frontend/src/pages/access/roles/index.less | 74 ++++++ frontend/src/pages/access/roles/index.tsx | 233 ++++++++++-------- frontend/src/pages/access/users/index.tsx | 28 +-- .../pages/bindings/role-permission/index.less | 42 +++- .../pages/bindings/role-permission/index.tsx | 4 +- .../src/pages/bindings/user-role/index.less | 42 +++- .../src/pages/bindings/user-role/index.tsx | 6 +- frontend/src/pages/business/AiModels.tsx | 36 +-- .../src/pages/business/ClientManagement.css | 13 + .../src/pages/business/ClientManagement.tsx | 26 +- .../pages/business/ExternalAppManagement.css | 13 + .../pages/business/ExternalAppManagement.tsx | 30 +-- frontend/src/pages/business/HotWords.css | 26 +- .../src/pages/business/LicenseManagement.css | 13 + .../src/pages/business/LicenseManagement.tsx | 2 +- frontend/src/pages/business/MeetingDetail.tsx | 16 +- .../business/MeetingPointsManagement.tsx | 6 +- frontend/src/pages/business/Meetings.css | 7 + frontend/src/pages/business/Meetings.tsx | 2 +- .../src/pages/business/PromptTemplates.tsx | 8 +- .../business/PublicDeviceMeetingCreate.tsx | 2 +- .../pages/business/ScreenSaverManagement.css | 48 ++++ .../pages/business/ScreenSaverManagement.tsx | 6 +- frontend/src/pages/business/SpeakerReg.tsx | 4 +- .../business/TenantMeetingPointsSettings.css | 38 +++ .../business/TenantMeetingPointsSettings.tsx | 12 +- frontend/src/pages/devices/index.tsx | 2 +- frontend/src/pages/home/index.less | 111 ++++++++- .../src/pages/organization/orgs/index.tsx | 8 +- .../src/pages/organization/tenants/index.tsx | 14 +- .../src/pages/profile/AvatarCropDialog.css | 57 +++++ .../src/pages/system/dictionaries/index.less | 48 ++++ .../src/pages/system/dictionaries/index.tsx | 12 +- .../src/pages/system/sys-params/index.tsx | 12 +- 43 files changed, 1038 insertions(+), 253 deletions(-) diff --git a/frontend/src/components/business/MeetingCreateDrawer.tsx b/frontend/src/components/business/MeetingCreateDrawer.tsx index 21c828b..79e2f72 100644 --- a/frontend/src/components/business/MeetingCreateDrawer.tsx +++ b/frontend/src/components/business/MeetingCreateDrawer.tsx @@ -399,7 +399,7 @@ export const MeetingCreateDrawer: React.FC = ({ onClose={onCancel} width="min(960px, calc(100vw - 48px))" forceRender - destroyOnClose={false} + destroyOnHidden={false} placement="right" closable={false} footer={ diff --git a/frontend/src/components/shared/DataListPanel/DataListPanel.css b/frontend/src/components/shared/DataListPanel/DataListPanel.css index 8d675d1..3cdc603 100644 --- a/frontend/src/components/shared/DataListPanel/DataListPanel.css +++ b/frontend/src/components/shared/DataListPanel/DataListPanel.css @@ -121,6 +121,7 @@ min-width: 0; min-height: 32px; align-items: center; + justify-content: flex-end; } .data-list-panel__table-container { @@ -240,6 +241,22 @@ border-bottom-color: rgba(22, 119, 255, 0.18) !important; } +@media (max-width: 1200px) { + .data-list-panel__toolbar { + align-items: flex-start; + flex-wrap: wrap; + } + + .data-list-panel__right-actions { + flex: 1 1 420px; + } + + .data-list-panel__right-actions .ant-space { + justify-content: flex-end; + row-gap: 8px; + } +} + @media (max-width: 768px) { .data-list-panel { padding: 12px; @@ -258,7 +275,15 @@ .data-list-panel__right-actions, .data-list-panel__right-actions .ant-space, .data-list-panel__right-actions .ant-input-affix-wrapper, - .data-list-panel__right-actions .ant-select { + .data-list-panel__right-actions .ant-input, + .data-list-panel__right-actions .ant-select, + .data-list-panel__right-actions .ant-picker, + .data-list-panel__right-actions .ant-input-number, + .data-list-panel__right-actions .ant-btn { width: 100% !important; } + + .data-list-panel__right-actions .ant-space { + justify-content: flex-start; + } } diff --git a/frontend/src/components/shared/ListTable/ListTable.css b/frontend/src/components/shared/ListTable/ListTable.css index 95c8d2c..719e000 100644 --- a/frontend/src/components/shared/ListTable/ListTable.css +++ b/frontend/src/components/shared/ListTable/ListTable.css @@ -42,7 +42,11 @@ } .selection-action { + padding: 0; + border: 0; + background: transparent; color: var(--link-color); + font-family: inherit; font-size: 14px; cursor: pointer; text-decoration: none; diff --git a/frontend/src/components/shared/ListTable/ListTable.tsx b/frontend/src/components/shared/ListTable/ListTable.tsx index e8d6997..75524ee 100644 --- a/frontend/src/components/shared/ListTable/ListTable.tsx +++ b/frontend/src/components/shared/ListTable/ListTable.tsx @@ -39,7 +39,7 @@ function ListTable>({ showSizeChanger: { showSearch: false }, showQuickJumper: true, }, - scroll = { x: 1200 }, + scroll = { x: "max-content" }, onRowClick, selectedRow, loading = false, @@ -49,7 +49,7 @@ function ListTable>({ }: ListTableProps) { const mergedScroll = React.useMemo(() => { return { - x: 1200, + x: "max-content", ...scroll, }; }, [scroll]); @@ -95,9 +95,9 @@ function ListTable>({ 已选择 {totalCount || total} 项 {onClearSelection && ( - + )} ) : selectedRowKeys.length > 0 ? ( @@ -106,14 +106,14 @@ function ListTable>({ 已选择 {selectedRowKeys.length} 项 {onSelectAllPages && selectedRowKeys.length < (totalCount || total) && ( - + )} {onClearSelection && ( - + )} ) : ( diff --git a/frontend/src/components/shared/SectionCard/SectionCard.css b/frontend/src/components/shared/SectionCard/SectionCard.css index d7f936f..0bf01a8 100644 --- a/frontend/src/components/shared/SectionCard/SectionCard.css +++ b/frontend/src/components/shared/SectionCard/SectionCard.css @@ -201,6 +201,24 @@ flex-direction: column; } + .section-card__title-wrap { + width: 100%; + max-width: 100%; + } + + .section-card__title { + align-items: flex-start; + white-space: normal; + } + + .section-card__title::before { + margin-top: 6px; + } + + .section-card__description { + white-space: normal; + } + .section-card__extra { width: 100%; justify-content: flex-start; diff --git a/frontend/src/index.css b/frontend/src/index.css index 1755f96..876aa01 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -33,6 +33,14 @@ --meeting-progress-color: #1677ff; --app-form-drawer-width: 600px; --app-form-drawer-max-width: calc(100vw - 48px); + --app-breakpoint-sm: 576px; + --app-breakpoint-md: 768px; + --app-breakpoint-lg: 992px; + --app-breakpoint-xl: 1200px; + --app-page-padding: 16px; + --app-page-padding-mobile: 12px; + --app-control-min-width: 160px; + --app-control-wide-width: 220px; } :root[data-theme="minimal"] { @@ -365,6 +373,15 @@ body::after { padding: 8px 4px 4px; } +.app-responsive-form-row { + width: 100%; +} + +.app-responsive-form-row .ant-col, +.app-responsive-form-field { + min-width: 0; +} + .ant-drawer:has(.app-page__drawer-footer) .ant-drawer-content-wrapper, .ant-drawer:has(.screen-saver-drawer__footer) .ant-drawer-content-wrapper, .meeting-create-drawer-root .ant-drawer-content-wrapper { @@ -928,6 +945,15 @@ body::after { .app-page__content-toolbar-filters { justify-content: flex-start; } + + .app-page__content-toolbar .ant-space, + .app-page__content-toolbar .ant-input, + .app-page__content-toolbar .ant-input-affix-wrapper, + .app-page__content-toolbar .ant-select, + .app-page__content-toolbar .ant-picker, + .app-page__content-toolbar .ant-input-number { + width: 100% !important; + } } @@ -1351,6 +1377,14 @@ body::after { --meeting-progress-color: #1677ff; --app-form-drawer-width: 600px; --app-form-drawer-max-width: calc(100vw - 48px); + --app-breakpoint-sm: 576px; + --app-breakpoint-md: 768px; + --app-breakpoint-lg: 992px; + --app-breakpoint-xl: 1200px; + --app-page-padding: 16px; + --app-page-padding-mobile: 12px; + --app-control-min-width: 160px; + --app-control-wide-width: 220px; } :root[data-theme="tech"] { @@ -1385,6 +1419,14 @@ body::after { --meeting-progress-color: #1677ff; --app-form-drawer-width: 600px; --app-form-drawer-max-width: calc(100vw - 48px); + --app-breakpoint-sm: 576px; + --app-breakpoint-md: 768px; + --app-breakpoint-lg: 992px; + --app-breakpoint-xl: 1200px; + --app-page-padding: 16px; + --app-page-padding-mobile: 12px; + --app-control-min-width: 160px; + --app-control-wide-width: 220px; } html, diff --git a/frontend/src/layouts/AppLayout.css b/frontend/src/layouts/AppLayout.css index 9789502..a94e17a 100644 --- a/frontend/src/layouts/AppLayout.css +++ b/frontend/src/layouts/AppLayout.css @@ -54,6 +54,11 @@ gap: 16px; } +.mobile-menu-trigger { + display: none; + flex: 0 0 auto; +} + .main-sider { position: relative; height: calc(100vh - 64px); @@ -270,6 +275,106 @@ background: var(--app-bg-layout); } +.main-sider-backdrop { + display: none; +} + +@media (max-width: 768px) { + .main-header { + gap: 8px; + padding: 0 12px; + } + + .mobile-menu-trigger { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--app-text-main); + } + + .app-header-logo { + flex: 1 1 auto; + gap: 8px; + } + + .app-header-logo img { + height: 32px; + max-width: 120px; + } + + .app-header-logo-title { + max-width: 120px; + font-size: 16px; + } + + .header-right { + flex: 0 0 auto; + min-width: 0; + } + + .header-right > .ant-space { + gap: 12px !important; + } + + .header-right .ant-avatar + span { + display: none; + } + + .main-shell { + position: relative; + } + + .main-sider { + position: fixed !important; + top: 64px; + bottom: 0; + left: 0; + z-index: 30; + width: 180px !important; + min-width: 180px !important; + max-width: 180px !important; + height: calc(100vh - 64px); + border-top: none; + box-shadow: 8px 0 24px rgba(15, 23, 42, 0.16); + transform: translateX(-100%); + transition: transform 0.24s ease; + } + + .main-layout--mobile-sider-open .main-sider { + transform: translateX(0); + } + + .main-layout--mobile .main-sider--collapsed { + width: 0 !important; + min-width: 0 !important; + max-width: 0 !important; + flex: 0 0 0 !important; + } + + .main-layout--mobile .main-sider .collapsedCenter { + display: none; + } + + .main-sider .ant-layout-sider-children { + height: calc(100vh - 64px); + } + + .main-layout--mobile-sider-open .main-sider-backdrop { + position: fixed; + inset: 64px 0 0; + z-index: 25; + display: block; + padding: 0; + border: 0; + background: rgba(15, 23, 42, 0.28); + } + + .main-content-layout { + flex: 1 1 auto; + width: 100%; + } +} + :root[data-theme="tech"] .main-header, :root[data-theme="tech"] .main-sider, :root[data-theme="tech"] .main-menu, diff --git a/frontend/src/layouts/AppLayout.tsx b/frontend/src/layouts/AppLayout.tsx index 70b9754..5b224cc 100644 --- a/frontend/src/layouts/AppLayout.tsx +++ b/frontend/src/layouts/AppLayout.tsx @@ -99,6 +99,7 @@ function findActiveMenu(nodes: PermissionMenuNode[], path: string, parentKeys: s export default function AppLayout() { const { t, i18n } = useTranslation(); const [collapsed, setCollapsed] = useState(false); + const [isMobileLayout, setIsMobileLayout] = useState(false); const [menus, setMenus] = useState([]); const [availableTenants, setAvailableTenants] = useState([]); const [currentTenantId, setCurrentTenantId] = useState(null); @@ -320,6 +321,30 @@ export default function AppLayout() { setOpenKeys((prev) => Array.from(new Set([...prev, ...activeMenu.parentKeys]))); }, [activeMenu]); + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const mediaQuery = window.matchMedia("(max-width: 768px)"); + const syncMobileLayout = () => { + setIsMobileLayout(mediaQuery.matches); + if (mediaQuery.matches) { + setCollapsed(true); + } + }; + + syncMobileLayout(); + mediaQuery.addEventListener("change", syncMobileLayout); + return () => mediaQuery.removeEventListener("change", syncMobileLayout); + }, []); + + useEffect(() => { + if (isMobileLayout) { + setCollapsed(true); + } + }, [isMobileLayout, location.pathname]); + const userMenuItems: MenuProps["items"] = useMemo(() => { const items: NonNullable = [ { @@ -382,14 +407,36 @@ export default function AppLayout() { ); + const layoutClassName = [ + "main-layout", + isMobileLayout ? "main-layout--mobile" : "", + isMobileLayout && !collapsed ? "main-layout--mobile-sider-open" : "", + ].filter(Boolean).join(" "); + return ( - +
+
+ {isMobileLayout && !collapsed ? ( + } > - setActiveTab(key as RoleTabKey)} className="role-detail-tabs"> - {"功能权限"}} key="permissions"> -
-
- { - const checked = Array.isArray(keys) ? keys : keys.checked; - const halfChecked = info.halfCheckedKeys || []; - setSelectedPermIds(checked.map((key) => Number(key))); - setHalfCheckedIds(halfChecked.map((key) => Number(key))); - setPermissionsDirty(true); - }} - defaultExpandAll - /> -
-
-
- {"数据权限"}} key="dataScope"> -
-
- { - setDataScopeType(event.target.value); - setDataScopeDirty(true); - }} - optionType="button" - buttonStyle="solid" - > - {DATA_SCOPE_OPTIONS.map((item) => ( - {item.label} - ))} - -
-
{getDataScopeDescription(dataScopeType)}
- {dataScopeType === "CUSTOM" ? ( -
- { - const checked = Array.isArray(keys) ? keys : keys.checked; - setScopeOrgIds(checked.map((key) => Number(key))); - setDataScopeDirty(true); - }} - defaultExpandAll + setActiveTab(key as RoleTabKey)} + className="role-detail-tabs" + items={[ + { + key: "permissions", + label: {"功能权限"}, + children: ( +
+
+ { + const checked = Array.isArray(keys) ? keys : keys.checked; + const halfChecked = info.halfCheckedKeys || []; + setSelectedPermIds(checked.map((key) => Number(key))); + setHalfCheckedIds(halfChecked.map((key) => Number(key))); + setPermissionsDirty(true); + }} + defaultExpandAll + /> +
+
+ ), + }, + { + key: "dataScope", + label: {"数据权限"}, + children: ( +
+
+ { + setDataScopeType(event.target.value); + setDataScopeDirty(true); + }} + optionType="button" + buttonStyle="solid" + > + {DATA_SCOPE_OPTIONS.map((item) => ( + {item.label} + ))} + +
+
{getDataScopeDescription(dataScopeType)}
+ {dataScopeType === "CUSTOM" ? ( +
+ { + const checked = Array.isArray(keys) ? keys : keys.checked; + setScopeOrgIds(checked.map((key) => Number(key))); + setDataScopeDirty(true); + }} + defaultExpandAll + /> +
+ ) : ( + + )} +
+ ), + }, + { + key: "users", + label: {`成员管理 (${roleUsers.length})`}, + children: ( +
+
+ {"已绑定用户"} + +
+ ( + + } style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} /> +
+
{user.displayName}
+
@{user.username}
+
+
+ ) + }, + { title: "手机号", dataIndex: "phone", className: "tabular-nums" }, + { title: "状态", dataIndex: "status", width: 80, render: (status: number) => {status === 1 ? "启用" : "停用"} }, + { + title: "操作", + key: "action", + width: 80, + render: (_: unknown, user: SysUser) => ( + void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}> + - -
( - - } style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} /> -
-
{user.displayName}
-
@{user.username}
-
-
- ) - }, - { title: "手机号", dataIndex: "phone", className: "tabular-nums" }, - { title: "状态", dataIndex: "status", width: 80, render: (status: number) => {status === 1 ? "启用" : "停用"} }, - { - title: "操作", - key: "action", - width: 80, - render: (_: unknown, user: SysUser) => ( - void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}> -
setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} /> - setDrawerOpen(false)} width={420} destroyOnClose footer={
}> + setDrawerOpen(false)} width={420} destroyOnHidden footer={
}> -
- - + - {fields.map(({ key, name, ...restField }) => ( 1 && + + - + + + + {role.roleName} diff --git a/frontend/src/pages/business/AiModels.tsx b/frontend/src/pages/business/AiModels.tsx index 33f2413..d90e2d2 100644 --- a/frontend/src/pages/business/AiModels.tsx +++ b/frontend/src/pages/business/AiModels.tsx @@ -623,8 +623,8 @@ const AiModels: React.FC = () => { - - + + { }}/> - + { - - + + @@ -724,8 +724,8 @@ const AiModels: React.FC = () => { {activeType === "ASR" && isLocalProvider && ( - + @@ -734,13 +734,13 @@ const AiModels: React.FC = () => { )} {activeType === "ASR" && isTencentProvider && ( - - + + - + @@ -752,13 +752,13 @@ const AiModels: React.FC = () => { - + - + @@ -772,13 +772,13 @@ const AiModels: React.FC = () => { - - + + - + @@ -787,8 +787,8 @@ const AiModels: React.FC = () => { )} - - + + { /> - + diff --git a/frontend/src/pages/business/ClientManagement.css b/frontend/src/pages/business/ClientManagement.css index 495262d..bcc8a56 100644 --- a/frontend/src/pages/business/ClientManagement.css +++ b/frontend/src/pages/business/ClientManagement.css @@ -16,3 +16,16 @@ gap: 12px; padding: 8px; } + +@media (max-width: 768px) { + .client-management-page > .page-container__body, + .client-management-page .section-card, + .client-management-page .section-card__content { + overflow-y: auto; + } + + .client-management-page__content > .data-list-panel { + flex: 0 0 min(560px, calc(100vh - 140px)); + min-height: 430px; + } +} diff --git a/frontend/src/pages/business/ClientManagement.tsx b/frontend/src/pages/business/ClientManagement.tsx index 7c94b79..24b3ec5 100644 --- a/frontend/src/pages/business/ClientManagement.tsx +++ b/frontend/src/pages/business/ClientManagement.tsx @@ -403,10 +403,10 @@ export default function ClientManagement() { width: 150, render: (_, record) => ( - }> - - + + - + { void handleUpload(file as File); return Upload.LIST_IGNORE; }}> @@ -504,18 +504,18 @@ export default function ClientManagement() { - - - + + + - - - + + +