feat:响应式布局调整

dev_na
puz 2026-07-06 11:08:36 +08:00
parent 93900308e0
commit 9d0edbfa00
43 changed files with 1038 additions and 253 deletions

View File

@ -399,7 +399,7 @@ export const MeetingCreateDrawer: React.FC<MeetingCreateDrawerProps> = ({
onClose={onCancel} onClose={onCancel}
width="min(960px, calc(100vw - 48px))" width="min(960px, calc(100vw - 48px))"
forceRender forceRender
destroyOnClose={false} destroyOnHidden={false}
placement="right" placement="right"
closable={false} closable={false}
footer={ footer={

View File

@ -121,6 +121,7 @@
min-width: 0; min-width: 0;
min-height: 32px; min-height: 32px;
align-items: center; align-items: center;
justify-content: flex-end;
} }
.data-list-panel__table-container { .data-list-panel__table-container {
@ -240,6 +241,22 @@
border-bottom-color: rgba(22, 119, 255, 0.18) !important; 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) { @media (max-width: 768px) {
.data-list-panel { .data-list-panel {
padding: 12px; padding: 12px;
@ -258,7 +275,15 @@
.data-list-panel__right-actions, .data-list-panel__right-actions,
.data-list-panel__right-actions .ant-space, .data-list-panel__right-actions .ant-space,
.data-list-panel__right-actions .ant-input-affix-wrapper, .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; width: 100% !important;
} }
.data-list-panel__right-actions .ant-space {
justify-content: flex-start;
}
} }

View File

@ -42,7 +42,11 @@
} }
.selection-action { .selection-action {
padding: 0;
border: 0;
background: transparent;
color: var(--link-color); color: var(--link-color);
font-family: inherit;
font-size: 14px; font-size: 14px;
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;

View File

@ -39,7 +39,7 @@ function ListTable<T extends Record<string, any>>({
showSizeChanger: { showSearch: false }, showSizeChanger: { showSearch: false },
showQuickJumper: true, showQuickJumper: true,
}, },
scroll = { x: 1200 }, scroll = { x: "max-content" },
onRowClick, onRowClick,
selectedRow, selectedRow,
loading = false, loading = false,
@ -49,7 +49,7 @@ function ListTable<T extends Record<string, any>>({
}: ListTableProps<T>) { }: ListTableProps<T>) {
const mergedScroll = React.useMemo(() => { const mergedScroll = React.useMemo(() => {
return { return {
x: 1200, x: "max-content",
...scroll, ...scroll,
}; };
}, [scroll]); }, [scroll]);
@ -95,9 +95,9 @@ function ListTable<T extends Record<string, any>>({
<span className="count-highlight">{totalCount || total}</span> <span className="count-highlight">{totalCount || total}</span>
</span> </span>
{onClearSelection && ( {onClearSelection && (
<a onClick={onClearSelection} className="selection-action"> <button type="button" onClick={onClearSelection} className="selection-action">
</a> </button>
)} )}
</> </>
) : selectedRowKeys.length > 0 ? ( ) : selectedRowKeys.length > 0 ? (
@ -106,14 +106,14 @@ function ListTable<T extends Record<string, any>>({
<span className="count-highlight">{selectedRowKeys.length}</span> <span className="count-highlight">{selectedRowKeys.length}</span>
</span> </span>
{onSelectAllPages && selectedRowKeys.length < (totalCount || total) && ( {onSelectAllPages && selectedRowKeys.length < (totalCount || total) && (
<a onClick={onSelectAllPages} className="selection-action"> <button type="button" onClick={onSelectAllPages} className="selection-action">
{totalCount || total} {totalCount || total}
</a> </button>
)} )}
{onClearSelection && ( {onClearSelection && (
<a onClick={onClearSelection} className="selection-action"> <button type="button" onClick={onClearSelection} className="selection-action">
</a> </button>
)} )}
</> </>
) : ( ) : (

View File

@ -201,6 +201,24 @@
flex-direction: column; 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 { .section-card__extra {
width: 100%; width: 100%;
justify-content: flex-start; justify-content: flex-start;

View File

@ -33,6 +33,14 @@
--meeting-progress-color: #1677ff; --meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px; --app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px); --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"] { :root[data-theme="minimal"] {
@ -365,6 +373,15 @@ body::after {
padding: 8px 4px 4px; 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(.app-page__drawer-footer) .ant-drawer-content-wrapper,
.ant-drawer:has(.screen-saver-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 { .meeting-create-drawer-root .ant-drawer-content-wrapper {
@ -928,6 +945,15 @@ body::after {
.app-page__content-toolbar-filters { .app-page__content-toolbar-filters {
justify-content: flex-start; 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; --meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px; --app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px); --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"] { :root[data-theme="tech"] {
@ -1385,6 +1419,14 @@ body::after {
--meeting-progress-color: #1677ff; --meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px; --app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px); --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, html,

View File

@ -54,6 +54,11 @@
gap: 16px; gap: 16px;
} }
.mobile-menu-trigger {
display: none;
flex: 0 0 auto;
}
.main-sider { .main-sider {
position: relative; position: relative;
height: calc(100vh - 64px); height: calc(100vh - 64px);
@ -270,6 +275,106 @@
background: var(--app-bg-layout); 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-header,
:root[data-theme="tech"] .main-sider, :root[data-theme="tech"] .main-sider,
:root[data-theme="tech"] .main-menu, :root[data-theme="tech"] .main-menu,

View File

@ -99,6 +99,7 @@ function findActiveMenu(nodes: PermissionMenuNode[], path: string, parentKeys: s
export default function AppLayout() { export default function AppLayout() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [isMobileLayout, setIsMobileLayout] = useState(false);
const [menus, setMenus] = useState<SysPermission[]>([]); const [menus, setMenus] = useState<SysPermission[]>([]);
const [availableTenants, setAvailableTenants] = useState<TenantInfo[]>([]); const [availableTenants, setAvailableTenants] = useState<TenantInfo[]>([]);
const [currentTenantId, setCurrentTenantId] = useState<number | null>(null); const [currentTenantId, setCurrentTenantId] = useState<number | null>(null);
@ -320,6 +321,30 @@ export default function AppLayout() {
setOpenKeys((prev) => Array.from(new Set([...prev, ...activeMenu.parentKeys]))); setOpenKeys((prev) => Array.from(new Set([...prev, ...activeMenu.parentKeys])));
}, [activeMenu]); }, [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 userMenuItems: MenuProps["items"] = useMemo(() => {
const items: NonNullable<MenuProps["items"]> = [ const items: NonNullable<MenuProps["items"]> = [
{ {
@ -382,14 +407,36 @@ export default function AppLayout() {
</div> </div>
); );
const layoutClassName = [
"main-layout",
isMobileLayout ? "main-layout--mobile" : "",
isMobileLayout && !collapsed ? "main-layout--mobile-sider-open" : "",
].filter(Boolean).join(" ");
return ( return (
<Layout className="main-layout"> <Layout className={layoutClassName}>
<Header className="main-header"> <Header className="main-header">
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
shape="circle"
onClick={() => setCollapsed((prev) => !prev)}
className="mobile-menu-trigger"
aria-label={collapsed ? "展开菜单" : "收起菜单"}
/>
{renderLogo()} {renderLogo()}
<div className="header-right">{headerRightTools}</div> <div className="header-right">{headerRightTools}</div>
</Header> </Header>
<Layout className="main-shell"> <Layout className="main-shell">
{isMobileLayout && !collapsed ? (
<button
type="button"
className="main-sider-backdrop"
aria-label="关闭菜单"
onClick={() => setCollapsed(true)}
/>
) : null}
<Sider <Sider
trigger={null} trigger={null}
collapsible collapsible
@ -414,6 +461,11 @@ export default function AppLayout() {
selectedKeys={selectedMenuKeys} selectedKeys={selectedMenuKeys}
openKeys={collapsed ? [] : openKeys} openKeys={collapsed ? [] : openKeys}
onOpenChange={setOpenKeys} onOpenChange={setOpenKeys}
onClick={() => {
if (isMobileLayout) {
setCollapsed(true);
}
}}
inlineCollapsed={collapsed} inlineCollapsed={collapsed}
items={menuItems} items={menuItems}
className="main-menu" className="main-menu"

View File

@ -40,7 +40,7 @@ import "./index.less";
const { Text } = Typography; const { Text } = Typography;
type TreePermission = SysPermission & { key: number; children?: TreePermission[] }; type TreePermission = SysPermission & { key: number; children?: TreePermission[] };
type MenuIconComponent = React.ElementType<{ "aria-hidden"?: string | boolean }>; type MenuIconComponent = React.ElementType<{ "aria-hidden"?: boolean | "true" | "false" }>;
const legacyIconAliases: Record<string, string> = { const legacyIconAliases: Record<string, string> = {
dashboard: "DashboardOutlined", dashboard: "DashboardOutlined",
@ -611,7 +611,7 @@ export default function Permissions() {
} }
> >
<Form form={form} layout="vertical" className="permission-form" onValuesChange={(changed) => changed.permType === "button" && form.setFieldsValue({ isVisible: 0 })}> <Form form={form} layout="vertical" className="permission-form" onValuesChange={(changed) => changed.permType === "button" && form.setFieldsValue({ isVisible: 0 })}>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={24}> <Col span={24}>
<Form.Item label={t("permissions.permType")} name="permType" rules={[{ required: true }]}> <Form.Item label={t("permissions.permType")} name="permType" rules={[{ required: true }]}>
<Select options={typeDict.map((item) => ({ value: item.itemValue, label: item.itemLabel }))} aria-label={t("permissions.permType")} /> <Select options={typeDict.map((item) => ({ value: item.itemValue, label: item.itemLabel }))} aria-label={t("permissions.permType")} />
@ -627,19 +627,19 @@ export default function Permissions() {
<Form.Item label={<Space><span>{t("permissions.permCode")}</span><Tooltip title={t("permissionsExt.codeHelp")}><InfoCircleOutlined className="text-gray-400" /></Tooltip></Space>} name="code" rules={[{ required: true, message: t("permissions.permCode") }]}> <Form.Item label={<Space><span>{t("permissions.permCode")}</span><Tooltip title={t("permissionsExt.codeHelp")}><InfoCircleOutlined className="text-gray-400" /></Tooltip></Space>} name="code" rules={[{ required: true, message: t("permissions.permCode") }]}>
<Input placeholder={t("permissionsExt.codePlaceholder")} className="tabular-nums" /> <Input placeholder={t("permissionsExt.codePlaceholder")} className="tabular-nums" />
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("permissions.path")} name="path"> <Form.Item label={t("permissions.path")} name="path">
<Input placeholder={t("permissionsExt.pathPlaceholder")} className="tabular-nums" /> <Input placeholder={t("permissionsExt.pathPlaceholder")} className="tabular-nums" />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("permissions.component")} name="component"> <Form.Item label={t("permissions.component")} name="component">
<Input placeholder={t("permissionsExt.componentPlaceholder")} /> <Input placeholder={t("permissionsExt.componentPlaceholder")} />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={24}> <Col span={24}>
<Form.Item label={t("permissions.icon")}> <Form.Item label={t("permissions.icon")}>
<Form.Item name="icon" noStyle> <Form.Item name="icon" noStyle>
@ -712,20 +712,20 @@ export default function Permissions() {
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("permissions.sort")} name="sortOrder" initialValue={0}> <Form.Item label={t("permissions.sort")} name="sortOrder" initialValue={0}>
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" /> <InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("permissions.isVisible")} name="isVisible" initialValue={1}> <Form.Item label={t("permissions.isVisible")} name="isVisible" initialValue={1}>
<Select options={visibleDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} /> <Select options={visibleDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("common.status")} name="status" initialValue={1}> <Form.Item label={t("common.status")} name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} /> <Select options={statusDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
</Form.Item> </Form.Item>

View File

@ -376,3 +376,77 @@
border: 1px dashed #cccccc; border: 1px dashed #cccccc;
margin: 0; margin: 0;
} }
@media (max-width: 991px) {
.roles-page-v2 > .page-container__body,
.roles-page-v2 .section-card,
.roles-page-v2 .section-card__content,
.roles-data-panel,
.roles-data-panel .data-list-panel__table-container,
.roles-data-panel .data-list-panel__table-area,
.roles-data-panel .app-page__table-wrap {
overflow-y: auto;
}
.roles-layout,
.roles-layout__row {
height: auto;
min-height: 100%;
}
.roles-layout__row {
row-gap: 12px;
}
.roles-layout__side,
.roles-layout__detail {
height: auto;
min-height: 360px;
}
.roles-side-card,
.roles-detail-card {
height: min(560px, calc(100vh - 180px));
min-height: 360px;
}
}
@media (max-width: 768px) {
.roles-page-v2 {
padding: 0;
}
.role-search-bar,
.role-members-toolbar {
align-items: stretch;
flex-direction: column;
}
.role-detail-card > .ant-card-head {
align-items: flex-start;
}
.role-detail-card > .ant-card-head .ant-card-head-wrapper {
align-items: flex-start;
flex-direction: column;
gap: 8px;
}
.role-detail-card > .ant-card-head .ant-card-extra,
.role-detail-card > .ant-card-head .ant-card-extra .ant-btn,
.role-search-bar .ant-btn {
width: 100%;
}
.roles-side-card,
.roles-detail-card {
height: min(520px, calc(100vh - 150px));
min-height: 320px;
}
.role-detail-icon {
width: 40px;
height: 40px;
font-size: 20px;
}
}

View File

@ -442,10 +442,10 @@ export default function Roles() {
> >
<div className="roles-layout"> <div className="roles-layout">
<Row gutter={8} className="roles-layout__row"> <Row gutter={8} className="roles-layout__row">
<Col span={7} className="roles-layout__side"> <Col xs={24} lg={7} className="roles-layout__side">
<Card <Card
title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>} title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>}
bordered={false} variant="borderless"
className="app-page__panel-card roles-side-card" className="app-page__panel-card roles-side-card"
> >
<div className="role-search-panel"> <div className="role-search-panel">
@ -493,11 +493,11 @@ export default function Roles() {
<div className="role-item-actions"> <div className="role-item-actions">
<Space size={4}> <Space size={4}>
<Tooltip title="编辑"> <Tooltip title="编辑">
<Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} /> <Button type="text" size="small" icon={<EditOutlined />} onClick={(event) => openEditBasic(event, item)} aria-label="编辑角色" />
</Tooltip> </Tooltip>
{item.roleCode !== "ADMIN" && ( {item.roleCode !== "ADMIN" && (
<Popconfirm title="确定删除该角色吗?" okText="确定" cancelText="取消" onConfirm={(event) => void handleRemove(event!, item.roleId)}> <Popconfirm title="确定删除该角色吗?" okText="确定" cancelText="取消" onConfirm={(event) => void handleRemove(event!, item.roleId)}>
<Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} /> <Button type="text" size="small" danger icon={<DeleteOutlined />} onClick={(event) => event.stopPropagation()} aria-label="删除角色" title="删除角色" />
</Popconfirm> </Popconfirm>
)} )}
</Space> </Space>
@ -514,116 +514,133 @@ export default function Roles() {
</Card> </Card>
</Col> </Col>
<Col span={17} className="roles-layout__detail"> <Col xs={24} lg={17} className="roles-layout__detail">
{selectedRole ? ( {selectedRole ? (
<Card <Card
className="app-page__panel-card roles-detail-card" className="app-page__panel-card roles-detail-card"
bordered={false} variant="borderless"
title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>} title={<div className="role-detail-header"><div className="role-detail-icon"><SafetyCertificateOutlined /></div><div className="role-detail-heading"><div className="role-detail-title">{selectedRole.roleName}</div><Text type="secondary" className="role-detail-code">{selectedRole.roleCode}</Text></div></div>}
extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>} extra={<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={handlePrimarySave} disabled={saveDisabled} style={{ borderRadius: "6px" }}>{saveLabel}</Button>}
> >
<Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as RoleTabKey)} className="role-detail-tabs"> <Tabs
<Tabs.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions"> activeKey={activeTab}
<div className="role-detail-pane"> onChange={(key) => setActiveTab(key as RoleTabKey)}
<div className="permission-tree-wrapper"> className="role-detail-tabs"
<Tree items={[
checkable {
selectable={false} key: "permissions",
checkStrictly={false} label: <Space><KeyOutlined />{"功能权限"}</Space>,
treeData={permissionTreeData} children: (
checkedKeys={selectedPermIds} <div className="role-detail-pane">
onCheck={(keys, info) => { <div className="permission-tree-wrapper">
const checked = Array.isArray(keys) ? keys : keys.checked; <Tree
const halfChecked = info.halfCheckedKeys || []; checkable
setSelectedPermIds(checked.map((key) => Number(key))); selectable={false}
setHalfCheckedIds(halfChecked.map((key) => Number(key))); checkStrictly={false}
setPermissionsDirty(true); treeData={permissionTreeData}
}} checkedKeys={selectedPermIds}
defaultExpandAll onCheck={(keys, info) => {
/> const checked = Array.isArray(keys) ? keys : keys.checked;
</div> const halfChecked = info.halfCheckedKeys || [];
</div> setSelectedPermIds(checked.map((key) => Number(key)));
</Tabs.TabPane> setHalfCheckedIds(halfChecked.map((key) => Number(key)));
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope"> setPermissionsDirty(true);
<div className="role-detail-pane"> }}
<div style={{ marginBottom: 16 }}> defaultExpandAll
<Radio.Group />
value={dataScopeType} </div>
onChange={(event) => { </div>
setDataScopeType(event.target.value); ),
setDataScopeDirty(true); },
}} {
optionType="button" key: "dataScope",
buttonStyle="solid" label: <Space><ApartmentOutlined />{"数据权限"}</Space>,
> children: (
{DATA_SCOPE_OPTIONS.map((item) => ( <div className="role-detail-pane">
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button> <div style={{ marginBottom: 16 }}>
))} <Radio.Group
</Radio.Group> value={dataScopeType}
</div> onChange={(event) => {
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div> setDataScopeType(event.target.value);
{dataScopeType === "CUSTOM" ? ( setDataScopeDirty(true);
<div className="permission-tree-wrapper"> }}
<Tree optionType="button"
checkable buttonStyle="solid"
selectable={false} >
treeData={scopeOrgTree} {DATA_SCOPE_OPTIONS.map((item) => (
checkedKeys={scopeOrgIds} <Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
onCheck={(keys) => { ))}
const checked = Array.isArray(keys) ? keys : keys.checked; </Radio.Group>
setScopeOrgIds(checked.map((key) => Number(key))); </div>
setDataScopeDirty(true); <div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
}} {dataScopeType === "CUSTOM" ? (
defaultExpandAll <div className="permission-tree-wrapper">
<Tree
checkable
selectable={false}
treeData={scopeOrgTree}
checkedKeys={scopeOrgIds}
onCheck={(keys) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
setScopeOrgIds(checked.map((key) => Number(key)));
setDataScopeDirty(true);
}}
defaultExpandAll
/>
</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" />
)}
</div>
),
},
{
key: "users",
label: <Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>,
children: (
<div className="role-detail-pane">
<div className="role-members-toolbar">
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{"绑定用户"}</Button>
</div>
<Table
rowKey="userId"
size="small"
loading={loadingUsers}
dataSource={roleUsers}
pagination={{ ...getStandardPagination(roleUsers.length, 1, 10, undefined, { size: "small", showSizeChanger: false }), current: undefined }}
columns={[
{
title: "用户信息",
render: (_: unknown, user: SysUser) => (
<Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
<div>
<div style={{ fontWeight: 500 }}>{user.displayName}</div>
<div style={{ fontSize: 11, color: "#bfbfbf" }}>@{user.username}</div>
</div>
</Space>
)
},
{ title: "手机号", dataIndex: "phone", className: "tabular-nums" },
{ title: "状态", dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? "启用" : "停用"}</Tag> },
{
title: "操作",
key: "action",
width: 80,
render: (_: unknown, user: SysUser) => (
<Popconfirm title="确定解除该用户绑定吗?" okText="确定" cancelText="取消" onConfirm={() => void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} aria-label="解除用户绑定" title="解除用户绑定" />
</Popconfirm>
)
}
]}
/> />
</div> </div>
) : ( ),
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="当前范围不需要选择部门" /> },
)} ]}
</div> />
</Tabs.TabPane>
<Tabs.TabPane tab={<Space><TeamOutlined />{`成员管理 (${roleUsers.length})`}</Space>} key="users">
<div className="role-detail-pane">
<div className="role-members-toolbar">
<Title level={5} style={{ margin: 0 }}>{"已绑定用户"}</Title>
<Button type="primary" ghost icon={<UserAddOutlined />} onClick={openUserModal} disabled={!can("sys:role:update")}>{"绑定用户"}</Button>
</div>
<Table
rowKey="userId"
size="small"
loading={loadingUsers}
dataSource={roleUsers}
pagination={{ ...getStandardPagination(roleUsers.length, 1, 10, undefined, { size: "small", showSizeChanger: false }), current: undefined }}
columns={[
{
title: "用户信息",
render: (_: unknown, user: SysUser) => (
<Space>
<Avatar size="small" icon={<UserOutlined />} style={{ backgroundColor: "#f0f2f5", color: "#8c8c8c" }} />
<div>
<div style={{ fontWeight: 500 }}>{user.displayName}</div>
<div style={{ fontSize: 11, color: "#bfbfbf" }}>@{user.username}</div>
</div>
</Space>
)
},
{ title: "手机号", dataIndex: "phone", className: "tabular-nums" },
{ title: "状态", dataIndex: "status", width: 80, render: (status: number) => <Tag color={status === 1 ? "green" : "red"}>{status === 1 ? "启用" : "停用"}</Tag> },
{
title: "操作",
key: "action",
width: 80,
render: (_: unknown, user: SysUser) => (
<Popconfirm title="确定解除该用户绑定吗?" okText="确定" cancelText="取消" onConfirm={() => void handleUnbindUser(user.userId)} disabled={!can("sys:role:update")}>
<Button type="text" danger size="small" icon={<DeleteOutlined />} disabled={!can("sys:role:update")} />
</Popconfirm>
)
}
]}
/>
</div>
</Tabs.TabPane>
</Tabs>
</Card> </Card>
) : ( ) : (
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div> <div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
@ -634,14 +651,14 @@ export default function Roles() {
</DataListPanel> </DataListPanel>
</SectionCard> </SectionCard>
<Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnClose> <Modal title="绑定用户到角色" open={userModalOpen} onCancel={() => setUserModalOpen(false)} onOk={() => void handleAddUsers()} okText="确定" cancelText="取消" width={650} destroyOnHidden>
<div style={{ marginBottom: 16 }}> <div style={{ marginBottom: 16 }}>
<Input placeholder="搜索用户名或显示名称" prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear /> <Input placeholder="搜索用户名或显示名称" prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
</div> </div>
<Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ ...getStandardPagination(filteredModalUsers.length, 1, 6, undefined, { size: "small", showSizeChanger: false }), current: undefined }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} /> <Table rowKey="userId" size="small" dataSource={filteredModalUsers} pagination={{ ...getStandardPagination(filteredModalUsers.length, 1, 6, undefined, { size: "small", showSizeChanger: false }), current: undefined }} rowSelection={{ selectedRowKeys: selectedUserKeys, onChange: (keys) => setSelectedUserKeys(keys as number[]) }} columns={[{ title: "显示名称", dataIndex: "displayName" }, { title: "用户名", dataIndex: "username" }, { title: "手机号", dataIndex: "phone" }]} />
</Modal> </Modal>
<Drawer title={editing ? "编辑角色" : "新增角色"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{"取消"}</Button><Button type="primary" loading={saving} onClick={() => void submitBasic()}>{"保存"}</Button></div>}> <Drawer title={editing ? "编辑角色" : "新增角色"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnHidden footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{"取消"}</Button><Button type="primary" loading={saving} onClick={() => void submitBasic()}>{"保存"}</Button></div>}>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item label="租户" name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}> <Form.Item label="租户" name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} /> <Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} />

View File

@ -552,7 +552,7 @@ export default function Users() {
</SectionCard> </SectionCard>
<Drawer title={<div className="user-drawer-title"><UserOutlined aria-hidden="true"/><span>{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</span> <Drawer title={<div className="user-drawer-title"><UserOutlined aria-hidden="true"/><span>{editing ? t("users.drawerTitleEdit") : t("users.drawerTitleCreate")}</span>
</div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnClose </div>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={520} destroyOnHidden
footer={<div className="app-page__drawer-footer"><Button footer={<div className="app-page__drawer-footer"><Button
onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary"
loading={saving} loading={saving}
@ -560,8 +560,8 @@ export default function Users() {
</div>}> </div>}>
<Form form={form} layout="vertical" className="user-form"> <Form form={form} layout="vertical" className="user-form">
<Title level={5} style={{ marginBottom: 16 }}>{t("usersExt.basicInfo")}</Title> <Title level={5} style={{ marginBottom: 16 }}>{t("usersExt.basicInfo")}</Title>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item label={t("users.username")} name="username" rules={[{ <Col xs={24} md={12}><Form.Item label={t("users.username")} name="username" rules={[{
required: true, required: true,
message: t("users.username") message: t("users.username")
}, { }, {
@ -571,17 +571,17 @@ export default function Users() {
extra={t("usersExt.usernameFormatTip", {defaultValue: "登录名只能输入数字、小写英文、@ 和 _"})}><Input extra={t("usersExt.usernameFormatTip", {defaultValue: "登录名只能输入数字、小写英文、@ 和 _"})}><Input
placeholder={t("usersExt.usernamePlaceholder", {defaultValue: "仅支持 a-z、0-9、@、_"})} disabled={!!editing} placeholder={t("usersExt.usernamePlaceholder", {defaultValue: "仅支持 a-z、0-9、@、_"})} disabled={!!editing}
className="tabular-nums"/></Form.Item></Col> className="tabular-nums"/></Form.Item></Col>
<Col span={12}><Form.Item label={t("users.displayName")} name="displayName" <Col xs={24} md={12}><Form.Item label={t("users.displayName")} name="displayName"
rules={[{required: true, message: t("users.displayName")}]}><Input rules={[{required: true, message: t("users.displayName")}]}><Input
placeholder={t("users.displayName")}/></Form.Item></Col> placeholder={t("users.displayName")}/></Form.Item></Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item label={t("users.email")} name="email" <Col xs={24} md={12}><Form.Item label={t("users.email")} name="email"
rules={[{required: true, message: "请输入邮箱地址"}, { rules={[{required: true, message: "请输入邮箱地址"}, {
type: "email", type: "email",
message: "请输入正确的邮箱格式" message: "请输入正确的邮箱格式"
}]}><Input placeholder={t("usersExt.emailPlaceholder")} className="tabular-nums"/></Form.Item></Col> }]}><Input placeholder={t("usersExt.emailPlaceholder")} className="tabular-nums"/></Form.Item></Col>
<Col span={12}><Form.Item label={t("users.phone")} name="phone"><Input placeholder={t("users.phone")} <Col xs={24} md={12}><Form.Item label={t("users.phone")} name="phone"><Input placeholder={t("users.phone")}
className="tabular-nums"/></Form.Item></Col> className="tabular-nums"/></Form.Item></Col>
</Row> </Row>
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl"><Input <Form.Item label={t("profile.avatarUrl")} name="avatarUrl"><Input
@ -610,13 +610,13 @@ export default function Users() {
<> <>
{fields.map(({ key, name, ...restField }) => ( {fields.map(({ key, name, ...restField }) => (
<Card key={key} size="small" className="mb-3" styles={{ body: { padding: "12px" } }} title={t("usersExt.membershipTitle", { index: name + 1 })} extra={fields.length > 1 && <Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />}> <Card key={key} size="small" className="mb-3" styles={{ body: { padding: "12px" } }} title={t("usersExt.membershipTitle", { index: name + 1 })} extra={fields.length > 1 && <Button type="text" danger icon={<MinusCircleOutlined />} onClick={() => remove(name)} />}>
<Row gutter={12}> <Row gutter={12} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item {...restField} label={t("users.tenant")} name={[name, "tenantId"]} rules={[{ required: true, message: t("usersExt.membershipRequired") }]}> <Form.Item {...restField} label={t("users.tenant")} name={[name, "tenantId"]} rules={[{ required: true, message: t("usersExt.membershipRequired") }]}>
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} placeholder={t("usersExt.selectTenant")} /> <Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} placeholder={t("usersExt.selectTenant")} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<MembershipOrgSelect fieldProps={{ ...restField }} name={name} tenantId={form.getFieldValue(["memberships", name, "tenantId"])} /> <MembershipOrgSelect fieldProps={{ ...restField }} name={name} tenantId={form.getFieldValue(["memberships", name, "tenantId"])} />
</Col> </Col>
</Row> </Row>
@ -683,8 +683,8 @@ export default function Users() {
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" /> <Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
</Form.Item> </Form.Item>
) : null} ) : null}
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item <Form.Item
label={t("common.status")} label={t("common.status")}
name="status" name="status"
@ -697,7 +697,7 @@ export default function Users() {
</Form.Item> </Form.Item>
</Col> </Col>
{isPlatformMode && {isPlatformMode &&
<Col span={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch/></Form.Item></Col>} <Col xs={24} md={12}><Form.Item label={t("users.platformAdmin")} name="isPlatformAdmin" valuePropName="checked"><Switch/></Form.Item></Col>}
</Row> </Row>
</Form> </Form>
</Drawer> </Drawer>
@ -712,7 +712,7 @@ export default function Users() {
}} }}
onOk={() => void submitResetPassword()} onOk={() => void submitResetPassword()}
confirmLoading={resetPasswordLoading} confirmLoading={resetPasswordLoading}
destroyOnClose destroyOnHidden
> >
<Form form={resetPasswordForm} layout="vertical"> <Form form={resetPasswordForm} layout="vertical">
<Form.Item <Form.Item

View File

@ -22,7 +22,8 @@
margin: 0 8px 8px; margin: 0 8px 8px;
} }
.role-permission-layout > .ant-col { .role-permission-layout > .ant-col,
.role-permission-layout__pane {
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
@ -34,3 +35,42 @@
.role-permission-layout .full-height-card .ant-card-body { .role-permission-layout .full-height-card .ant-card-body {
min-height: 0; min-height: 0;
} }
@media (max-width: 991px) {
.role-permission-page > .page-container__body,
.role-permission-page .section-card,
.role-permission-page .section-card__content {
overflow-y: auto;
}
.role-permission-layout {
overflow: visible;
row-gap: 12px;
}
.role-permission-layout > .ant-col,
.role-permission-layout__pane {
height: auto;
min-height: 360px;
}
.role-permission-layout .full-height-card {
height: min(430px, calc(100vh - 170px));
min-height: 360px;
}
}
@media (max-width: 768px) {
.role-permission-page {
padding: 0;
}
.role-permission-toolbar {
margin: 0 0 8px;
}
.role-permission-layout .full-height-card {
height: min(390px, calc(100vh - 155px));
min-height: 320px;
}
}

View File

@ -196,7 +196,7 @@ export default function RolePermissionBinding() {
<div className="app-page__content-toolbar-filters" /> <div className="app-page__content-toolbar-filters" />
</div> </div>
<Row gutter={16} className="role-permission-layout"> <Row gutter={16} className="role-permission-layout">
<Col xs={24} lg={10} style={{ height: "100%" }}> <Col xs={24} lg={10} className="role-permission-layout__pane">
<Card title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>{t("rolePerm.roleList")}</span></Space>} className="app-page__panel-card full-height-card"> <Card title={<Space><SafetyCertificateOutlined aria-hidden="true" /><span>{t("rolePerm.roleList")}</span></Space>} className="app-page__panel-card full-height-card">
<div className="mb-4"> <div className="mb-4">
<Input <Input
@ -247,7 +247,7 @@ export default function RolePermissionBinding() {
</Card> </Card>
</Col> </Col>
<Col xs={24} lg={14} style={{ height: "100%" }}> <Col xs={24} lg={14} className="role-permission-layout__pane">
<Card <Card
title={<Space><KeyOutlined aria-hidden="true" /><span>{t("rolePerm.permConfig")}</span></Space>} title={<Space><KeyOutlined aria-hidden="true" /><span>{t("rolePerm.permConfig")}</span></Space>}
className="app-page__panel-card full-height-card" className="app-page__panel-card full-height-card"

View File

@ -22,7 +22,8 @@
margin: 0 8px 8px; margin: 0 8px 8px;
} }
.user-role-layout > .ant-col { .user-role-layout > .ant-col,
.user-role-layout__pane {
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
@ -34,3 +35,42 @@
.user-role-layout .full-height-card .ant-card-body { .user-role-layout .full-height-card .ant-card-body {
min-height: 0; min-height: 0;
} }
@media (max-width: 991px) {
.user-role-page > .page-container__body,
.user-role-page .section-card,
.user-role-page .section-card__content {
overflow-y: auto;
}
.user-role-layout {
overflow: visible;
row-gap: 12px;
}
.user-role-layout > .ant-col,
.user-role-layout__pane {
height: auto;
min-height: 360px;
}
.user-role-layout .full-height-card {
height: min(430px, calc(100vh - 170px));
min-height: 360px;
}
}
@media (max-width: 768px) {
.user-role-page {
padding: 0;
}
.user-role-toolbar {
margin: 0 0 8px;
}
.user-role-layout .full-height-card {
height: min(390px, calc(100vh - 155px));
min-height: 320px;
}
}

View File

@ -116,7 +116,7 @@ export default function UserRoleBinding() {
<div className="app-page__content-toolbar-filters" /> <div className="app-page__content-toolbar-filters" />
</div> </div>
<Row gutter={16} className="user-role-layout"> <Row gutter={16} className="user-role-layout">
<Col xs={24} lg={12} style={{ height: "100%" }}> <Col xs={24} lg={12} className="user-role-layout__pane">
<Card title={<Space><UserOutlined aria-hidden="true" /><span>{t("userRole.userList")}</span></Space>} className="app-page__panel-card full-height-card"> <Card title={<Space><UserOutlined aria-hidden="true" /><span>{t("userRole.userList")}</span></Space>} className="app-page__panel-card full-height-card">
<div className="mb-4"> <div className="mb-4">
<Input <Input
@ -167,7 +167,7 @@ export default function UserRoleBinding() {
</Card> </Card>
</Col> </Col>
<Col xs={24} lg={12} style={{ height: "100%" }}> <Col xs={24} lg={12} className="user-role-layout__pane">
<Card <Card
title={<Space><TeamOutlined aria-hidden="true" /><span>{t("userRole.grantRoles")}</span></Space>} title={<Space><TeamOutlined aria-hidden="true" /><span>{t("userRole.grantRoles")}</span></Space>}
className="app-page__panel-card full-height-card" className="app-page__panel-card full-height-card"
@ -178,7 +178,7 @@ export default function UserRoleBinding() {
<Checkbox.Group style={{ width: "100%" }} value={checkedRoleIds} onChange={(values) => setCheckedRoleIds(values as number[])} disabled={loadingRoles}> <Checkbox.Group style={{ width: "100%" }} value={checkedRoleIds} onChange={(values) => setCheckedRoleIds(values as number[])} disabled={loadingRoles}>
<Row gutter={[16, 16]}> <Row gutter={[16, 16]}>
{roles.map((role) => ( {roles.map((role) => (
<Col key={role.roleId} span={12}> <Col key={role.roleId} xs={24} md={12}>
<Checkbox value={role.roleId} className="w-full"> <Checkbox value={role.roleId} className="w-full">
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<span style={{ fontWeight: 500 }}>{role.roleName}</span> <span style={{ fontWeight: 500 }}>{role.roleName}</span>

View File

@ -623,8 +623,8 @@ const AiModels: React.FC = () => {
</Tag> </Tag>
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item <Form.Item
name="modelName" name="modelName"
label="显示名称" label="显示名称"
@ -635,7 +635,7 @@ const AiModels: React.FC = () => {
}}/> }}/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item <Form.Item
name="provider" name="provider"
label="提供商" label="提供商"
@ -652,8 +652,8 @@ const AiModels: React.FC = () => {
</Col> </Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="sortOrder" label="排序值"> <Form.Item name="sortOrder" label="排序值">
<InputNumber min={0} style={{ width: "100%" }} /> <InputNumber min={0} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
@ -724,8 +724,8 @@ const AiModels: React.FC = () => {
</Form.Item> </Form.Item>
{activeType === "ASR" && isLocalProvider && ( {activeType === "ASR" && isLocalProvider && (
<Row gutter={16} hidden> <Row gutter={16} hidden className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="svThreshold" label="声纹阈值"> <Form.Item name="svThreshold" label="声纹阈值">
<InputNumber min={0} max={1} step={0.01} style={{ width: "100%" }} /> <InputNumber min={0} max={1} step={0.01} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
@ -734,13 +734,13 @@ const AiModels: React.FC = () => {
)} )}
{activeType === "ASR" && isTencentProvider && ( {activeType === "ASR" && isTencentProvider && (
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "请输入 App ID"}]}> <Form.Item name="tencentAppId" label="App ID" rules={[{required: true, message: "请输入 App ID"}]}>
<Input/> <Input/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="tencentSecretId" label="Secret ID" <Form.Item name="tencentSecretId" label="Secret ID"
rules={[{required: true, message: "请输入 Secret ID"}]}> rules={[{required: true, message: "请输入 Secret ID"}]}>
<Input/> <Input/>
@ -752,13 +752,13 @@ const AiModels: React.FC = () => {
<Input.Password/> <Input.Password/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="tencentOfflineModelCode" label="离线识别模型" <Form.Item name="tencentOfflineModelCode" label="离线识别模型"
rules={[{required: true, message: "请输入离线识别模型"}]}> rules={[{required: true, message: "请输入离线识别模型"}]}>
<Input placeholder="例如16k_zh"/> <Input placeholder="例如16k_zh"/>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="tencentRealtimeModelCode" label="实时识别模型" <Form.Item name="tencentRealtimeModelCode" label="实时识别模型"
rules={[{required: true, message: "请输入实时识别模型"}]}> rules={[{required: true, message: "请输入实时识别模型"}]}>
<Input placeholder="例如16k_zh_realtime"/> <Input placeholder="例如16k_zh_realtime"/>
@ -772,13 +772,13 @@ const AiModels: React.FC = () => {
<Form.Item name="apiPath" label="API 路径" initialValue="/v1/chat/completions"> <Form.Item name="apiPath" label="API 路径" initialValue="/v1/chat/completions">
<Input /> <Input />
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="temperature" label="Temperature"> <Form.Item name="temperature" label="Temperature">
<InputNumber min={0} max={2} step={0.1} style={{ width: "100%" }} /> <InputNumber min={0} max={2} step={0.1} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="topP" label="Top P"> <Form.Item name="topP" label="Top P">
<InputNumber min={0} max={1} step={0.1} style={{ width: "100%" }} /> <InputNumber min={0} max={1} step={0.1} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
@ -787,8 +787,8 @@ const AiModels: React.FC = () => {
</> </>
)} )}
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={8}> <Col xs={24} md={8}>
<Form.Item name="isDefaultChecked" label="设为默认" valuePropName="checked"> <Form.Item name="isDefaultChecked" label="设为默认" valuePropName="checked">
<Switch <Switch
checkedChildren="是" checkedChildren="是"
@ -801,7 +801,7 @@ const AiModels: React.FC = () => {
/> />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={8}> <Col xs={24} md={8}>
<Form.Item name="statusChecked" label="状态" valuePropName="checked"> <Form.Item name="statusChecked" label="状态" valuePropName="checked">
<Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)} /> <Switch checkedChildren="启用" unCheckedChildren="禁用" disabled={Boolean(isDefaultChecked)} />
</Form.Item> </Form.Item>

View File

@ -16,3 +16,16 @@
gap: 12px; gap: 12px;
padding: 8px; 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;
}
}

View File

@ -403,10 +403,10 @@ export default function ClientManagement() {
width: 150, width: 150,
render: (_, record) => ( render: (_, record) => (
<Space size={4}> <Space size={4}>
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} /> <Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} aria-label="编辑客户端版本" title="编辑客户端版本" />
<Button type="text" icon={<DownloadOutlined />} href={record.downloadUrl} target="_blank" /> <Button type="text" icon={<DownloadOutlined />} href={record.downloadUrl} target="_blank" aria-label="下载安装包" title="下载安装包" />
<Popconfirm title="确认删除该版本吗?" onConfirm={() => void handleDelete(record)}> <Popconfirm title="确认删除该版本吗?" onConfirm={() => void handleDelete(record)}>
<Button type="text" danger icon={<DeleteOutlined />} /> <Button type="text" danger icon={<DeleteOutlined />} aria-label="删除客户端版本" title="删除客户端版本" />
</Popconfirm> </Popconfirm>
</Space> </Space>
), ),
@ -473,7 +473,7 @@ export default function ClientManagement() {
columns={columns} columns={columns}
dataSource={pagedRecords} dataSource={pagedRecords}
loading={loading || groupLoading || platformLoading} loading={loading || groupLoading || platformLoading}
scroll={{ x: 1100, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
pagination={false} pagination={false}
/> />
</DataListPanel> </DataListPanel>
@ -481,8 +481,8 @@ export default function ClientManagement() {
<Drawer title={editing ? "编辑客户端版本" : "新增客户端版本"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={680} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}></Button><Button type="primary" icon={<UploadOutlined />} loading={saving} onClick={() => void handleSubmit()}></Button></div>}> <Drawer title={editing ? "编辑客户端版本" : "新增客户端版本"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={680} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}></Button><Button type="primary" icon={<UploadOutlined />} loading={saving} onClick={() => void handleSubmit()}></Button></div>}>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item name="platformCode" label="发布平台" rules={[{ required: true, message: "请选择发布平台" }]}> <Form.Item name="platformCode" label="发布平台" rules={[{ required: true, message: "请选择发布平台" }]}>
<Select placeholder="选择平台" disabled={!!editing || platformGroups.length === 0}> <Select placeholder="选择平台" disabled={!!editing || platformGroups.length === 0}>
{platformGroups.map((group) => ( {platformGroups.map((group) => (
@ -495,7 +495,7 @@ export default function ClientManagement() {
</Select> </Select>
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label="安装包上传"> <Form.Item label="安装包上传">
<Upload showUploadList={false} beforeUpload={(file) => { void handleUpload(file as File); return Upload.LIST_IGNORE; }}> <Upload showUploadList={false} beforeUpload={(file) => { void handleUpload(file as File); return Upload.LIST_IGNORE; }}>
<Button icon={<UploadOutlined />} loading={uploading} style={{ width: "100%" }}></Button> <Button icon={<UploadOutlined />} loading={uploading} style={{ width: "100%" }}></Button>
@ -504,18 +504,18 @@ export default function ClientManagement() {
</Col> </Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item name="version" label="版本号" rules={[{ required: true, message: "请输入版本号" }]}><Input placeholder="如 1.0.0" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name="version" label="版本号" rules={[{ required: true, message: "请输入版本号" }]}><Input placeholder="如 1.0.0" /></Form.Item></Col>
<Col span={12}><Form.Item name="versionCode" label="版本码"><InputNumber min={0} style={{ width: "100%" }} placeholder="如 1000" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name="versionCode" label="版本码"><InputNumber min={0} style={{ width: "100%" }} placeholder="如 1000" /></Form.Item></Col>
</Row> </Row>
<Form.Item name="downloadUrl" label="下载链接" rules={[{ required: true, message: "请输入下载链接" }]}> <Form.Item name="downloadUrl" label="下载链接" rules={[{ required: true, message: "请输入下载链接" }]}>
<Input placeholder="https://..." /> <Input placeholder="https://..." />
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item name="fileSize" label="文件大小Bytes"><InputNumber min={0} style={{ width: "100%" }} /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name="fileSize" label="文件大小Bytes"><InputNumber min={0} style={{ width: "100%" }} /></Form.Item></Col>
<Col span={12}><Form.Item name="minSystemVersion" label="最低系统要求"><Input placeholder="如 Android 8.0 / Windows 10" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name="minSystemVersion" label="最低系统要求"><Input placeholder="如 Android 8.0 / Windows 10" /></Form.Item></Col>
</Row> </Row>
<Form.Item name="releaseNotes" label="更新说明"><TextArea rows={4} placeholder="请输入版本更新内容" /></Form.Item> <Form.Item name="releaseNotes" label="更新说明"><TextArea rows={4} placeholder="请输入版本更新内容" /></Form.Item>

View File

@ -17,6 +17,19 @@
padding: 8px; padding: 8px;
} }
@media (max-width: 768px) {
.external-app-page > .page-container__body,
.external-app-page .section-card,
.external-app-page .section-card__content {
overflow-y: auto;
}
.external-app-page__content > .data-list-panel {
flex: 0 0 min(520px, calc(100vh - 140px));
min-height: 400px;
}
}
.external-app-icon-form-item { .external-app-icon-form-item {
margin-bottom: 20px; margin-bottom: 20px;
} }

View File

@ -306,10 +306,10 @@ export default function ExternalAppManagement() {
const link = record.appType === "native" ? info.apkUrl : info.webUrl; const link = record.appType === "native" ? info.apkUrl : info.webUrl;
return ( return (
<Space size={4}> <Space size={4}>
<Button type="text" icon={<LinkOutlined />} href={link} target="_blank" disabled={!link} /> <Button type="text" icon={<LinkOutlined />} href={link} target="_blank" disabled={!link} aria-label="打开应用入口" title="打开应用入口" />
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} /> <Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} aria-label="编辑外部应用" title="编辑外部应用" />
<Popconfirm title="确认删除该应用吗?" onConfirm={() => void handleDelete(record)}> <Popconfirm title="确认删除该应用吗?" onConfirm={() => void handleDelete(record)}>
<Button type="text" danger icon={<DeleteOutlined />} /> <Button type="text" danger icon={<DeleteOutlined />} aria-label="删除外部应用" title="删除外部应用" />
</Popconfirm> </Popconfirm>
</Space> </Space>
); );
@ -366,9 +366,9 @@ export default function ExternalAppManagement() {
<Drawer title={editing ? "编辑外部应用" : "新增外部应用"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={700} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}></Button><Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void handleSubmit()}></Button></div>}> <Drawer title={editing ? "编辑外部应用" : "新增外部应用"} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={700} destroyOnHidden forceRender footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}></Button><Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={() => void handleSubmit()}></Button></div>}>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={16}><Form.Item name="appName" label="应用名称" rules={[{ required: true, message: "请输入应用名称" }]}><Input placeholder="请输入应用名称" /></Form.Item></Col> <Col xs={24} md={16}><Form.Item name="appName" label="应用名称" rules={[{ required: true, message: "请输入应用名称" }]}><Input placeholder="请输入应用名称" /></Form.Item></Col>
<Col span={8}><Form.Item name="appType" label="应用类型" rules={[{ required: true, message: "请选择应用类型" }]}><Select options={[{ label: "原生应用", value: "native" }, { label: "Web 应用", value: "web" }]} /></Form.Item></Col> <Col xs={24} md={8}><Form.Item name="appType" label="应用类型" rules={[{ required: true, message: "请选择应用类型" }]}><Select options={[{ label: "原生应用", value: "native" }, { label: "Web 应用", value: "web" }]} /></Form.Item></Col>
</Row> </Row>
<Form.Item name="iconUrl" hidden> <Form.Item name="iconUrl" hidden>
@ -410,26 +410,26 @@ export default function ExternalAppManagement() {
<Button icon={<UploadOutlined />} loading={uploadingApk}> APK </Button> <Button icon={<UploadOutlined />} loading={uploadingApk}> APK </Button>
</Upload> </Upload>
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item name={["appInfo", "versionName"]} label="版本号"><Input placeholder="如 1.0.0" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name={["appInfo", "versionName"]} label="版本号"><Input placeholder="如 1.0.0" /></Form.Item></Col>
<Col span={12}><Form.Item name={["appInfo", "packageName"]} label="包名"><Input placeholder="com.example.app" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name={["appInfo", "packageName"]} label="包名"><Input placeholder="com.example.app" /></Form.Item></Col>
</Row> </Row>
<Form.Item name={["appInfo", "apkUrl"]} label="APK 地址" rules={[{ required: true, message: "请上传或填写 APK 地址" }]}><Input prefix={<LinkOutlined />} placeholder="https://..." /></Form.Item> <Form.Item name={["appInfo", "apkUrl"]} label="APK 地址" rules={[{ required: true, message: "请上传或填写 APK 地址" }]}><Input prefix={<LinkOutlined />} placeholder="https://..." /></Form.Item>
</> </>
) : ( ) : (
<> <>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item name={["appInfo", "versionName"]} label="版本号"><Input placeholder="可选" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name={["appInfo", "versionName"]} label="版本号"><Input placeholder="可选" /></Form.Item></Col>
<Col span={12}><Form.Item name={["appInfo", "webUrl"]} label="Web 地址" rules={[{ required: true, message: "请输入 Web 地址" }]}><Input prefix={<GlobalOutlined />} placeholder="https://..." /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name={["appInfo", "webUrl"]} label="Web 地址" rules={[{ required: true, message: "请输入 Web 地址" }]}><Input prefix={<GlobalOutlined />} placeholder="https://..." /></Form.Item></Col>
</Row> </Row>
</> </>
)} )}
</Form.Item> </Form.Item>
<Form.Item name="description" label="应用描述"><TextArea rows={3} placeholder="请输入应用用途说明" /></Form.Item> <Form.Item name="description" label="应用描述"><TextArea rows={3} placeholder="请输入应用用途说明" /></Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}><Form.Item name="sortOrder" label="排序权重"><InputNumber min={0} style={{ width: "100%" }} /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name="sortOrder" label="排序权重"><InputNumber min={0} style={{ width: "100%" }} /></Form.Item></Col>
<Col span={12}><Form.Item name="remark" label="备注"><Input placeholder="选填" /></Form.Item></Col> <Col xs={24} md={12}><Form.Item name="remark" label="备注"><Input placeholder="选填" /></Form.Item></Col>
</Row> </Row>
<Form.Item name="statusEnabled" label="启用状态" valuePropName="checked"><Switch /></Form.Item> <Form.Item name="statusEnabled" label="启用状态" valuePropName="checked"><Switch /></Form.Item>
</Form> </Form>

View File

@ -320,16 +320,30 @@
} }
@media (max-width: 992px) { @media (max-width: 992px) {
.hotwords-page > .page-container__body,
.hotwords-page .section-card,
.hotwords-page .section-card__content {
overflow-y: auto;
}
.hotwords-layout { .hotwords-layout {
flex: none;
flex-direction: column; flex-direction: column;
overflow: auto; overflow: visible;
} }
.hotwords-group-panel.ant-card { .hotwords-group-panel.ant-card {
width: 100%; width: 100%;
max-width: none; max-width: none;
height: min(430px, calc(100vh - 170px));
min-height: 360px; min-height: 360px;
} }
.hotwords-list-panel {
flex: none;
height: min(560px, calc(100vh - 150px));
min-height: 430px;
}
} }
@media (max-width: 768px) { @media (max-width: 768px) {
@ -340,4 +354,14 @@
.hotwords-list-panel .data-list-panel__right-actions .ant-select { .hotwords-list-panel .data-list-panel__right-actions .ant-select {
width: 100% !important; width: 100% !important;
} }
.hotwords-group-panel.ant-card {
height: min(390px, calc(100vh - 155px));
min-height: 320px;
}
.hotwords-list-panel {
height: min(520px, calc(100vh - 135px));
min-height: 380px;
}
} }

View File

@ -16,3 +16,16 @@
gap: 12px; gap: 12px;
padding: 8px; padding: 8px;
} }
@media (max-width: 768px) {
.license-management-page > .page-container__body,
.license-management-page .section-card,
.license-management-page .section-card__content {
overflow-y: auto;
}
.license-management-page__content > .data-list-panel {
flex: 0 0 min(520px, calc(100vh - 140px));
min-height: 400px;
}
}

View File

@ -184,7 +184,7 @@ export default function LicenseManagement() {
dataSource={pagedRecords} dataSource={pagedRecords}
loading={loading} loading={loading}
pagination={false} pagination={false}
scroll={{ x: 1000, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
/> />
</DataListPanel> </DataListPanel>
</SectionCard> </SectionCard>

View File

@ -599,8 +599,8 @@ const MeetingProgressDisplay: React.FC<{
<Text type="secondary"></Text> <Text type="secondary"></Text>
</div> </div>
<Divider style={{ margin: '32px 0' }} /> <Divider style={{ margin: '32px 0' }} />
<Row gutter={24}> <Row gutter={[24, 16]}>
<Col span={8}> <Col xs={24} md={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Title level={4} style={{ margin: 0 }}> <Title level={4} style={{ margin: 0 }}>
@ -608,7 +608,7 @@ const MeetingProgressDisplay: React.FC<{
</Title> </Title>
</Space> </Space>
</Col> </Col>
<Col span={8}> <Col xs={24} md={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Title level={4} style={{ margin: 0 }}> <Title level={4} style={{ margin: 0 }}>
@ -616,7 +616,7 @@ const MeetingProgressDisplay: React.FC<{
</Title> </Title>
</Space> </Space>
</Col> </Col>
<Col span={8}> <Col xs={24} md={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}> <Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}>
@ -942,8 +942,8 @@ const UnifiedMeetingProgressDisplay: React.FC<{
{renderStageTimeline()} {renderStageTimeline()}
<Divider style={{ margin: '32px 0 24px' }} /> <Divider style={{ margin: '32px 0 24px' }} />
<Row gutter={24}> <Row gutter={[24, 16]}>
<Col span={8}> <Col xs={24} md={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Title level={4} style={{ margin: 0 }}> <Title level={4} style={{ margin: 0 }}>
@ -951,7 +951,7 @@ const UnifiedMeetingProgressDisplay: React.FC<{
</Title> </Title>
</Space> </Space>
</Col> </Col>
<Col span={8}> <Col xs={24} md={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}> <Title level={4} style={{ margin: 0, color: isError ? '#ff4d4f' : '#52c41a' }}>
@ -959,7 +959,7 @@ const UnifiedMeetingProgressDisplay: React.FC<{
</Title> </Title>
</Space> </Space>
</Col> </Col>
<Col span={8}> <Col xs={24} md={8}>
<Space direction="vertical" size={0}> <Space direction="vertical" size={0}>
<Text type="secondary"></Text> <Text type="secondary"></Text>
<Title level={4} style={{ margin: 0, color: '#4f46e5' }}> <Title level={4} style={{ margin: 0, color: '#4f46e5' }}>

View File

@ -456,7 +456,7 @@ export default function MeetingPointsManagement() {
columns={overviewColumns} columns={overviewColumns}
dataSource={overviewRows} dataSource={overviewRows}
loading={false} loading={false}
scroll={{ x: 900, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
pagination={false} pagination={false}
/> />
) : activeTabKey === "personal" ? ( ) : activeTabKey === "personal" ? (
@ -466,7 +466,7 @@ export default function MeetingPointsManagement() {
columns={personalAccountColumns} columns={personalAccountColumns}
dataSource={pagedPersonalAccounts} dataSource={pagedPersonalAccounts}
loading={false} loading={false}
scroll={{ x: 900, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
pagination={false} pagination={false}
/> />
) : ( ) : (
@ -476,7 +476,7 @@ export default function MeetingPointsManagement() {
columns={ledgerColumns} columns={ledgerColumns}
dataSource={records} dataSource={records}
loading={loading} loading={loading}
scroll={{ x: 1100, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
pagination={false} pagination={false}
/> />
)} )}

View File

@ -477,7 +477,14 @@
} }
.meetings-table__title-link { .meetings-table__title-link {
padding: 0;
border: 0;
background: transparent;
color: #1677ff;
font: inherit;
font-weight: 500; font-weight: 500;
cursor: pointer;
text-align: left;
} }
.meetings-table__source-tag.ant-tag { .meetings-table__source-tag.ant-tag {

View File

@ -700,7 +700,7 @@ const Meetings: React.FC = () => {
title: "会议标题", title: "会议标题",
dataIndex: "title", dataIndex: "title",
key: "title", key: "title",
render: (text: string, record: MeetingVO) => <a className="meetings-table__title-link" onClick={() => handleOpenMeeting(record)}>{text}</a>, render: (text: string, record: MeetingVO) => <button type="button" className="meetings-table__title-link" onClick={() => handleOpenMeeting(record)}>{text}</button>,
}, },
{ {
title: "状态", title: "状态",

View File

@ -368,15 +368,15 @@ const PromptTemplates: React.FC = () => {
return ( return (
<Space size={2} onClick={(e) => e.stopPropagation()}> <Space size={2} onClick={(e) => e.stopPropagation()}>
<Tooltip title="查看"> <Tooltip title="查看">
<Button type="text" size="small" icon={<EyeOutlined />} onClick={() => showDetail(item)} /> <Button type="text" size="small" icon={<EyeOutlined />} onClick={() => showDetail(item)} aria-label="查看模板" />
</Tooltip> </Tooltip>
{canEdit && ( {canEdit && (
<Tooltip title="编辑"> <Tooltip title="编辑">
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleOpenDrawer(item)} /> <Button type="text" size="small" icon={<EditOutlined />} onClick={() => handleOpenDrawer(item)} aria-label="编辑模板" />
</Tooltip> </Tooltip>
)} )}
<Tooltip title="以此创建"> <Tooltip title="以此创建">
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleOpenDrawer(item, true)} /> <Button type="text" size="small" icon={<CopyOutlined />} onClick={() => handleOpenDrawer(item, true)} aria-label="以此创建模板" />
</Tooltip> </Tooltip>
{canEdit && ( {canEdit && (
<Popconfirm <Popconfirm
@ -386,7 +386,7 @@ const PromptTemplates: React.FC = () => {
cancelText={t('common.cancel')} cancelText={t('common.cancel')}
> >
<Tooltip title="删除"> <Tooltip title="删除">
<Button type="text" size="small" danger icon={<DeleteOutlined />} /> <Button type="text" size="small" danger icon={<DeleteOutlined />} aria-label="删除模板" />
</Tooltip> </Tooltip>
</Popconfirm> </Popconfirm>
)} )}

View File

@ -89,7 +89,7 @@ export default function PublicDeviceMeetingCreate() {
</div> </div>
</Space> </Space>
<Card bordered={false} className="public-device-create-notice"> <Card variant="borderless" className="public-device-create-notice">
<Space direction="vertical" size={12}> <Space direction="vertical" size={12}>
<Text> <Text>
<CheckCircleOutlined className="public-device-create-check" /> <CheckCircleOutlined className="public-device-create-check" />

View File

@ -150,10 +150,15 @@
.screen-saver-crop-modal .ant-modal-content { .screen-saver-crop-modal .ant-modal-content {
overflow: hidden; overflow: hidden;
border-radius: 4px; border-radius: 4px;
max-height: calc(100vh - 48px);
display: flex;
flex-direction: column;
} }
.screen-saver-crop-modal .ant-modal-body { .screen-saver-crop-modal .ant-modal-body {
padding: 0; padding: 0;
min-height: 0;
overflow: auto;
} }
.screen-saver-crop-modal__layout { .screen-saver-crop-modal__layout {
@ -287,10 +292,13 @@
@media (max-width: 992px) { @media (max-width: 992px) {
.screen-saver-crop-modal__layout { .screen-saver-crop-modal__layout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
min-height: 0;
} }
.screen-saver-crop-modal__stage { .screen-saver-crop-modal__stage {
padding: 24px; padding: 24px;
border-right: none;
border-bottom: 1px solid #e6e6e6;
} }
.screen-saver-crop-modal__sidebar { .screen-saver-crop-modal__sidebar {
@ -311,4 +319,44 @@
.screen-saver-page .screen-saver-table-thumb { .screen-saver-page .screen-saver-table-thumb {
width: 88px; width: 88px;
} }
.screen-saver-crop-modal {
max-width: calc(100vw - 24px);
}
.screen-saver-crop-modal__stage {
padding: 20px;
}
.screen-saver-crop-modal__viewport {
width: min(100%, 360px);
}
.screen-saver-crop-modal__sidebar {
padding: 16px 20px 20px;
}
.screen-saver-crop-modal__footer {
flex-wrap: wrap;
}
}
@media (max-height: 680px) {
.screen-saver-crop-modal .ant-modal-content {
max-height: calc(100vh - 24px);
}
.screen-saver-crop-modal__layout {
min-height: 0;
}
.screen-saver-crop-modal__stage {
gap: 14px;
padding-top: 18px;
padding-bottom: 18px;
}
.screen-saver-crop-modal__viewport {
width: min(100%, 520px);
}
} }

View File

@ -707,9 +707,9 @@ export default function ScreenSaverManagement() {
} }
return ( return (
<Space size={4}> <Space size={4}>
<Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} /> <Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} aria-label="编辑屏保" title="编辑屏保" />
<Popconfirm title="确认删除该屏保吗?" onConfirm={() => void handleDelete(record)}> <Popconfirm title="确认删除该屏保吗?" onConfirm={() => void handleDelete(record)}>
<Button type="text" danger icon={<DeleteOutlined />} /> <Button type="text" danger icon={<DeleteOutlined />} aria-label="删除屏保" title="删除屏保" />
</Popconfirm> </Popconfirm>
</Space> </Space>
); );
@ -798,7 +798,7 @@ export default function ScreenSaverManagement() {
locale={{ locale={{
emptyText: <Empty description="暂无屏保素材" />, emptyText: <Empty description="暂无屏保素材" />,
}} }}
scroll={{ x: 1100, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
/> />
</DataListPanel> </DataListPanel>
</SectionCard> </SectionCard>

View File

@ -708,7 +708,7 @@ const SpeakerReg: React.FC = () => {
columns={columns} columns={columns}
dataSource={filteredSpeakers} dataSource={filteredSpeakers}
loading={listLoading} loading={listLoading}
scroll={{ x: 800, y: "100%" }} scroll={{ x: "max-content", y: "100%" }}
pagination={false} pagination={false}
/> />

View File

@ -11,6 +11,28 @@
background: transparent; background: transparent;
} }
.tenant-meeting-points__toolbar {
align-items: center;
}
.tenant-meeting-points__filters {
justify-content: flex-end;
max-width: 100%;
}
.tenant-meeting-points__filter {
width: var(--app-control-min-width, 180px);
min-width: 0;
}
.tenant-meeting-points__filter--wide {
width: var(--app-control-wide-width, 220px);
}
.tenant-meeting-points__list-panel .ant-table-cell {
white-space: nowrap;
}
.tenant-meeting-points__tenant-section { .tenant-meeting-points__tenant-section {
max-width: 100%; max-width: 100%;
} }
@ -47,6 +69,10 @@
gap: 12px; gap: 12px;
} }
.tenant-meeting-points__tenant-identity .ant-space {
min-width: 0;
}
.tenant-meeting-points__tenant-icon { .tenant-meeting-points__tenant-icon {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@ -69,6 +95,7 @@
.tenant-meeting-points__tenant-actions { .tenant-meeting-points__tenant-actions {
justify-content: flex-end; justify-content: flex-end;
min-width: 0;
} }
.tenant-meeting-points__mode-panel { .tenant-meeting-points__mode-panel {
@ -199,6 +226,17 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.tenant-meeting-points__toolbar {
align-items: stretch;
}
.tenant-meeting-points__filters,
.tenant-meeting-points__filters .ant-space-item,
.tenant-meeting-points__filter,
.tenant-meeting-points__filter--wide {
width: 100% !important;
}
.tenant-meeting-points__tenant-content { .tenant-meeting-points__tenant-content {
padding: 8px; padding: 8px;
} }

View File

@ -318,30 +318,32 @@ export default function TenantMeetingPointsSettings() {
description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。" description="按租户控制会议积分是否校验余额,关闭后按无限余额模式记录消耗。"
> >
<DataListPanel <DataListPanel
className="tenant-meeting-points__list-panel"
toolbarClassName="tenant-meeting-points__toolbar"
leftActions={ leftActions={
<Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}> <Button icon={<ReloadOutlined />} onClick={() => void handleRefresh()}>
</Button> </Button>
} }
rightActions={ rightActions={
<Space wrap> <Space wrap className="tenant-meeting-points__filters">
<Input <Input
className="tenant-meeting-points__filter tenant-meeting-points__filter--wide"
placeholder="按租户名称搜索" placeholder="按租户名称搜索"
value={params.tenantName} value={params.tenantName}
onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))} onChange={(event) => setParams((prev) => ({ ...prev, tenantName: event.target.value }))}
style={{ width: 220 }}
prefix={<SearchOutlined className="text-gray-400" />} prefix={<SearchOutlined className="text-gray-400" />}
allowClear allowClear
/> />
<Input <Input
className="tenant-meeting-points__filter"
placeholder="按租户编码搜索" placeholder="按租户编码搜索"
value={params.tenantCode} value={params.tenantCode}
onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))} onChange={(event) => setParams((prev) => ({ ...prev, tenantCode: event.target.value }))}
style={{ width: 180 }}
allowClear allowClear
/> />
<Select <Select
style={{ width: 180 }} className="tenant-meeting-points__filter"
value={params.balanceCheckEnabled} value={params.balanceCheckEnabled}
onChange={(value) => setParams((prev) => ({ ...prev, balanceCheckEnabled: value }))} onChange={(value) => setParams((prev) => ({ ...prev, balanceCheckEnabled: value }))}
options={[ options={[
@ -391,7 +393,7 @@ export default function TenantMeetingPointsSettings() {
dataSource={records} dataSource={records}
loading={loading} loading={loading}
totalCount={total} totalCount={total}
scroll={{x: 1200, y: "100%"}} scroll={{x: "max-content", y: "100%"}}
pagination={false} pagination={false}
/> />
</DataListPanel> </DataListPanel>

View File

@ -362,7 +362,7 @@ export default function Devices() {
open={open} open={open}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
width={420} width={420}
destroyOnClose destroyOnHidden
footer={ footer={
<div className="app-page__drawer-footer"> <div className="app-page__drawer-footer">
<Button onClick={() => setOpen(false)}>{t("common.cancel")}</Button> <Button onClick={() => setOpen(false)}>{t("common.cancel")}</Button>

View File

@ -19,15 +19,18 @@
.home-shell-card { .home-shell-card {
padding: 0; padding: 0;
min-height: 0;
} }
.home-shell-card .section-card__content { .home-shell-card .section-card__content {
padding: 0; padding: 0;
min-height: 0;
background: transparent; background: transparent;
} }
.home-data-panel { .home-data-panel {
padding: 0; padding: 0;
min-height: 0;
background: transparent; background: transparent;
} }
@ -47,8 +50,8 @@
position: relative; position: relative;
flex: 1; flex: 1;
height: 100%; height: 100%;
min-height: 100%; min-height: 0;
padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) 0; padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) clamp(28px, 5vh, 48px);
border-radius: 4px; border-radius: 4px;
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%); background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
color: @home-text-main; color: @home-text-main;
@ -102,6 +105,7 @@
width: 100%; width: 100%;
margin: 0 auto; margin: 0 auto;
flex: 1; flex: 1;
min-height: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: flex-start; justify-content: flex-start;
@ -323,6 +327,7 @@
.home-recent-section { .home-recent-section {
margin-top: 0; margin-top: 0;
padding-bottom: 4px;
} }
.home-section-header { .home-section-header {
@ -510,6 +515,106 @@
} }
} }
@media (max-height: 820px) {
.home-container {
padding-top: clamp(24px, 5vh, 40px);
padding-bottom: clamp(24px, 4vh, 36px);
.home-content-wrapper {
gap: clamp(18px, 3vh, 28px);
}
.home-title {
font-size: clamp(30px, 4vw, 48px) !important;
margin-bottom: clamp(22px, 4vh, 36px) !important;
}
.home-action-item {
padding: 24px 22px;
}
.home-action-item .home-action-icon-wrapper {
width: 60px;
height: 60px;
margin-bottom: 22px;
}
.home-action-item .home-action-title {
font-size: 22px !important;
margin-bottom: 12px !important;
}
.home-section-header {
margin-bottom: 16px;
}
.home-recent-card {
min-height: 124px;
padding: 16px 20px;
}
.home-recent-card .home-recent-card-footer {
margin-top: 12px;
}
}
}
@media (max-height: 680px) {
.home-container {
padding-top: 20px;
padding-bottom: 28px;
.home-content-wrapper {
gap: 16px;
}
.home-title {
font-size: clamp(28px, 3.6vw, 42px) !important;
margin-bottom: 18px !important;
}
.home-action-item {
padding: 20px 18px;
}
.home-action-item .home-action-icon-wrapper {
width: 52px;
height: 52px;
margin-bottom: 16px;
}
.home-action-item .home-action-title {
font-size: 20px !important;
margin-bottom: 8px !important;
}
.home-action-item .home-action-line {
font-size: 14px;
line-height: 1.4;
}
.home-recent-card {
min-height: 112px;
padding: 14px 18px;
}
}
}
@media (max-width: 768px) {
.home-container {
padding: 24px 18px 32px;
.home-title {
font-size: clamp(28px, 9vw, 40px) !important;
margin-bottom: 24px !important;
}
.home-recent-card {
min-height: 128px;
}
}
}
.home-container > * { .home-container > * {
animation: homeFadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both; animation: homeFadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
} }

View File

@ -195,7 +195,7 @@ export default function Orgs() {
</div> </div>
</SectionCard> </SectionCard>
<Drawer title={<Space><ApartmentOutlined aria-hidden="true" /><span>{editing ? t("orgs.drawerTitleEdit") : t("orgs.drawerTitleCreate")}</span></Space>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnClose footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}> <Drawer title={<Space><ApartmentOutlined aria-hidden="true" /><span>{editing ? t("orgs.drawerTitleEdit") : t("orgs.drawerTitleCreate")}</span></Space>} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={420} destroyOnHidden footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}>
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Form.Item label={t("users.tenant")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}> <Form.Item label={t("users.tenant")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
<Select disabled options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} /> <Select disabled options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} />
@ -210,13 +210,13 @@ export default function Orgs() {
<Form.Item label={t("orgs.orgCode")} name="orgCode"> <Form.Item label={t("orgs.orgCode")} name="orgCode">
<Input placeholder={t("orgsExt.orgCodePlaceholder")} className="tabular-nums" /> <Input placeholder={t("orgsExt.orgCodePlaceholder")} className="tabular-nums" />
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("dicts.sort")} name="sortOrder" initialValue={0}> <Form.Item label={t("dicts.sort")} name="sortOrder" initialValue={0}>
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" /> <InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("common.status")} name="status" initialValue={1}> <Form.Item label={t("common.status")} name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} /> <Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
</Form.Item> </Form.Item>

View File

@ -246,17 +246,17 @@ export default function Tenants() {
open={drawerOpen} open={drawerOpen}
onClose={() => setDrawerOpen(false)} onClose={() => setDrawerOpen(false)}
width={480} width={480}
destroyOnClose destroyOnHidden
footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>} footer={<div className="app-page__drawer-footer"><Button onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={submit}>{t("common.save")}</Button></div>}
> >
<Form form={form} layout="vertical"> <Form form={form} layout="vertical">
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("tenants.tenantName")} name="tenantName" rules={[{ required: true, message: t("tenants.tenantName") }]}> <Form.Item label={t("tenants.tenantName")} name="tenantName" rules={[{ required: true, message: t("tenants.tenantName") }]}>
<Input placeholder={t("tenantsExt.tenantNamePlaceholder")} /> <Input placeholder={t("tenantsExt.tenantNamePlaceholder")} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("tenants.tenantCode")} name="tenantCode" rules={[{ required: true, message: t("tenants.tenantCode") }]}> <Form.Item label={t("tenants.tenantCode")} name="tenantCode" rules={[{ required: true, message: t("tenants.tenantCode") }]}>
<Input placeholder={t("tenantsExt.tenantCodePlaceholder")} disabled={!!editing} className="tabular-nums" /> <Input placeholder={t("tenantsExt.tenantCodePlaceholder")} disabled={!!editing} className="tabular-nums" />
</Form.Item> </Form.Item>
@ -285,13 +285,13 @@ export default function Tenants() {
/> />
</Form.Item> </Form.Item>
)} )}
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("tenants.contactName")} name="contactName"> <Form.Item label={t("tenants.contactName")} name="contactName">
<Input placeholder={t("tenantsExt.contactNamePlaceholder")} /> <Input placeholder={t("tenantsExt.contactNamePlaceholder")} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("tenants.contactPhone")} name="contactPhone"> <Form.Item label={t("tenants.contactPhone")} name="contactPhone">
<Input placeholder={t("tenantsExt.contactPhonePlaceholder")} className="tabular-nums" /> <Input placeholder={t("tenantsExt.contactPhonePlaceholder")} className="tabular-nums" />
</Form.Item> </Form.Item>

View File

@ -7,6 +7,17 @@
padding: 0; padding: 0;
} }
.avatar-crop-modal .ant-modal-content {
max-height: calc(100vh - 48px);
display: flex;
flex-direction: column;
}
.avatar-crop-modal .ant-modal-body {
min-height: 0;
overflow: auto;
}
.avatar-crop-modal__layout { .avatar-crop-modal__layout {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.2fr) 320px; grid-template-columns: minmax(0, 1.2fr) 320px;
@ -116,3 +127,49 @@
background: #f8fafc; background: #f8fafc;
border-top: 1px solid #e6e6e6; border-top: 1px solid #e6e6e6;
} }
@media (max-width: 768px) {
.avatar-crop-modal {
max-width: calc(100vw - 24px);
}
.avatar-crop-modal__layout {
grid-template-columns: 1fr;
min-height: 0;
}
.avatar-crop-modal__stage {
gap: 20px;
padding: 24px 20px;
border-right: none;
border-bottom: 1px solid #e6e6e6;
}
.avatar-crop-modal__viewport {
width: min(300px, calc(100vw - 88px));
height: min(300px, calc(100vw - 88px));
}
.avatar-crop-modal__sidebar-card,
.avatar-crop-modal__footer {
padding-right: 20px;
padding-left: 20px;
}
}
@media (max-height: 680px) {
.avatar-crop-modal .ant-modal-content {
max-height: calc(100vh - 24px);
}
.avatar-crop-modal__stage {
gap: 16px;
padding-top: 20px;
padding-bottom: 20px;
}
.avatar-crop-modal__viewport {
width: 240px;
height: 240px;
}
}

View File

@ -195,3 +195,51 @@
.h-full { .h-full {
height: 100%; height: 100%;
} }
@media (max-width: 991px) {
.dictionaries-page > .page-container__body,
.dictionaries-page .section-card,
.dictionaries-page .section-card__content {
overflow-y: auto;
}
.dictionaries-layout {
flex: none;
height: auto;
overflow: visible;
row-gap: 12px;
}
.dictionaries-layout > .ant-col {
height: auto;
min-height: 360px;
}
.dictionaries-layout > .ant-col > .app-page__panel-card {
height: min(430px, calc(100vh - 160px));
min-height: 360px;
}
}
@media (max-width: 768px) {
.dictionaries-page {
padding: 0;
}
.dict-type-search-row {
align-items: stretch;
flex-direction: column;
}
.dict-type-search-row .ant-btn,
.dict-type-search-input,
.dict-type-search-input .ant-input,
.dict-type-search-input .ant-input-group-wrapper {
width: 100%;
}
.dictionaries-layout > .ant-col > .app-page__panel-card {
height: min(390px, calc(100vh - 150px));
min-height: 320px;
}
}

View File

@ -170,7 +170,7 @@ export default function Dictionaries() {
description={t("dicts.subtitle")} description={t("dicts.subtitle")}
> >
<Row gutter={16} className="dictionaries-layout"> <Row gutter={16} className="dictionaries-layout">
<Col span={8} className="h-full flex flex-col overflow-hidden"> <Col xs={24} lg={8} className="h-full flex flex-col overflow-hidden">
<Card title={<Space><BookOutlined aria-hidden="true"/><span>{t("dicts.dictType")}</span></Space>} <Card title={<Space><BookOutlined aria-hidden="true"/><span>{t("dicts.dictType")}</span></Space>}
className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{
body: { body: {
@ -221,8 +221,8 @@ export default function Dictionaries() {
<div className="dict-type-code text-xs text-gray-400 truncate tabular-nums">{record.typeCode}</div> <div className="dict-type-code text-xs text-gray-400 truncate tabular-nums">{record.typeCode}</div>
</div> </div>
<div className="dict-type-actions flex gap-1"> <div className="dict-type-actions flex gap-1">
{can("sys_dict:type:update") && <Button type="text" size="small" icon={<EditOutlined aria-hidden="true" style={{ fontSize: "12px" }} />} onClick={(event) => { event.stopPropagation(); handleEditType(record); }} />} {can("sys_dict:type:update") && <Button type="text" size="small" icon={<EditOutlined aria-hidden="true" style={{ fontSize: "12px" }} />} onClick={(event) => { event.stopPropagation(); handleEditType(record); }} aria-label={t("common.edit")} title={t("common.edit")} />}
{can("sys_dict:type:delete") && <Popconfirm title={t("dictsExt.deleteType", { name: record.typeName })} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={(event) => { event?.stopPropagation(); handleDeleteType(record.dictTypeId); }}><Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" style={{ fontSize: "12px" }} />} onClick={(event) => event.stopPropagation()} /></Popconfirm>} {can("sys_dict:type:delete") && <Popconfirm title={t("dictsExt.deleteType", { name: record.typeName })} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={(event) => { event?.stopPropagation(); handleDeleteType(record.dictTypeId); }}><Button type="text" size="small" danger icon={<DeleteOutlined aria-hidden="true" style={{ fontSize: "12px" }} />} onClick={(event) => event.stopPropagation()} aria-label={t("common.delete")} title={t("common.delete")} /></Popconfirm>}
</div> </div>
</div> </div>
) )
@ -233,7 +233,7 @@ export default function Dictionaries() {
</Card> </Card>
</Col> </Col>
<Col span={16} className="h-full flex flex-col overflow-hidden"> <Col xs={24} lg={16} className="h-full flex flex-col overflow-hidden">
<Card title={<Space><ProfileOutlined aria-hidden="true" /><span>{t("dicts.dictItem")}{selectedType ? ` - ${selectedType.typeName}` : ""}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 12, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:item:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddItem} disabled={!selectedType}>{t("common.create")}</Button>}> <Card title={<Space><ProfileOutlined aria-hidden="true" /><span>{t("dicts.dictItem")}{selectedType ? ` - ${selectedType.typeName}` : ""}</span></Space>} className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{ body: { padding: 12, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} extra={can("sys_dict:item:create") && <Button type="primary" size="small" icon={<PlusOutlined aria-hidden="true" />} onClick={handleAddItem} disabled={!selectedType}>{t("common.create")}</Button>}>
{selectedType ? ( {selectedType ? (
<div className="dict-item-table-wrap"> <div className="dict-item-table-wrap">
@ -281,7 +281,7 @@ export default function Dictionaries() {
<Drawer title={<Space><BookOutlined <Drawer title={<Space><BookOutlined
aria-hidden="true"/><span>{editingType ? t("dicts.drawerTitleTypeEdit") : t("dicts.drawerTitleTypeCreate")}</span></Space>} aria-hidden="true"/><span>{editingType ? t("dicts.drawerTitleTypeEdit") : t("dicts.drawerTitleTypeCreate")}</span></Space>}
open={typeDrawerVisible} onClose={closeTypeDrawer} width={400} destroyOnClose open={typeDrawerVisible} onClose={closeTypeDrawer} width={400} destroyOnHidden
footer={<div className="app-page__drawer-footer"><Button footer={<div className="app-page__drawer-footer"><Button
onClick={closeTypeDrawer}>{t("common.cancel")}</Button><Button type="primary" onClick={closeTypeDrawer}>{t("common.cancel")}</Button><Button type="primary"
onClick={handleTypeSubmit}>{t("common.save")}</Button> onClick={handleTypeSubmit}>{t("common.save")}</Button>
@ -301,7 +301,7 @@ export default function Dictionaries() {
<Drawer title={<Space><ProfileOutlined <Drawer title={<Space><ProfileOutlined
aria-hidden="true"/><span>{editingItem ? t("dicts.drawerTitleItemEdit") : t("dicts.drawerTitleItemCreate")}</span></Space>} aria-hidden="true"/><span>{editingItem ? t("dicts.drawerTitleItemEdit") : t("dicts.drawerTitleItemCreate")}</span></Space>}
open={itemDrawerVisible} onClose={closeItemDrawer} width={400} destroyOnClose open={itemDrawerVisible} onClose={closeItemDrawer} width={400} destroyOnHidden
footer={<div className="app-page__drawer-footer"><Button footer={<div className="app-page__drawer-footer"><Button
onClick={closeItemDrawer}>{t("common.cancel")}</Button><Button type="primary" onClick={closeItemDrawer}>{t("common.cancel")}</Button><Button type="primary"
onClick={handleItemSubmit}>{t("common.save")}</Button> onClick={handleItemSubmit}>{t("common.save")}</Button>

View File

@ -241,10 +241,10 @@ export default function SysParams() {
fixed: "right" as const, fixed: "right" as const,
render: (_: any, record: SysParamVO) => ( render: (_: any, record: SysParamVO) => (
<Space> <Space>
{can("sys_param:update") && <Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />} {can("sys_param:update") && <Button type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} aria-label={t("common.edit")} title={t("common.edit")} />}
{can("sys_param:delete") && record.isSystem !== 1 && ( {can("sys_param:delete") && record.isSystem !== 1 && (
<Popconfirm title={t("sysParamsExt.deleteConfirm")} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={() => handleDelete(record.paramId)}> <Popconfirm title={t("sysParamsExt.deleteConfirm")} okText={t("common.confirm")} cancelText={t("common.cancel")} onConfirm={() => handleDelete(record.paramId)}>
<Button type="text" danger icon={<DeleteOutlined />} /> <Button type="text" danger icon={<DeleteOutlined />} aria-label={t("common.delete")} title={t("common.delete")} />
</Popconfirm> </Popconfirm>
)} )}
</Space> </Space>
@ -309,7 +309,7 @@ export default function SysParams() {
open={drawerOpen} open={drawerOpen}
onClose={closeDrawer} onClose={closeDrawer}
width={500} width={500}
destroyOnClose destroyOnHidden
footer={<div className="app-page__drawer-footer"><Button footer={<div className="app-page__drawer-footer"><Button
onClick={closeDrawer}>{t("common.cancel")}</Button><Button type="primary" loading={saving} onClick={closeDrawer}>{t("common.cancel")}</Button><Button type="primary" loading={saving}
onClick={submit}>{t("common.save")}</Button></div>} onClick={submit}>{t("common.save")}</Button></div>}
@ -318,8 +318,8 @@ export default function SysParams() {
<Form.Item label={t("sysParams.paramKey")} name="paramKey" rules={[{ required: true, message: t("sysParams.paramKey") }]}> <Form.Item label={t("sysParams.paramKey")} name="paramKey" rules={[{ required: true, message: t("sysParams.paramKey") }]}>
<Input placeholder={t("sysParamsExt.paramKeyPlaceholder")} disabled={!!editing} /> <Input placeholder={t("sysParamsExt.paramKeyPlaceholder")} disabled={!!editing} />
</Form.Item> </Form.Item>
<Row gutter={16}> <Row gutter={16} className="app-responsive-form-row">
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("sysParams.paramType")} name="paramType" rules={[{ required: true, message: t("sysParams.paramType") }]}> <Form.Item label={t("sysParams.paramType")} name="paramType" rules={[{ required: true, message: t("sysParams.paramType") }]}>
<Select placeholder={t("sysParams.paramType")} options={paramTypeDict.map((item) => ({ label: item.itemLabel, value: item.itemValue }))} onChange={(value) => { <Select placeholder={t("sysParams.paramType")} options={paramTypeDict.map((item) => ({ label: item.itemLabel, value: item.itemValue }))} onChange={(value) => {
drawerForm.setFieldValue("paramValue", isType(value, "Boolean") ? "true" : ""); drawerForm.setFieldValue("paramValue", isType(value, "Boolean") ? "true" : "");
@ -327,7 +327,7 @@ export default function SysParams() {
}} /> }} />
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={12}> <Col xs={24} md={12}>
<Form.Item label={t("common.status")} name="status" initialValue={1}> <Form.Item label={t("common.status")} name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} /> <Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
</Form.Item> </Form.Item>