[fix]:[FL-125][系统参数管理页面继续对齐用户管理页面交互与移动端样式]

Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
chengkai3
2026-06-19 23:12:35 +08:00
parent 0a776c1cf8
commit 22ef1f0055
3 changed files with 157 additions and 31 deletions
+67 -24
View File
@@ -63,13 +63,20 @@ const PARAM_TABLE_MIN_SCROLL_Y = 180;
const PARAM_TABLE_VIEWPORT_GAP = 40;
const PARAM_TABLE_FALLBACK_RESERVE = 220;
function paramStatusLabel(status: SystemParamSummary["status"]): string {
if (status === "enabled") return "已启用";
if (status === "disabled") return "已禁用";
return status || "-";
}
export default function AdminSystemParamsPage() {
const { user, initializing, fetchWithAuth, hasPermission } = useAuth();
const queryClient = useQueryClient();
const isMobile = useMobileDetection();
const [formApi] = Form.useForm<FormState>();
const [keyword, setKeyword] = useState("");
const [keywordInput, setKeywordInput] = useState("");
const [searchKeyword, setSearchKeyword] = useState("");
const [statusFilter, setStatusFilter] = useState<StatusFilter>(undefined);
const [editingId, setEditingId] = useState<number | null>(null);
const [editorOpen, setEditorOpen] = useState(false);
@@ -84,23 +91,26 @@ export default function AdminSystemParamsPage() {
const [allLoadedParams, setAllLoadedParams] = useState<SystemParamSummary[]>([]);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const pageCardRef = useRef<HTMLDivElement | null>(null);
const keywordDebounceTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const canRead = hasPermission("system_param.read") || hasPermission("system_param.manage");
const canManage = hasPermission("system_param.manage");
const { current: paginationCurrent, pageSize: paginationPageSize } = pagination;
const trimmedKeyword = searchKeyword.trim();
const listPath = useMemo(() => {
const params = new URLSearchParams();
params.set("limit", String(pagination.pageSize));
params.set("offset", String((pagination.current - 1) * pagination.pageSize));
if (keyword.trim()) {
params.set("keyword", keyword.trim());
params.set("limit", String(paginationPageSize));
params.set("offset", String((paginationCurrent - 1) * paginationPageSize));
if (trimmedKeyword) {
params.set("keyword", trimmedKeyword);
}
if (statusFilter) {
params.set("status", statusFilter);
}
const qs = params.toString();
return `/api/v1/admin/system-params${qs ? `?${qs}` : ""}`;
}, [keyword, statusFilter, pagination.current, pagination.pageSize]);
}, [paginationCurrent, paginationPageSize, statusFilter, trimmedKeyword]);
const listQuery = useQuery({
queryKey: [listPath],
@@ -249,7 +259,22 @@ export default function AdminSystemParamsPage() {
}
}, [deleteMutation]);
const items = listQuery.data?.items ?? [];
const handleKeywordChange = (value: string) => {
setKeywordInput(value);
if (keywordDebounceTimeoutRef.current) {
clearTimeout(keywordDebounceTimeoutRef.current);
}
keywordDebounceTimeoutRef.current = setTimeout(() => {
setSearchKeyword(value);
setPagination((prev) => ({ ...prev, current: 1 }));
setCardViewPage(() => 1);
setAllLoadedParams(() => []);
}, 500);
};
const items = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items]);
const listError = listQuery.error instanceof Error ? listQuery.error.message : "";
useToastFeedback({
@@ -261,15 +286,22 @@ export default function AdminSystemParamsPage() {
// Accumulate loaded params for card view
useEffect(() => {
if (viewMode === "card" && items.length > 0 && !listQuery.isLoading) {
setAllLoadedParams((prev) => {
const existingIds = new Set(prev.map((p) => p.id));
const newParams = items.filter((p) => !existingIds.has(p.id));
return [...prev, ...newParams];
});
setIsLoadingMore(false);
if (viewMode === "card" && !listQuery.isLoading) {
if (cardViewPage === 1) {
setAllLoadedParams(() => items);
} else {
setAllLoadedParams((prev) => {
if (items.length === 0) {
return prev;
}
const existingIds = new Set(prev.map((p) => p.id));
const newParams = items.filter((p) => !existingIds.has(p.id));
return [...prev, ...newParams];
});
}
setIsLoadingMore(() => false);
}
}, [items, listQuery.isLoading, viewMode]);
}, [items, listQuery.isLoading, viewMode, cardViewPage]);
// Handle infinite scroll for card view
useEffect(() => {
@@ -295,6 +327,7 @@ export default function AdminSystemParamsPage() {
if (loadedCount < total) {
setIsLoadingMore(true);
setCardViewPage((prev) => prev + 1);
setPagination((prev) => ({ ...prev, current: prev.current + 1 }));
}
}
};
@@ -305,9 +338,17 @@ export default function AdminSystemParamsPage() {
// Reset card view state when filters change
useEffect(() => {
setCardViewPage(1);
setAllLoadedParams([]);
}, [statusFilter, keyword]);
setCardViewPage(() => 1);
setAllLoadedParams(() => []);
}, [statusFilter, trimmedKeyword]);
useEffect(() => {
return () => {
if (keywordDebounceTimeoutRef.current) {
clearTimeout(keywordDebounceTimeoutRef.current);
}
};
}, []);
const renderParamCard = (param: SystemParamSummary) => {
const deleteLoading = deletingId === param.id;
@@ -349,7 +390,7 @@ export default function AdminSystemParamsPage() {
<Space className="min-w-0" size={8}>
<Typography.Text strong>{param.param_name}</Typography.Text>
<Tag color={param.status === "enabled" ? "success" : "default"} bordered={false}>
{param.status === "enabled" ? "已启用" : "已禁用"}
{paramStatusLabel(param.status)}
</Tag>
</Space>
}
@@ -438,7 +479,7 @@ export default function AdminSystemParamsPage() {
width: 120,
render: (value: SystemParamSummary["status"]) => (
<Tag color={value === "enabled" ? "success" : "default"}>
{value === "enabled" ? "已启用" : "已禁用"}
{paramStatusLabel(value)}
</Tag>
),
},
@@ -622,8 +663,8 @@ export default function AdminSystemParamsPage() {
<Input
allowClear
placeholder="按参数键/名称/值搜索"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
value={keywordInput}
onChange={(event) => handleKeywordChange(event.target.value)}
/>
</Form.Item>
</Form>
@@ -633,8 +674,8 @@ export default function AdminSystemParamsPage() {
<Input
allowClear
placeholder="按参数键/名称/值搜索"
value={keyword}
onChange={(event) => setKeyword(event.target.value)}
value={keywordInput}
onChange={(event) => handleKeywordChange(event.target.value)}
/>
</Form.Item>
<Form.Item label="状态" style={{ width: 170 }}>
@@ -649,6 +690,8 @@ export default function AdminSystemParamsPage() {
onChange={(value) => {
setStatusFilter(value);
setPagination((prev) => ({ ...prev, current: 1 }));
setCardViewPage(() => 1);
setAllLoadedParams(() => []);
}}
/>
</Form.Item>
+4 -7
View File
@@ -616,7 +616,8 @@ export default function AdminUsersPage() {
useEffect(() => {
if (viewMode === "card" && !usersQuery.isLoading) {
if (cardViewPage === 1) {
setAllLoadedUsers(users);
// eslint-disable-next-line react-hooks/set-state-in-effect -- Mobile card view intentionally mirrors paged query results into an accumulated list.
setAllLoadedUsers(() => users);
} else {
setAllLoadedUsers((prev) => {
if (users.length === 0) {
@@ -664,12 +665,6 @@ export default function AdminUsersPage() {
return () => cardBody.removeEventListener("scroll", handleScroll);
}, [viewMode, isLoadingMore, usersQuery.isLoading, usersQuery.data?.total, allLoadedUsers.length]);
// Reset card view state when switching modes or filters change
useEffect(() => {
setCardViewPage(1);
setAllLoadedUsers([]);
}, [statusFilter, trimmedKeyword]);
const updateTableScrollY = useCallback(() => {
if (typeof window === "undefined") {
return;
@@ -1053,6 +1048,8 @@ export default function AdminUsersPage() {
onChange={(value) => {
setStatusFilter(value);
setPagination((prev) => ({ ...prev, current: 1 }));
setCardViewPage(1);
setAllLoadedUsers([]);
}}
/>
</Form.Item>
+86
View File
@@ -116,6 +116,26 @@
background: var(--ant-color-bg-container) !important;
}
:root[data-fquiz-theme="dark"] .admin-system-params-param-card {
border-color: color-mix(in srgb, var(--fquiz-theme-primary) 35%, var(--ant-color-border-secondary)) !important;
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--ant-color-bg-container) 92%, var(--fquiz-theme-primary) 8%) 0%,
var(--ant-color-bg-container) 100%
) !important;
box-shadow: 0 8px 18px color-mix(in srgb, black 40%, transparent) !important;
}
:root[data-fquiz-theme="dark"] .admin-system-params-param-card > .ant-card-head {
border-bottom-color: color-mix(in srgb, var(--fquiz-theme-primary) 25%, var(--ant-color-border-secondary)) !important;
background: color-mix(in srgb, var(--fquiz-theme-primary) 10%, transparent) !important;
}
:root[data-fquiz-theme="dark"] .admin-system-params-param-card > .ant-card-body {
background: var(--ant-color-bg-container) !important;
}
:root[data-fquiz-theme="dark"] .admin-atp-models-model-card {
border-color: color-mix(in srgb, var(--fquiz-theme-primary) 35%, var(--ant-color-border-secondary)) !important;
background:
@@ -501,6 +521,72 @@ body {
min-height: var(--admin-system-params-table-body-min-height, 180px);
}
.admin-system-params-page-card {
display: flex;
flex: 1;
min-height: 0;
flex-direction: column;
}
.admin-system-params-page-card > .ant-card-body {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
overflow-x: hidden;
overflow-y: auto;
}
.admin-system-params-card-view {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
}
.admin-system-params-card-view-content {
min-height: 0;
flex: 1;
padding: 2px 2px 4px;
}
.admin-system-params-card-view-state {
display: flex;
min-height: 240px;
flex: 1;
align-items: center;
justify-content: center;
}
.admin-system-params-param-card {
height: 100%;
border-color: color-mix(in srgb, var(--fquiz-theme-primary) 26%, var(--ant-color-border-secondary));
background:
linear-gradient(
180deg,
color-mix(in srgb, var(--fquiz-theme-bg-container) 96%, var(--fquiz-theme-primary) 4%) 0%,
var(--fquiz-theme-bg-container) 100%
);
box-shadow: 0 8px 18px color-mix(in srgb, var(--fquiz-theme-text-primary) 8%, transparent);
}
.admin-system-params-param-card > .ant-card-head {
min-height: 44px;
border-bottom-color: color-mix(in srgb, var(--fquiz-theme-primary) 18%, var(--ant-color-border-secondary));
background: color-mix(in srgb, var(--fquiz-theme-primary) 6%, transparent);
}
.admin-system-params-param-card > .ant-card-body {
padding-block: 14px;
}
.admin-system-params-param-card-field {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
gap: 8px;
align-items: baseline;
}
.admin-scheduled-tasks-table-anchor .ant-table-body {
min-height: var(--admin-scheduled-tasks-table-body-min-height, 180px);
}