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