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>) { }: ListTableProps<T>) {
const mergedScroll = React.useMemo(() => { const mergedScroll = React.useMemo(() => {
return { return {
x: "max(100%, 960px)",
...scroll, ...scroll,
x: scroll.x ?? "max(100%, 960px)",
}; };
}, [scroll]); }, [scroll]);

View File

@ -385,6 +385,7 @@ export default function Users() {
{ {
title: t("users.userInfo"), title: t("users.userInfo"),
key: "user", key: "user",
width: 260,
render: (_: any, record: SysUser) => ( render: (_: any, record: SysUser) => (
<Space> <Space>
<Avatar className="user-avatar-placeholder" size={40} src={record.avatarUrl || undefined} icon={record.avatarUrl ? undefined : <UserOutlined />} /> <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"), title: t("users.roles"),
key: "roles", key: "roles",
width: 220,
render: (_: any, record: SysUser) => ( render: (_: any, record: SysUser) => (
<Space wrap size={[0, 4]}> <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>} {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} columns={columns}
dataSource={data} dataSource={data}
loading={loading} loading={loading}
scroll={{ y: "100%" }} scroll={{ x: "max(100%, 1360px)", y: "100%" }}
pagination={false} pagination={false}
/> />
</DataListPanel> </DataListPanel>

View File

@ -95,7 +95,7 @@
--meeting-card-columns: 4; --meeting-card-columns: 4;
--meeting-card-rows: 3; --meeting-card-rows: 3;
--meeting-card-grid-gap: clamp(8px, 0.9cqw, 12px); --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; flex: 1 1 auto;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
@ -724,7 +724,7 @@
} }
.meetings-card-scroll { .meetings-card-scroll {
--meeting-card-grid-min-height: clamp(144px, 16vh, 156px); --meeting-card-grid-min-height: 164px;
--meeting-card-grid-gap: 8px; --meeting-card-grid-gap: 8px;
padding: 4px 6px 8px; padding: 4px 6px 8px;
} }
@ -772,3 +772,10 @@
padding: 14px !important; 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, Typography,
} from "antd"; } from "antd";
import dayjs from "dayjs"; 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 { useNavigate, useSearchParams } from "react-router-dom";
import { listUsers } from "../../api"; import { listUsers } from "../../api";
@ -62,7 +62,15 @@ const PAUSED_DISPLAY_STATUS = 5;
const REALTIME_ACTIVE_DISPLAY_STATUS = 6; const REALTIME_ACTIVE_DISPLAY_STATUS = 6;
const REALTIME_IDLE_DISPLAY_STATUS = 7; const REALTIME_IDLE_DISPLAY_STATUS = 7;
const ALL_STATUS_FILTER = "all"; 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 TABLE_PAGE_SIZE = 10;
const QUEUED_RETRY_THRESHOLD_MS = 2 * 60 * 1000; const QUEUED_RETRY_THRESHOLD_MS = 2 * 60 * 1000;
const MEETING_STATUS_FILTER_OPTIONS = [ const MEETING_STATUS_FILTER_OPTIONS = [
@ -260,6 +268,26 @@ const getMeetingTagList = (tags: unknown) => {
return []; 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<{ const MeetingCardItem: React.FC<{
item: MeetingVO; item: MeetingVO;
config: { text: string; color: string; bgColor: string }; config: { text: string; color: string; bgColor: string };
@ -415,7 +443,7 @@ const Meetings: React.FC = () => {
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0);
const [current, setCurrent] = useState(1); const [current, setCurrent] = useState(1);
const [displayMode, setDisplayMode] = useState<"card" | "list">("card"); 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 [searchTitle, setSearchTitle] = useState("");
const [searchKeyword, setSearchKeyword] = useState(""); const [searchKeyword, setSearchKeyword] = useState("");
const [viewType, setViewType] = useState<"all" | "created" | "involved">("all"); const [viewType, setViewType] = useState<"all" | "created" | "involved">("all");
@ -430,19 +458,29 @@ const Meetings: React.FC = () => {
}); });
const [, setUserList] = useState<SysUser[]>([]); const [, setUserList] = useState<SysUser[]>([]);
const progressTerminalRefreshRef = useRef<Map<number, string>>(new Map()); const progressTerminalRefreshRef = useRef<Map<number, string>>(new Map());
const fetchRequestSeqRef = useRef(0);
const [retryingMeetingIds, setRetryingMeetingIds] = useState<Record<number, boolean>>({}); 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 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) => { const handlePaginationChange = (page: number, pageSize: number) => {
setCurrent(pageSize !== size ? 1 : page); const nextSize = displayMode === "card" ? cardLayout.pageSize : pageSize;
setSize(pageSize); setCurrent(nextSize !== size ? 1 : page);
setSize(nextSize);
}; };
const handleDisplayModeChange = (mode: "card" | "list") => { const handleDisplayModeChange = (mode: "card" | "list") => {
setDisplayMode(mode); setDisplayMode(mode);
setSize(mode === "card" ? CARD_PAGE_SIZE : TABLE_PAGE_SIZE); setSize(mode === "card" ? cardLayout.pageSize : TABLE_PAGE_SIZE);
setCurrent(1); setCurrent(1);
}; };
@ -498,6 +536,56 @@ const Meetings: React.FC = () => {
void fetchData(); void fetchData();
}, [current, size, searchTitle, viewType, statusFilter]); }, [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(() => { useEffect(() => {
const maxPage = Math.max(1, Math.ceil(total / size)); const maxPage = Math.max(1, Math.ceil(total / size));
if (current > maxPage) { if (current > maxPage) {
@ -617,6 +705,7 @@ const Meetings: React.FC = () => {
}, []); }, []);
const fetchData = async (silent = false) => { const fetchData = async (silent = false) => {
const requestSeq = ++fetchRequestSeqRef.current;
if (!silent) { if (!silent) {
setLoading(true); setLoading(true);
} }
@ -638,11 +727,14 @@ const Meetings: React.FC = () => {
} catch {} } catch {}
} }
const nextData = records.map((item) => applyRealtimeSessionStatus(item, statusMap[item.id])); const nextData = records.map((item) => applyRealtimeSessionStatus(item, statusMap[item.id]));
if (requestSeq !== fetchRequestSeqRef.current) {
return;
}
setData(nextData); setData(nextData);
setTotal(res.data?.data?.total || 0); setTotal(res.data?.data?.total || 0);
await loadBatchProgress(nextData); await loadBatchProgress(nextData);
} finally { } finally {
if (!silent) { if (!silent && requestSeq === fetchRequestSeqRef.current) {
setLoading(false); setLoading(false);
} }
} }
@ -885,14 +977,21 @@ const Meetings: React.FC = () => {
variant={displayMode === "card" ? "card" : "table"} variant={displayMode === "card" ? "card" : "table"}
current={current} current={current}
pageSize={size} pageSize={size}
pageSizeOptions={displayMode === "card" ? ["12", "24", "36", "48"] : undefined} pageSizeOptions={displayMode === "card" ? cardPageSizeOptions : undefined}
showSizeChanger={displayMode === "card" ? false : undefined}
total={total} total={total}
onChange={handlePaginationChange} onChange={handlePaginationChange}
/> />
} }
> >
{displayMode === "card" ? ( {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 }}> <Skeleton loading={loading} active paragraph={{ rows: 8 }}>
<List <List
dataSource={data} dataSource={data}
@ -909,7 +1008,7 @@ const Meetings: React.FC = () => {
onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }} onRetrySchedule={(meeting) => { void handleRetrySchedule(meeting); }}
onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }} onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }}
retrying={!!retryingMeetingIds[item.id]} retrying={!!retryingMeetingIds[item.id]}
spacious={isFullCardPage} spacious={isSpaciousCardLayout}
/> />
); );
}} }}