diff --git a/frontend/src/components/shared/ListTable/ListTable.tsx b/frontend/src/components/shared/ListTable/ListTable.tsx index 135a5c6..3057df6 100644 --- a/frontend/src/components/shared/ListTable/ListTable.tsx +++ b/frontend/src/components/shared/ListTable/ListTable.tsx @@ -49,8 +49,8 @@ function ListTable>({ }: ListTableProps) { const mergedScroll = React.useMemo(() => { return { - x: "max(100%, 960px)", ...scroll, + x: scroll.x ?? "max(100%, 960px)", }; }, [scroll]); diff --git a/frontend/src/pages/access/users/index.tsx b/frontend/src/pages/access/users/index.tsx index 2786fd4..85808e4 100644 --- a/frontend/src/pages/access/users/index.tsx +++ b/frontend/src/pages/access/users/index.tsx @@ -385,6 +385,7 @@ export default function Users() { { title: t("users.userInfo"), key: "user", + width: 260, render: (_: any, record: SysUser) => ( } /> @@ -459,6 +460,7 @@ export default function Users() { { title: t("users.roles"), key: "roles", + width: 220, render: (_: any, record: SysUser) => ( {record.roles && record.roles.length > 0 ? record.roles.map((role) => {role.roleName}) : {t("usersExt.noRoles")}} @@ -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} /> diff --git a/frontend/src/pages/business/Meetings.css b/frontend/src/pages/business/Meetings.css index 1543219..66d0562 100644 --- a/frontend/src/pages/business/Meetings.css +++ b/frontend/src/pages/business/Meetings.css @@ -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; +} diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index f1da4d0..6677d99 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -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([]); const progressTerminalRefreshRef = useRef>(new Map()); + const fetchRequestSeqRef = useRef(0); const [retryingMeetingIds, setRetryingMeetingIds] = useState>({}); + const cardScrollRef = useRef(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" ? ( -
+
{ onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }} onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }} retrying={!!retryingMeetingIds[item.id]} - spacious={isFullCardPage} + spacious={isSpaciousCardLayout} /> ); }}