feat: 界面优化2

dev_na
puz 2026-07-08 18:05:27 +08:00
parent 7671f94fcd
commit e39f1405b3
4 changed files with 123 additions and 15 deletions

View File

@ -49,8 +49,8 @@ function ListTable<T extends Record<string, any>>({
}: ListTableProps<T>) {
const mergedScroll = React.useMemo(() => {
return {
x: "max(100%, 960px)",
...scroll,
x: scroll.x ?? "max(100%, 960px)",
};
}, [scroll]);

View File

@ -385,6 +385,7 @@ export default function Users() {
{
title: t("users.userInfo"),
key: "user",
width: 260,
render: (_: any, record: SysUser) => (
<Space>
<Avatar className="user-avatar-placeholder" size={40} src={record.avatarUrl || undefined} icon={record.avatarUrl ? undefined : <UserOutlined />} />
@ -459,6 +460,7 @@ export default function Users() {
{
title: t("users.roles"),
key: "roles",
width: 220,
render: (_: any, record: SysUser) => (
<Space wrap size={[0, 4]}>
{record.roles && record.roles.length > 0 ? record.roles.map((role) => <Tag key={role.roleId} color="cyan">{role.roleName}</Tag>) : <Text type="secondary">{t("usersExt.noRoles")}</Text>}
@ -545,7 +547,7 @@ export default function Users() {
columns={columns}
dataSource={data}
loading={loading}
scroll={{ y: "100%" }}
scroll={{ x: "max(100%, 1360px)", y: "100%" }}
pagination={false}
/>
</DataListPanel>

View File

@ -95,7 +95,7 @@
--meeting-card-columns: 4;
--meeting-card-rows: 3;
--meeting-card-grid-gap: clamp(8px, 0.9cqw, 12px);
--meeting-card-grid-min-height: clamp(156px, 14vh, 172px);
--meeting-card-grid-min-height: 176px;
flex: 1 1 auto;
height: 100%;
min-height: 0;
@ -724,7 +724,7 @@
}
.meetings-card-scroll {
--meeting-card-grid-min-height: clamp(144px, 16vh, 156px);
--meeting-card-grid-min-height: 164px;
--meeting-card-grid-gap: 8px;
padding: 4px 6px 8px;
}
@ -772,3 +772,10 @@
padding: 14px !important;
}
}
.meetings-card-scroll--adaptive .ant-list-items {
grid-template-columns: repeat(var(--meeting-card-adaptive-columns, var(--meeting-card-columns)), minmax(0, 1fr));
grid-auto-rows: var(--meeting-card-grid-min-height);
grid-template-rows: none;
align-content: start;
}

View File

@ -29,7 +29,7 @@ import {
Typography,
} from "antd";
import dayjs from "dayjs";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { listUsers } from "../../api";
@ -62,7 +62,15 @@ const PAUSED_DISPLAY_STATUS = 5;
const REALTIME_ACTIVE_DISPLAY_STATUS = 6;
const REALTIME_IDLE_DISPLAY_STATUS = 7;
const ALL_STATUS_FILTER = "all";
const CARD_PAGE_SIZE = 12;
const CARD_DEFAULT_PAGE_SIZE = 12;
const CARD_DEFAULT_LAYOUT = { columns: 4, rows: 3, pageSize: CARD_DEFAULT_PAGE_SIZE };
const CARD_MIN_COLUMN_WIDTH = 320;
const CARD_MAX_COLUMNS = 4;
const CARD_ROW_HEIGHT = 176;
const CARD_ROW_HEIGHT_COMPACT = 164;
const CARD_MAX_ROWS = 5;
const CARD_GRID_GAP = 10;
const CARD_GRID_GAP_COMPACT = 8;
const TABLE_PAGE_SIZE = 10;
const QUEUED_RETRY_THRESHOLD_MS = 2 * 60 * 1000;
const MEETING_STATUS_FILTER_OPTIONS = [
@ -260,6 +268,26 @@ const getMeetingTagList = (tags: unknown) => {
return [];
};
const clampCardCount = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max);
const calculateCardLayout = (width: number, height: number, viewportHeight: number) => {
if (width <= 0 || height <= 0) {
return CARD_DEFAULT_LAYOUT;
}
const isCompactHeight = viewportHeight <= 820 || height <= 520;
const gap = isCompactHeight ? CARD_GRID_GAP_COMPACT : CARD_GRID_GAP;
const rowHeight = isCompactHeight ? CARD_ROW_HEIGHT_COMPACT : CARD_ROW_HEIGHT;
const columns = clampCardCount(Math.floor((width + gap) / (CARD_MIN_COLUMN_WIDTH + gap)), 1, CARD_MAX_COLUMNS);
const rows = clampCardCount(Math.floor((height + gap) / (rowHeight + gap)), 1, CARD_MAX_ROWS);
return {
columns,
rows,
pageSize: columns * rows,
};
};
const MeetingCardItem: React.FC<{
item: MeetingVO;
config: { text: string; color: string; bgColor: string };
@ -415,7 +443,7 @@ const Meetings: React.FC = () => {
const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1);
const [displayMode, setDisplayMode] = useState<"card" | "list">("card");
const [size, setSize] = useState(CARD_PAGE_SIZE);
const [size, setSize] = useState(CARD_DEFAULT_PAGE_SIZE);
const [searchTitle, setSearchTitle] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
const [viewType, setViewType] = useState<"all" | "created" | "involved">("all");
@ -430,19 +458,29 @@ const Meetings: React.FC = () => {
});
const [, setUserList] = useState<SysUser[]>([]);
const progressTerminalRefreshRef = useRef<Map<number, string>>(new Map());
const fetchRequestSeqRef = useRef(0);
const [retryingMeetingIds, setRetryingMeetingIds] = useState<Record<number, boolean>>({});
const cardScrollRef = useRef<HTMLDivElement | null>(null);
const [cardLayout, setCardLayout] = useState(CARD_DEFAULT_LAYOUT);
const activeFilterCount = (statusFilter !== ALL_STATUS_FILTER ? 1 : 0) + (searchTitle ? 1 : 0);
const isFullCardPage = displayMode === "card" && size === CARD_PAGE_SIZE && data.length === CARD_PAGE_SIZE;
const isSpaciousCardLayout = displayMode === "card" && cardLayout.columns >= 3 && cardLayout.rows >= 3;
const cardPageSizeOptions = useMemo(
() => Array.from(new Set([cardLayout.pageSize, 8, 12, 16, 20, 24, 36, 48]))
.sort((a, b) => a - b)
.map(String),
[cardLayout.pageSize],
);
const handlePaginationChange = (page: number, pageSize: number) => {
setCurrent(pageSize !== size ? 1 : page);
setSize(pageSize);
const nextSize = displayMode === "card" ? cardLayout.pageSize : pageSize;
setCurrent(nextSize !== size ? 1 : page);
setSize(nextSize);
};
const handleDisplayModeChange = (mode: "card" | "list") => {
setDisplayMode(mode);
setSize(mode === "card" ? CARD_PAGE_SIZE : TABLE_PAGE_SIZE);
setSize(mode === "card" ? cardLayout.pageSize : TABLE_PAGE_SIZE);
setCurrent(1);
};
@ -498,6 +536,56 @@ const Meetings: React.FC = () => {
void fetchData();
}, [current, size, searchTitle, viewType, statusFilter]);
useEffect(() => {
if (displayMode !== "card") {
return;
}
const node = cardScrollRef.current;
if (!node || typeof ResizeObserver === "undefined") {
return;
}
let frameId = 0;
const updateCardLayout = (width: number, height: number) => {
if (frameId) {
cancelAnimationFrame(frameId);
}
frameId = requestAnimationFrame(() => {
const nextLayout = calculateCardLayout(width, height, window.innerHeight);
setCardLayout((prevLayout) => (
prevLayout.columns === nextLayout.columns
&& prevLayout.rows === nextLayout.rows
&& prevLayout.pageSize === nextLayout.pageSize
? prevLayout
: nextLayout
));
});
};
updateCardLayout(node.clientWidth, node.clientHeight);
const observer = new ResizeObserver(([entry]) => {
updateCardLayout(entry.contentRect.width, entry.contentRect.height);
});
observer.observe(node);
return () => {
if (frameId) {
cancelAnimationFrame(frameId);
}
observer.disconnect();
};
}, [displayMode]);
useEffect(() => {
if (displayMode !== "card" || size === cardLayout.pageSize) {
return;
}
setCurrent(1);
setSize(cardLayout.pageSize);
}, [cardLayout.pageSize, displayMode, size]);
useEffect(() => {
const maxPage = Math.max(1, Math.ceil(total / size));
if (current > maxPage) {
@ -617,6 +705,7 @@ const Meetings: React.FC = () => {
}, []);
const fetchData = async (silent = false) => {
const requestSeq = ++fetchRequestSeqRef.current;
if (!silent) {
setLoading(true);
}
@ -638,11 +727,14 @@ const Meetings: React.FC = () => {
} catch {}
}
const nextData = records.map((item) => applyRealtimeSessionStatus(item, statusMap[item.id]));
if (requestSeq !== fetchRequestSeqRef.current) {
return;
}
setData(nextData);
setTotal(res.data?.data?.total || 0);
await loadBatchProgress(nextData);
} finally {
if (!silent) {
if (!silent && requestSeq === fetchRequestSeqRef.current) {
setLoading(false);
}
}
@ -885,14 +977,21 @@ const Meetings: React.FC = () => {
variant={displayMode === "card" ? "card" : "table"}
current={current}
pageSize={size}
pageSizeOptions={displayMode === "card" ? ["12", "24", "36", "48"] : undefined}
pageSizeOptions={displayMode === "card" ? cardPageSizeOptions : undefined}
showSizeChanger={displayMode === "card" ? false : undefined}
total={total}
onChange={handlePaginationChange}
/>
}
>
{displayMode === "card" ? (
<div className={isFullCardPage ? "meetings-card-scroll meetings-card-scroll--full-page" : "meetings-card-scroll"}>
<div
ref={cardScrollRef}
className="meetings-card-scroll meetings-card-scroll--adaptive"
style={{
"--meeting-card-adaptive-columns": cardLayout.columns,
} as React.CSSProperties}
>
<Skeleton loading={loading} active paragraph={{ rows: 8 }}>
<List
dataSource={data}
@ -909,7 +1008,7 @@ const Meetings: React.FC = () => {
onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }}
onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }}
retrying={!!retryingMeetingIds[item.id]}
spacious={isFullCardPage}
spacious={isSpaciousCardLayout}
/>
);
}}