feat: 界面优化2
parent
7671f94fcd
commit
e39f1405b3
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
|
|
|||
Loading…
Reference in New Issue