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}
width="min(960px, calc(100vw - 48px))"
forceRender
destroyOnClose={false}
destroyOnHidden={false}
placement="right"
closable={false}
footer={

View File

@ -121,6 +121,7 @@
min-width: 0;
min-height: 32px;
align-items: center;
justify-content: flex-end;
}
.data-list-panel__table-container {
@ -240,6 +241,22 @@
border-bottom-color: rgba(22, 119, 255, 0.18) !important;
}
@media (max-width: 1200px) {
.data-list-panel__toolbar {
align-items: flex-start;
flex-wrap: wrap;
}
.data-list-panel__right-actions {
flex: 1 1 420px;
}
.data-list-panel__right-actions .ant-space {
justify-content: flex-end;
row-gap: 8px;
}
}
@media (max-width: 768px) {
.data-list-panel {
padding: 12px;
@ -258,7 +275,15 @@
.data-list-panel__right-actions,
.data-list-panel__right-actions .ant-space,
.data-list-panel__right-actions .ant-input-affix-wrapper,
.data-list-panel__right-actions .ant-select {
.data-list-panel__right-actions .ant-input,
.data-list-panel__right-actions .ant-select,
.data-list-panel__right-actions .ant-picker,
.data-list-panel__right-actions .ant-input-number,
.data-list-panel__right-actions .ant-btn {
width: 100% !important;
}
.data-list-panel__right-actions .ant-space {
justify-content: flex-start;
}
}

View File

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

View File

@ -39,7 +39,7 @@ function ListTable<T extends Record<string, any>>({
showSizeChanger: { showSearch: false },
showQuickJumper: true,
},
scroll = { x: 1200 },
scroll = { x: "max-content" },
onRowClick,
selectedRow,
loading = false,
@ -49,7 +49,7 @@ function ListTable<T extends Record<string, any>>({
}: ListTableProps<T>) {
const mergedScroll = React.useMemo(() => {
return {
x: 1200,
x: "max-content",
...scroll,
};
}, [scroll]);
@ -95,9 +95,9 @@ function ListTable<T extends Record<string, any>>({
<span className="count-highlight">{totalCount || total}</span>
</span>
{onClearSelection && (
<a onClick={onClearSelection} className="selection-action">
<button type="button" onClick={onClearSelection} className="selection-action">
</a>
</button>
)}
</>
) : selectedRowKeys.length > 0 ? (
@ -106,14 +106,14 @@ function ListTable<T extends Record<string, any>>({
<span className="count-highlight">{selectedRowKeys.length}</span>
</span>
{onSelectAllPages && selectedRowKeys.length < (totalCount || total) && (
<a onClick={onSelectAllPages} className="selection-action">
<button type="button" onClick={onSelectAllPages} className="selection-action">
{totalCount || total}
</a>
</button>
)}
{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;
}
.section-card__title-wrap {
width: 100%;
max-width: 100%;
}
.section-card__title {
align-items: flex-start;
white-space: normal;
}
.section-card__title::before {
margin-top: 6px;
}
.section-card__description {
white-space: normal;
}
.section-card__extra {
width: 100%;
justify-content: flex-start;

View File

@ -33,6 +33,14 @@
--meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px);
--app-breakpoint-sm: 576px;
--app-breakpoint-md: 768px;
--app-breakpoint-lg: 992px;
--app-breakpoint-xl: 1200px;
--app-page-padding: 16px;
--app-page-padding-mobile: 12px;
--app-control-min-width: 160px;
--app-control-wide-width: 220px;
}
:root[data-theme="minimal"] {
@ -365,6 +373,15 @@ body::after {
padding: 8px 4px 4px;
}
.app-responsive-form-row {
width: 100%;
}
.app-responsive-form-row .ant-col,
.app-responsive-form-field {
min-width: 0;
}
.ant-drawer:has(.app-page__drawer-footer) .ant-drawer-content-wrapper,
.ant-drawer:has(.screen-saver-drawer__footer) .ant-drawer-content-wrapper,
.meeting-create-drawer-root .ant-drawer-content-wrapper {
@ -928,6 +945,15 @@ body::after {
.app-page__content-toolbar-filters {
justify-content: flex-start;
}
.app-page__content-toolbar .ant-space,
.app-page__content-toolbar .ant-input,
.app-page__content-toolbar .ant-input-affix-wrapper,
.app-page__content-toolbar .ant-select,
.app-page__content-toolbar .ant-picker,
.app-page__content-toolbar .ant-input-number {
width: 100% !important;
}
}
@ -1351,6 +1377,14 @@ body::after {
--meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px);
--app-breakpoint-sm: 576px;
--app-breakpoint-md: 768px;
--app-breakpoint-lg: 992px;
--app-breakpoint-xl: 1200px;
--app-page-padding: 16px;
--app-page-padding-mobile: 12px;
--app-control-min-width: 160px;
--app-control-wide-width: 220px;
}
:root[data-theme="tech"] {
@ -1385,6 +1419,14 @@ body::after {
--meeting-progress-color: #1677ff;
--app-form-drawer-width: 600px;
--app-form-drawer-max-width: calc(100vw - 48px);
--app-breakpoint-sm: 576px;
--app-breakpoint-md: 768px;
--app-breakpoint-lg: 992px;
--app-breakpoint-xl: 1200px;
--app-page-padding: 16px;
--app-page-padding-mobile: 12px;
--app-control-min-width: 160px;
--app-control-wide-width: 220px;
}
html,

View File

@ -54,6 +54,11 @@
gap: 16px;
}
.mobile-menu-trigger {
display: none;
flex: 0 0 auto;
}
.main-sider {
position: relative;
height: calc(100vh - 64px);
@ -270,6 +275,106 @@
background: var(--app-bg-layout);
}
.main-sider-backdrop {
display: none;
}
@media (max-width: 768px) {
.main-header {
gap: 8px;
padding: 0 12px;
}
.mobile-menu-trigger {
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--app-text-main);
}
.app-header-logo {
flex: 1 1 auto;
gap: 8px;
}
.app-header-logo img {
height: 32px;
max-width: 120px;
}
.app-header-logo-title {
max-width: 120px;
font-size: 16px;
}
.header-right {
flex: 0 0 auto;
min-width: 0;
}
.header-right > .ant-space {
gap: 12px !important;
}
.header-right .ant-avatar + span {
display: none;
}
.main-shell {
position: relative;
}
.main-sider {
position: fixed !important;
top: 64px;
bottom: 0;
left: 0;
z-index: 30;
width: 180px !important;
min-width: 180px !important;
max-width: 180px !important;
height: calc(100vh - 64px);
border-top: none;
box-shadow: 8px 0 24px rgba(15, 23, 42, 0.16);
transform: translateX(-100%);
transition: transform 0.24s ease;
}
.main-layout--mobile-sider-open .main-sider {
transform: translateX(0);
}
.main-layout--mobile .main-sider--collapsed {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
flex: 0 0 0 !important;
}
.main-layout--mobile .main-sider .collapsedCenter {
display: none;
}
.main-sider .ant-layout-sider-children {
height: calc(100vh - 64px);
}
.main-layout--mobile-sider-open .main-sider-backdrop {
position: fixed;
inset: 64px 0 0;
z-index: 25;
display: block;
padding: 0;
border: 0;
background: rgba(15, 23, 42, 0.28);
}
.main-content-layout {
flex: 1 1 auto;
width: 100%;
}
}
:root[data-theme="tech"] .main-header,
:root[data-theme="tech"] .main-sider,
:root[data-theme="tech"] .main-menu,

View File

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

View File

@ -40,7 +40,7 @@ import "./index.less";
const { Text } = Typography;
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> = {
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 })}>
<Row gutter={16}>
<Row gutter={16} className="app-responsive-form-row">
<Col span={24}>
<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")} />
@ -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") }]}>
<Input placeholder={t("permissionsExt.codePlaceholder")} className="tabular-nums" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item label={t("permissions.path")} name="path">
<Input placeholder={t("permissionsExt.pathPlaceholder")} className="tabular-nums" />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<Form.Item label={t("permissions.component")} name="component">
<Input placeholder={t("permissionsExt.componentPlaceholder")} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Row gutter={16} className="app-responsive-form-row">
<Col span={24}>
<Form.Item label={t("permissions.icon")}>
<Form.Item name="icon" noStyle>
@ -712,20 +712,20 @@ export default function Permissions() {
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item label={t("permissions.sort")} name="sortOrder" initialValue={0}>
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item label={t("permissions.isVisible")} name="isVisible" initialValue={1}>
<Select options={visibleDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<Form.Item label={t("common.status")} name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ value: Number(item.itemValue), label: item.itemLabel }))} />
</Form.Item>

View File

@ -376,3 +376,77 @@
border: 1px dashed #cccccc;
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">
<Row gutter={8} className="roles-layout__row">
<Col span={7} className="roles-layout__side">
<Col xs={24} lg={7} className="roles-layout__side">
<Card
title={<Space><ApartmentOutlined /><span>{"角色列表"}</span></Space>}
bordered={false}
variant="borderless"
className="app-page__panel-card roles-side-card"
>
<div className="role-search-panel">
@ -493,11 +493,11 @@ export default function Roles() {
<div className="role-item-actions">
<Space size={4}>
<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>
{item.roleCode !== "ADMIN" && (
<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>
)}
</Space>
@ -514,116 +514,133 @@ export default function Roles() {
</Card>
</Col>
<Col span={17} className="roles-layout__detail">
<Col xs={24} lg={17} className="roles-layout__detail">
{selectedRole ? (
<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>}
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.TabPane tab={<Space><KeyOutlined />{"功能权限"}</Space>} key="permissions">
<div className="role-detail-pane">
<div className="permission-tree-wrapper">
<Tree
checkable
selectable={false}
checkStrictly={false}
treeData={permissionTreeData}
checkedKeys={selectedPermIds}
onCheck={(keys, info) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
const halfChecked = info.halfCheckedKeys || [];
setSelectedPermIds(checked.map((key) => Number(key)));
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
setPermissionsDirty(true);
}}
defaultExpandAll
/>
</div>
</div>
</Tabs.TabPane>
<Tabs.TabPane tab={<Space><ApartmentOutlined />{"数据权限"}</Space>} key="dataScope">
<div className="role-detail-pane">
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={dataScopeType}
onChange={(event) => {
setDataScopeType(event.target.value);
setDataScopeDirty(true);
}}
optionType="button"
buttonStyle="solid"
>
{DATA_SCOPE_OPTIONS.map((item) => (
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
))}
</Radio.Group>
</div>
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
{dataScopeType === "CUSTOM" ? (
<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
<Tabs
activeKey={activeTab}
onChange={(key) => setActiveTab(key as RoleTabKey)}
className="role-detail-tabs"
items={[
{
key: "permissions",
label: <Space><KeyOutlined />{"功能权限"}</Space>,
children: (
<div className="role-detail-pane">
<div className="permission-tree-wrapper">
<Tree
checkable
selectable={false}
checkStrictly={false}
treeData={permissionTreeData}
checkedKeys={selectedPermIds}
onCheck={(keys, info) => {
const checked = Array.isArray(keys) ? keys : keys.checked;
const halfChecked = info.halfCheckedKeys || [];
setSelectedPermIds(checked.map((key) => Number(key)));
setHalfCheckedIds(halfChecked.map((key) => Number(key)));
setPermissionsDirty(true);
}}
defaultExpandAll
/>
</div>
</div>
),
},
{
key: "dataScope",
label: <Space><ApartmentOutlined />{"数据权限"}</Space>,
children: (
<div className="role-detail-pane">
<div style={{ marginBottom: 16 }}>
<Radio.Group
value={dataScopeType}
onChange={(event) => {
setDataScopeType(event.target.value);
setDataScopeDirty(true);
}}
optionType="button"
buttonStyle="solid"
>
{DATA_SCOPE_OPTIONS.map((item) => (
<Radio.Button key={item.value} value={item.value}>{item.label}</Radio.Button>
))}
</Radio.Group>
</div>
<div style={{ marginBottom: 16, color: "#64748b" }}>{getDataScopeDescription(dataScopeType)}</div>
{dataScopeType === "CUSTOM" ? (
<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>
) : (
<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>
) : (
<div className="app-page__empty-state"><Empty description="请选择左侧角色查看详情" /></div>
@ -634,14 +651,14 @@ export default function Roles() {
</DataListPanel>
</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 }}>
<Input placeholder="搜索用户名或显示名称" prefix={<SearchOutlined />} value={userSearchText} onChange={(event) => setUserSearchText(event.target.value)} allowClear />
</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" }]} />
</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.Item label="租户" name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
<Select options={tenants.map((tenant) => ({ label: tenant.tenantName, value: tenant.id }))} disabled={!!editing} />

View File

@ -552,7 +552,7 @@ export default function Users() {
</SectionCard>
<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
onClick={() => setDrawerOpen(false)}>{t("common.cancel")}</Button><Button type="primary"
loading={saving}
@ -560,8 +560,8 @@ export default function Users() {
</div>}>
<Form form={form} layout="vertical" className="user-form">
<Title level={5} style={{ marginBottom: 16 }}>{t("usersExt.basicInfo")}</Title>
<Row gutter={16}>
<Col span={12}><Form.Item label={t("users.username")} name="username" rules={[{
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}><Form.Item label={t("users.username")} name="username" rules={[{
required: true,
message: t("users.username")
}, {
@ -571,17 +571,17 @@ export default function Users() {
extra={t("usersExt.usernameFormatTip", {defaultValue: "登录名只能输入数字、小写英文、@ 和 _"})}><Input
placeholder={t("usersExt.usernamePlaceholder", {defaultValue: "仅支持 a-z、0-9、@、_"})} disabled={!!editing}
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
placeholder={t("users.displayName")}/></Form.Item></Col>
</Row>
<Row gutter={16}>
<Col span={12}><Form.Item label={t("users.email")} name="email"
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}><Form.Item label={t("users.email")} name="email"
rules={[{required: true, message: "请输入邮箱地址"}, {
type: "email",
message: "请输入正确的邮箱格式"
}]}><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>
</Row>
<Form.Item label={t("profile.avatarUrl")} name="avatarUrl"><Input
@ -610,13 +610,13 @@ export default function Users() {
<>
{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)} />}>
<Row gutter={12}>
<Col span={12}>
<Row gutter={12} className="app-responsive-form-row">
<Col xs={24} md={12}>
<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")} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<MembershipOrgSelect fieldProps={{ ...restField }} name={name} tenantId={form.getFieldValue(["memberships", name, "tenantId"])} />
</Col>
</Row>
@ -683,8 +683,8 @@ export default function Users() {
<Input.Password placeholder={t("usersExt.confirmPasswordPlaceholder")} autoComplete="new-password" />
</Form.Item>
) : null}
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item
label={t("common.status")}
name="status"
@ -697,7 +697,7 @@ export default function Users() {
</Form.Item>
</Col>
{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>
</Form>
</Drawer>
@ -712,7 +712,7 @@ export default function Users() {
}}
onOk={() => void submitResetPassword()}
confirmLoading={resetPasswordLoading}
destroyOnClose
destroyOnHidden
>
<Form form={resetPasswordForm} layout="vertical">
<Form.Item

View File

@ -22,7 +22,8 @@
margin: 0 8px 8px;
}
.role-permission-layout > .ant-col {
.role-permission-layout > .ant-col,
.role-permission-layout__pane {
height: 100%;
min-height: 0;
}
@ -34,3 +35,42 @@
.role-permission-layout .full-height-card .ant-card-body {
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>
<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">
<div className="mb-4">
<Input
@ -247,7 +247,7 @@ export default function RolePermissionBinding() {
</Card>
</Col>
<Col xs={24} lg={14} style={{ height: "100%" }}>
<Col xs={24} lg={14} className="role-permission-layout__pane">
<Card
title={<Space><KeyOutlined aria-hidden="true" /><span>{t("rolePerm.permConfig")}</span></Space>}
className="app-page__panel-card full-height-card"

View File

@ -22,7 +22,8 @@
margin: 0 8px 8px;
}
.user-role-layout > .ant-col {
.user-role-layout > .ant-col,
.user-role-layout__pane {
height: 100%;
min-height: 0;
}
@ -34,3 +35,42 @@
.user-role-layout .full-height-card .ant-card-body {
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>
<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">
<div className="mb-4">
<Input
@ -167,7 +167,7 @@ export default function UserRoleBinding() {
</Card>
</Col>
<Col xs={24} lg={12} style={{ height: "100%" }}>
<Col xs={24} lg={12} className="user-role-layout__pane">
<Card
title={<Space><TeamOutlined aria-hidden="true" /><span>{t("userRole.grantRoles")}</span></Space>}
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}>
<Row gutter={[16, 16]}>
{roles.map((role) => (
<Col key={role.roleId} span={12}>
<Col key={role.roleId} xs={24} md={12}>
<Checkbox value={role.roleId} className="w-full">
<Space direction="vertical" size={0}>
<span style={{ fontWeight: 500 }}>{role.roleName}</span>

View File

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

View File

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

View File

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

View File

@ -17,6 +17,19 @@
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 {
margin-bottom: 20px;
}

View File

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

View File

@ -320,16 +320,30 @@
}
@media (max-width: 992px) {
.hotwords-page > .page-container__body,
.hotwords-page .section-card,
.hotwords-page .section-card__content {
overflow-y: auto;
}
.hotwords-layout {
flex: none;
flex-direction: column;
overflow: auto;
overflow: visible;
}
.hotwords-group-panel.ant-card {
width: 100%;
max-width: none;
height: min(430px, calc(100vh - 170px));
min-height: 360px;
}
.hotwords-list-panel {
flex: none;
height: min(560px, calc(100vh - 150px));
min-height: 430px;
}
}
@media (max-width: 768px) {
@ -340,4 +354,14 @@
.hotwords-list-panel .data-list-panel__right-actions .ant-select {
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;
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}
loading={loading}
pagination={false}
scroll={{ x: 1000, y: "100%" }}
scroll={{ x: "max-content", y: "100%" }}
/>
</DataListPanel>
</SectionCard>

View File

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

View File

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

View File

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

View File

@ -700,7 +700,7 @@ const Meetings: React.FC = () => {
title: "会议标题",
dataIndex: "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: "状态",

View File

@ -368,15 +368,15 @@ const PromptTemplates: React.FC = () => {
return (
<Space size={2} onClick={(e) => e.stopPropagation()}>
<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>
{canEdit && (
<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 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>
{canEdit && (
<Popconfirm
@ -386,7 +386,7 @@ const PromptTemplates: React.FC = () => {
cancelText={t('common.cancel')}
>
<Tooltip title="删除">
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
<Button type="text" size="small" danger icon={<DeleteOutlined />} aria-label="删除模板" />
</Tooltip>
</Popconfirm>
)}

View File

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

View File

@ -150,10 +150,15 @@
.screen-saver-crop-modal .ant-modal-content {
overflow: hidden;
border-radius: 4px;
max-height: calc(100vh - 48px);
display: flex;
flex-direction: column;
}
.screen-saver-crop-modal .ant-modal-body {
padding: 0;
min-height: 0;
overflow: auto;
}
.screen-saver-crop-modal__layout {
@ -287,10 +292,13 @@
@media (max-width: 992px) {
.screen-saver-crop-modal__layout {
grid-template-columns: 1fr;
min-height: 0;
}
.screen-saver-crop-modal__stage {
padding: 24px;
border-right: none;
border-bottom: 1px solid #e6e6e6;
}
.screen-saver-crop-modal__sidebar {
@ -311,4 +319,44 @@
.screen-saver-page .screen-saver-table-thumb {
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 (
<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)}>
<Button type="text" danger icon={<DeleteOutlined />} />
<Button type="text" danger icon={<DeleteOutlined />} aria-label="删除屏保" title="删除屏保" />
</Popconfirm>
</Space>
);
@ -798,7 +798,7 @@ export default function ScreenSaverManagement() {
locale={{
emptyText: <Empty description="暂无屏保素材" />,
}}
scroll={{ x: 1100, y: "100%" }}
scroll={{ x: "max-content", y: "100%" }}
/>
</DataListPanel>
</SectionCard>

View File

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

View File

@ -11,6 +11,28 @@
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 {
max-width: 100%;
}
@ -47,6 +69,10 @@
gap: 12px;
}
.tenant-meeting-points__tenant-identity .ant-space {
min-width: 0;
}
.tenant-meeting-points__tenant-icon {
display: inline-flex;
align-items: center;
@ -69,6 +95,7 @@
.tenant-meeting-points__tenant-actions {
justify-content: flex-end;
min-width: 0;
}
.tenant-meeting-points__mode-panel {
@ -199,6 +226,17 @@
}
@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 {
padding: 8px;
}

View File

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

View File

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

View File

@ -19,15 +19,18 @@
.home-shell-card {
padding: 0;
min-height: 0;
}
.home-shell-card .section-card__content {
padding: 0;
min-height: 0;
background: transparent;
}
.home-data-panel {
padding: 0;
min-height: 0;
background: transparent;
}
@ -47,8 +50,8 @@
position: relative;
flex: 1;
height: 100%;
min-height: 100%;
padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) 0;
min-height: 0;
padding: clamp(40px, 8vh, 80px) clamp(24px, 5vw, 60px) clamp(28px, 5vh, 48px);
border-radius: 4px;
background: linear-gradient(135deg, #ffffff 0%, #f1f5f9 100%);
color: @home-text-main;
@ -102,6 +105,7 @@
width: 100%;
margin: 0 auto;
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
justify-content: flex-start;
@ -322,7 +326,8 @@
}
.home-recent-section {
margin-top: 0;
margin-top: 0;
padding-bottom: 4px;
}
.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 > * {
animation: homeFadeUp 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}

View File

@ -195,7 +195,7 @@ export default function Orgs() {
</div>
</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.Item label={t("users.tenant")} name="tenantId" rules={[{ required: true }]} hidden={!isPlatformMode}>
<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">
<Input placeholder={t("orgsExt.orgCodePlaceholder")} className="tabular-nums" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item label={t("dicts.sort")} name="sortOrder" initialValue={0}>
<InputNumber style={{ width: "100%" }} min={0} className="tabular-nums" />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<Form.Item label={t("common.status")} name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
</Form.Item>

View File

@ -246,17 +246,17 @@ export default function Tenants() {
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
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>}
>
<Form form={form} layout="vertical">
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item label={t("tenants.tenantName")} name="tenantName" rules={[{ required: true, message: t("tenants.tenantName") }]}>
<Input placeholder={t("tenantsExt.tenantNamePlaceholder")} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<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" />
</Form.Item>
@ -285,13 +285,13 @@ export default function Tenants() {
/>
</Form.Item>
)}
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<Form.Item label={t("tenants.contactName")} name="contactName">
<Input placeholder={t("tenantsExt.contactNamePlaceholder")} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<Form.Item label={t("tenants.contactPhone")} name="contactPhone">
<Input placeholder={t("tenantsExt.contactPhonePlaceholder")} className="tabular-nums" />
</Form.Item>

View File

@ -7,6 +7,17 @@
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 {
display: grid;
grid-template-columns: minmax(0, 1.2fr) 320px;
@ -116,3 +127,49 @@
background: #f8fafc;
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 {
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")}
>
<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>}
className="app-page__panel-card flex-1 flex flex-col overflow-hidden" styles={{
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>
<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: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: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()} aria-label={t("common.delete")} title={t("common.delete")} /></Popconfirm>}
</div>
</div>
)
@ -233,7 +233,7 @@ export default function Dictionaries() {
</Card>
</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>}>
{selectedType ? (
<div className="dict-item-table-wrap">
@ -281,7 +281,7 @@ export default function Dictionaries() {
<Drawer title={<Space><BookOutlined
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
onClick={closeTypeDrawer}>{t("common.cancel")}</Button><Button type="primary"
onClick={handleTypeSubmit}>{t("common.save")}</Button>
@ -301,7 +301,7 @@ export default function Dictionaries() {
<Drawer title={<Space><ProfileOutlined
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
onClick={closeItemDrawer}>{t("common.cancel")}</Button><Button type="primary"
onClick={handleItemSubmit}>{t("common.save")}</Button>

View File

@ -241,10 +241,10 @@ export default function SysParams() {
fixed: "right" as const,
render: (_: any, record: SysParamVO) => (
<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 && (
<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>
)}
</Space>
@ -309,7 +309,7 @@ export default function SysParams() {
open={drawerOpen}
onClose={closeDrawer}
width={500}
destroyOnClose
destroyOnHidden
footer={<div className="app-page__drawer-footer"><Button
onClick={closeDrawer}>{t("common.cancel")}</Button><Button type="primary" loading={saving}
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") }]}>
<Input placeholder={t("sysParamsExt.paramKeyPlaceholder")} disabled={!!editing} />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Row gutter={16} className="app-responsive-form-row">
<Col xs={24} md={12}>
<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) => {
drawerForm.setFieldValue("paramValue", isType(value, "Boolean") ? "true" : "");
@ -327,7 +327,7 @@ export default function SysParams() {
}} />
</Form.Item>
</Col>
<Col span={12}>
<Col xs={24} md={12}>
<Form.Item label={t("common.status")} name="status" initialValue={1}>
<Select options={statusDict.map((item) => ({ label: item.itemLabel, value: Number(item.itemValue) }))} />
</Form.Item>