feat: 优化响应布局

dev_na
puz 2026-07-07 09:13:19 +08:00
parent b06b7585d8
commit c0b4c0d319
20 changed files with 187 additions and 84 deletions

View File

@ -39,7 +39,7 @@ function ListTable<T extends Record<string, any>>({
showSizeChanger: { showSearch: false },
showQuickJumper: true,
},
scroll = { x: "max-content" },
scroll = { x: "max(100%, 960px)" },
onRowClick,
selectedRow,
loading = false,
@ -49,7 +49,7 @@ function ListTable<T extends Record<string, any>>({
}: ListTableProps<T>) {
const mergedScroll = React.useMemo(() => {
return {
x: "max-content",
x: "max(100%, 960px)",
...scroll,
};
}, [scroll]);

View File

@ -0,0 +1,28 @@
.long-text-preview {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
.long-text-preview-popover {
max-width: min(720px, 70vw);
}
.long-text-preview-popover__content {
max-width: min(680px, 66vw);
max-height: 360px;
margin: 0 !important;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
user-select: text;
line-height: 1.6;
}
.long-text-preview-popover__content--code {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 12px;
}

View File

@ -0,0 +1,47 @@
import { Popover, Typography } from "antd";
import type { TextProps } from "antd/es/typography/Text";
import "./LongTextPreview.css";
const { Paragraph, Text } = Typography;
type LongTextPreviewProps = {
value?: string | number | null;
emptyText?: string;
code?: boolean;
copyable?: boolean;
className?: string;
textType?: TextProps["type"];
strong?: boolean;
};
export default function LongTextPreview({
value,
emptyText = "-",
code = false,
copyable = true,
className,
textType,
strong,
}: LongTextPreviewProps) {
const text = value === undefined || value === null || value === "" ? emptyText : String(value);
const contentClassName = [
"long-text-preview-popover__content",
code ? "long-text-preview-popover__content--code" : undefined,
].filter(Boolean).join(" ");
return (
<Popover
overlayClassName="long-text-preview-popover"
placement="topLeft"
content={
<Paragraph className={contentClassName} copyable={copyable ? { text } : false}>
{text}
</Paragraph>
}
>
<Text code={code} type={textType} strong={strong} className={["long-text-preview", className].filter(Boolean).join(" ")}>
{text}
</Text>
</Popover>
);
}

View File

@ -88,3 +88,26 @@
}
}
}
.permissions-route-text {
font-size: 12px;
}
.permissions-route-text--component {
font-size: 11px;
}
.permissions-name-cell {
display: flex;
width: 100%;
min-width: 0;
}
.permissions-name-cell .ant-space-item:last-child {
flex: 1;
min-width: 0;
}
.permissions-name-cell .long-text-preview {
min-width: 0;
}

View File

@ -35,6 +35,7 @@ import { usePermission } from "@/hooks/usePermission";
import PageContainer from "@/components/shared/PageContainer";
import DataListPanel from "@/components/shared/DataListPanel";
import SectionCard from "@/components/shared/SectionCard";
import LongTextPreview from "@/components/shared/LongTextPreview";
import type { SysPermission } from "@/types";
import "./index.less";
@ -453,18 +454,22 @@ export default function Permissions() {
title: t("permissions.permName"),
dataIndex: "name",
key: "name",
width: 360,
ellipsis: { showTitle: false },
render: (text: string, record: TreePermission) => {
let icon = <CheckSquareOutlined style={{ color: "#52c41a" }} aria-hidden="true" />;
if (record.permType === "directory") icon = <FolderOutlined style={{ color: "#faad14" }} aria-hidden="true" />;
if (record.permType === "menu") icon = <MenuOutlined style={{ color: "#1890ff" }} aria-hidden="true" />;
return <Space>{icon}<Text strong={record.level === 1}>{text}</Text></Space>;
return <Space className="permissions-name-cell">{icon}<LongTextPreview value={text} strong={record.level === 1} copyable={false} /></Space>;
}
},
{
title: t("permissions.permCode"),
dataIndex: "code",
key: "code",
render: (text: string) => text ? <Tag color="blue" className="tabular-nums">{text}</Tag> : "-"
width: 220,
ellipsis: { showTitle: false },
render: (text: string) => text ? <LongTextPreview value={text} code className="tabular-nums" /> : "-"
},
{
title: t("permissions.permType"),
@ -478,15 +483,16 @@ export default function Permissions() {
return <Tag color={color}>{item ? item.itemLabel : type}</Tag>;
}
},
{ title: t("permissions.sort"), dataIndex: "sortOrder", width: 80, className: "tabular-nums" },
{ title: t("permissions.sort"), dataIndex: "sortOrder", width: 72, className: "tabular-nums" },
{
title: t("permissions.route"),
key: "route",
ellipsis: true,
width: 220,
ellipsis: { showTitle: false },
render: (_: unknown, record: TreePermission) => (
<div className="flex flex-col">
{record.path && <Text type="secondary" style={{ fontSize: "12px" }} className="tabular-nums">{record.path}</Text>}
{record.component && <Text type="secondary" style={{ fontSize: "11px" }}>{record.component}</Text>}
{record.path && <LongTextPreview value={record.path} textType="secondary" className="tabular-nums permissions-route-text" />}
{record.component && <LongTextPreview value={record.component} textType="secondary" className="permissions-route-text permissions-route-text--component" />}
</div>
)
},
@ -510,7 +516,7 @@ export default function Permissions() {
},
{
title: t("common.action"),
width: 150,
width: 132,
fixed: "right" as const,
render: (_: unknown, record: TreePermission) => (
<Space>
@ -583,7 +589,8 @@ export default function Permissions() {
columns={columns}
pagination={false}
size="middle"
scroll={{ x: 'max-content', y: '100%' }}
scroll={{ x: 'max(100%, 1080px)', y: '100%' }}
tableLayout="fixed"
expandable={{
defaultExpandAllRows: false,
rowExpandable: (record) => record.permType !== "button" && !!record.children?.length,

View File

@ -590,7 +590,7 @@ const AiModels: React.FC = () => {
columns={resolvedTableColumns}
dataSource={data}
loading={loading}
scroll={{ x: "max-content", y: "100%" }}
scroll={{ x: "max(100%, 960px)", y: "100%" }}
pagination={false}
/>
</DataListPanel>

View File

@ -473,7 +473,7 @@ export default function ClientManagement() {
columns={columns}
dataSource={pagedRecords}
loading={loading || groupLoading || platformLoading}
scroll={{ x: "max-content", y: "100%" }}
scroll={{ x: "max(100%, 960px)", y: "100%" }}
pagination={false}
/>
</DataListPanel>

View File

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

View File

@ -559,7 +559,7 @@ const HotWords: React.FC = () => {
dataSource={data}
rowKey="id"
loading={loading}
scroll={{ x: "max-content", y: "100%" }}
scroll={{ x: "max(100%, 1000px)", y: "100%" }}
pagination={false}
/>
</DataListPanel>

View File

@ -8,6 +8,7 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import SectionCard from "@/components/shared/SectionCard";
import SummaryStatCards from "@/components/shared/SummaryStatCards";
import LongTextPreview from "@/components/shared/LongTextPreview";
import { listLicenses, type LicenseVO } from "@/api/business/license";
import "./LicenseManagement.css";
@ -105,9 +106,10 @@ export default function LicenseManagement() {
title: "授权标识",
key: "license",
width: 320,
ellipsis: { showTitle: false },
render: (_, record) => (
<Space direction="vertical" size={0}>
<Text strong className="tabular-nums">{record.licenseSerial}</Text>
<Space direction="vertical" size={0} style={{ width: "100%" }}>
<LongTextPreview value={record.licenseSerial} strong className="tabular-nums" />
{/*<Text type="secondary" className="tabular-nums">{record.licenseCode}</Text>*/}
</Space>
),
@ -131,7 +133,8 @@ export default function LicenseManagement() {
dataIndex: "deviceCode",
key: "deviceCode",
width: 240,
render: (value) => <Text className="tabular-nums">{value || "-"}</Text>,
ellipsis: { showTitle: false },
render: (value) => <LongTextPreview value={value} className="tabular-nums" />,
},
{
title: "过期时间",
@ -184,7 +187,7 @@ export default function LicenseManagement() {
dataSource={pagedRecords}
loading={loading}
pagination={false}
scroll={{ x: "max-content", y: "100%" }}
scroll={{ x: "max(100%, 960px)", y: "100%" }}
/>
</DataListPanel>
</SectionCard>

View File

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

View File

@ -923,7 +923,7 @@ const Meetings: React.FC = () => {
rowKey="id"
loading={loading}
pagination={false}
scroll={{ x: "max-content", y: "calc(100vh - 430px)" }}
scroll={{ x: "max(100%, 1180px)", y: "calc(100vh - 430px)" }}
onRow={(record) => ({ onClick: () => handleOpenMeeting(record) })}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="开启您的第一场会议分析" /> }}
/>

View File

@ -457,7 +457,7 @@ const PromptTemplates: React.FC = () => {
rowKey="id"
loading={loading}
pagination={false}
scroll={{ x: "max-content", y: "100%" }}
scroll={{ x: "max(100%, 1400px)", y: "100%" }}
onRow={(record) => ({ onClick: () => showDetail(record) })}
locale={{ emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无可用模板" /> }}
/>

View File

@ -798,7 +798,7 @@ export default function ScreenSaverManagement() {
locale={{
emptyText: <Empty description="暂无屏保素材" />,
}}
scroll={{ x: "max-content", y: "100%" }}
scroll={{ x: "max(100%, 1080px)", y: "100%" }}
/>
</DataListPanel>
</SectionCard>

View File

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

View File

@ -393,7 +393,7 @@ export default function TenantMeetingPointsSettings() {
dataSource={records}
loading={loading}
totalCount={total}
scroll={{x: "max-content", y: "100%"}}
scroll={{x: "max(100%, 1280px)", y: "100%"}}
pagination={false}
/>
</DataListPanel>

View File

@ -7,6 +7,7 @@ import { useDict } from "@/hooks/useDict";
import { usePermission } from "@/hooks/usePermission";
import PageContainer from "@/components/shared/PageContainer";
import SectionCard from "@/components/shared/SectionCard";
import LongTextPreview from "@/components/shared/LongTextPreview";
import type { OrgNode, SysOrg, SysTenant } from "@/types";
import "./index.less";
@ -130,8 +131,8 @@ export default function Orgs() {
};
const columns = [
{ title: t("orgs.orgName"), dataIndex: "orgName", key: "orgName", render: (text: string) => <Text strong>{text}</Text> },
{ title: t("orgs.orgCode"), dataIndex: "orgCode", key: "orgCode", width: 150, render: (text: string) => <Tag className="tabular-nums">{text || "-"}</Tag> },
{ title: t("orgs.orgName"), dataIndex: "orgName", key: "orgName", width: 320, ellipsis: { showTitle: false }, render: (text: string) => <LongTextPreview value={text} strong /> },
{ title: t("orgs.orgCode"), dataIndex: "orgCode", key: "orgCode", width: 180, ellipsis: { showTitle: false }, render: (text: string) => <LongTextPreview value={text} code className="tabular-nums" /> },
{ title: t("orgs.sort"), dataIndex: "sortOrder", width: 100, className: "tabular-nums" },
{
title: t("common.status"),
@ -188,7 +189,7 @@ export default function Orgs() {
</div>
<div className="orgs-table-shell">
{selectedTenantId !== undefined ? (
<Table rowKey="id" columns={columns} dataSource={treeData} loading={loading} pagination={false} size="middle" scroll={{ y: "100%" }} expandable={{ defaultExpandAllRows: true }} />
<Table rowKey="id" columns={columns} dataSource={treeData} loading={loading} pagination={false} size="middle" scroll={{ x: "max(100%, 920px)", y: "100%" }} tableLayout="fixed" expandable={{ defaultExpandAllRows: true }} />
) : (
<div className="app-page__empty-state"><Empty description={t("orgs.selectTenant")} /></div>
)}

View File

@ -7,6 +7,7 @@ import { useDict } from "@/hooks/useDict";
import { usePermission } from "@/hooks/usePermission";
import PageContainer from "@/components/shared/PageContainer";
import SectionCard from "@/components/shared/SectionCard";
import LongTextPreview from "@/components/shared/LongTextPreview";
import { getStandardPagination } from "@/utils/pagination";
import type { SysDictItem, SysDictType } from "@/types";
import "./index.less";
@ -243,10 +244,11 @@ export default function Dictionaries() {
dataSource={items}
pagination={false}
size="middle"
scroll={{ y: "calc(100vh - 320px)" }}
scroll={{ x: "max(100%, 760px)", y: "calc(100vh - 320px)" }}
tableLayout="fixed"
columns={[
{ title: t("dicts.itemLabel"), dataIndex: "itemLabel", render: (text: string) => <Text strong>{text}</Text> },
{ title: t("dicts.itemValue"), dataIndex: "itemValue", className: "tabular-nums" },
{ title: t("dicts.itemLabel"), dataIndex: "itemLabel", width: 220, ellipsis: { showTitle: false }, render: (text: string) => <LongTextPreview value={text} strong /> },
{ title: t("dicts.itemValue"), dataIndex: "itemValue", width: 220, ellipsis: { showTitle: false }, render: (text: string) => <LongTextPreview value={text} code className="tabular-nums" /> },
{ title: t("dicts.sort"), dataIndex: "sortOrder", width: 80, className: "tabular-nums" },
{
title: t("common.status"),

View File

@ -9,6 +9,7 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard";
import LongTextPreview from "@/components/shared/LongTextPreview";
import { getDefaultPageSize } from "@/utils/pagination";
import type { SysLog, SysTenant, UserProfile } from "@/types";
import "./index.less";
@ -19,7 +20,7 @@ const DEFAULT_TABLE_PAGE_SIZE = getDefaultPageSize("table");
export default function Logs() {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState("OPERATION");
const [activeTab, setActiveTab] = useState("");
const [loading, setLoading] = useState(false);
const [data, setData] = useState<SysLog[]>([]);
const [total, setTotal] = useState(0);
@ -42,7 +43,7 @@ export default function Logs() {
sortField: "createdAt",
sortOrder: "descend" as any
});
const { items: logTypeDict } = useDict("sys_log_type");
const { items: logTypeDict, loading: logTypeLoading } = useDict("sys_log_type");
const { items: logStatusDict } = useDict("sys_log_status");
const userProfile = useMemo(() => {
@ -60,21 +61,36 @@ export default function Logs() {
if (!isPlatformAdmin || !params.tenantId) return t("logsExt.allTenants");
return tenants.find((tenant) => tenant.id === params.tenantId)?.tenantName || t("logsExt.tenantId", { id: params.tenantId });
}, [isPlatformAdmin, params.tenantId, tenants, t]);
const activeLogTypeLabel = useMemo(() => {
const dictLabel = logTypeDict.find((item) => item.itemValue === activeTab)?.itemLabel;
if (dictLabel) return dictLabel;
return activeTab === "OPERATION" ? t("logs.opLog") : t("logs.loginLog");
}, [activeTab, logTypeDict, t]);
const tabItems = useMemo(() => {
if (logTypeDict.length > 0) {
return logTypeDict.map((item) => ({
key: item.itemValue,
label: item.itemLabel,
}));
}
const tabItems = logTypeDict.length > 0
? logTypeDict.map((item) => ({
key: item.itemValue,
label: item.itemLabel,
}))
: [
if (logTypeLoading) {
return [];
}
return [
{ key: "OPERATION", label: t("logs.opLog") },
{ key: "LOGIN", label: t("logs.loginLog") },
];
}, [logTypeDict, logTypeLoading, t]);
const activeLogTypeLabel = useMemo(() => {
const activeItem = tabItems.find((item) => item.key === activeTab) || tabItems[0];
if (activeItem?.label) return activeItem.label;
return activeTab === "LOGIN" ? t("logs.loginLog") : t("logs.opLog");
}, [activeTab, tabItems, t]);
useEffect(() => {
if (!tabItems.length) return;
if (!activeTab || !tabItems.some((item) => item.key === activeTab)) {
setActiveTab(tabItems[0].key);
}
}, [activeTab, tabItems]);
const cleanAction = (
<Popconfirm
@ -85,13 +101,14 @@ export default function Logs() {
okButtonProps={{ danger: true, loading: cleaning }}
onConfirm={() => void handleClean()}
>
<Button danger className="logs-clean-action" icon={<DeleteOutlined aria-hidden="true" />} loading={cleaning}>
<Button danger className="logs-clean-action" icon={<DeleteOutlined aria-hidden="true" />} loading={cleaning} disabled={!activeTab}>
{t("logsExt.cleanCurrent", { type: activeLogTypeLabel })}
</Button>
</Popconfirm>
);
const loadData = async (currentParams = params) => {
if (!activeTab) return;
const requestSeq = requestSeqRef.current + 1;
requestSeqRef.current = requestSeq;
setLoading(true);
@ -172,6 +189,7 @@ export default function Logs() {
};
const handleClean = async () => {
if (!activeTab) return;
setCleaning(true);
try {
await cleanLogs(activeTab, isPlatformAdmin ? params.tenantId : undefined);
@ -207,8 +225,9 @@ export default function Logs() {
title: activeTab === "OPERATION" ? t("logsExt.actionLabel") : t("logs.opDetail"),
dataIndex: activeTab === "OPERATION" ? "actionName" : "operation",
key: activeTab === "OPERATION" ? "actionName" : "operation",
ellipsis: true,
render: (_: string, record: SysLog) => <Text type="secondary">{record.actionName || record.operation}</Text>
width: activeTab === "OPERATION" ? 360 : 300,
ellipsis: { showTitle: false },
render: (_: string, record: SysLog) => <LongTextPreview value={record.actionName || record.operation} textType="secondary" />
},
{
title: t("logs.ip"),
@ -393,7 +412,7 @@ export default function Logs() {
loading={loading}
onChange={handleTableChange}
totalCount={total}
scroll={{ y: "100%" }}
scroll={{ x: "max(100%, 1080px)", y: "100%" }}
pagination={false}
/>
</DataListPanel>

View File

@ -10,6 +10,7 @@ import DataListPanel from "@/components/shared/DataListPanel";
import ListTable from "@/components/shared/ListTable/ListTable";
import AppPagination from "@/components/shared/AppPagination";
import SectionCard from "@/components/shared/SectionCard";
import LongTextPreview from "@/components/shared/LongTextPreview";
import type { SysParamQuery, SysParamVO } from "@/types";
import "./index.less";
@ -160,6 +161,7 @@ export default function SysParams() {
title: t("sysParams.paramKey"),
dataIndex: "paramKey",
key: "paramKey",
width: 320,
render: (text: string, record: SysParamVO) => (
<Space direction="vertical" size={0}>
<Text className="param-key-text">{text}</Text>
@ -172,6 +174,7 @@ export default function SysParams() {
dataIndex: "paramValue",
key: "paramValue",
width: 360,
ellipsis: { showTitle: false },
render: (text: string, record: SysParamVO) => {
const type = record.paramType;
// Boolean: 彩色标签显示 是/否
@ -181,40 +184,10 @@ export default function SysParams() {
}
// JSON: 等宽字体,以纯字符串显示
if (isType(type, "JSON")) {
return (
<Tooltip title={text}>
<Text
code
style={{
display: "block",
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{text}
</Text>
</Tooltip>
);
return <LongTextPreview value={text} code />;
}
// Number / String: 等宽代码文本 + Tooltip
return (
<Tooltip title={text}>
<Text
code
style={{
display: "block",
maxWidth: "100%",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap"
}}
>
{text}
</Text>
</Tooltip>
);
// 数字和字符串:等宽代码文本,悬浮查看完整内容。
return <LongTextPreview value={text} code />;
}
},
{
@ -224,7 +197,7 @@ export default function SysParams() {
width: 120,
render: (type: string) => <Tag className="param-type-tag">{type || t("sysParamsExt.defaultType")}</Tag>
},
{ title: t("sysParams.description"), dataIndex: "description", key: "description", ellipsis: true },
{ title: t("sysParams.description"), dataIndex: "description", key: "description", width: 360, ellipsis: true },
{
title: t("common.status"),
dataIndex: "status",
@ -298,7 +271,7 @@ export default function SysParams() {
columns={columns}
dataSource={data}
loading={loading}
scroll={{ y: "100%" }}
scroll={{ x: "max(100%, 1350px)", y: "100%" }}
pagination={false}
/>
</DataListPanel>