refactor(frontend): split 7 oversized pages into feature co-location

Per guide ch.7 page split pattern (skeleton orchestrates, private
components live in features/<domain>/):
- InstancePage 1716->328, UsersPage 1309->240, TicketsPage 1076->195,
  TicketDetailPage 911->287, QueriesPage 883->479, LdapConfigPage 860->393,
  WorkflowPage 852->194
- new feature modules: datasource/ rbac/ ticket/ query/ system/ldap/
  governance/workflow/ (components + hooks + types + utils)
- stable keys replace index keys in dynamic lists; polling intervals
  and toast call sites preserved 1:1 (verified by agents)
- filter/pagination state moved to URL query via new useQueryState hook
  (Instance/Users/Tickets/AuditLogs pages): refresh/share/back-button
  restore state; composite updates applied atomically
- permission pure functions extracted to lib/permissions.ts (used by
  useAuth + useLogin); page-size audit: 0 pages over 700-line budget
This commit is contained in:
Logan.Li committed 2026-10-26 17:54:15 +08:00
1 parent ac563d1c5e
commit ac498c7ba2
92 files changed
+8224 -6146

No files matched your search

@@ -0,0 +1,35 @@
/**
* 用途:管理用户表格的多选状态(单选切换 / 全选切换 / 选中集合)
* 必需参数:visible(当前可见的用户列表,用于计算全选状态)
* 已知限制:翻页或刷新后不自动清除选中(与原页面行为一致,由调用方决定何时清空)
*/
import { useState } from 'react';
import type { UserItem } from '../types';
export function useUserSelection(visible: UserItem[]) {
const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
const allSelected = visible.length > 0 && visible.every(u => selectedIds.has(u.id));
const toggleSelectAll = () => {
if (allSelected) {
const next = new Set(selectedIds);
visible.forEach(u => next.delete(u.id));
setSelectedIds(next);
} else {
const next = new Set(selectedIds);
visible.forEach(u => next.add(u.id));
setSelectedIds(next);
}
};
const toggleSelect = (id: number) => {
const next = new Set(selectedIds);
if (next.has(id)) next.delete(id);
else next.add(id);
setSelectedIds(next);
};
return { selectedIds, setSelectedIds, allSelected, toggleSelectAll, toggleSelect };
}
@@ -0,0 +1,47 @@
/**
* 用途:按分页与筛选条件拉取用户列表(参数组装与后端接口保持原样)
* 必需参数:page / pageSize / search / roleFilter / statusFilter
* 已知限制:来源(sourceFilter)为前端过滤,不参与请求;失败时 toast 必现
*/
import { useState, useEffect, useCallback } from 'react';
import { userApi } from '@/api/user';
import { toast } from '@/hooks/useToast';
import type { UserItem } from '../types';
export interface UseUsersListParams {
page: number;
pageSize: number;
search: string;
roleFilter: string;
statusFilter: string;
}
export function useUsersList({ page, pageSize, search, roleFilter, statusFilter }: UseUsersListParams) {
const [users, setUsers] = useState<UserItem[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const fetchUsers = useCallback(async () => {
setLoading(true);
try {
const params: any = { page, page_size: pageSize };
if (search) params.search = search;
if (roleFilter) params.role = roleFilter;
if (statusFilter === 'active') params.is_active = true;
else if (statusFilter === 'disabled') params.is_active = false;
const res = await userApi.list(params);
const data = res as any;
setUsers(Array.isArray(data?.items) ? data.items : Array.isArray(data) ? data : []);
setTotal(data?.total ?? data?.length ?? 0);
} catch {
toast('error', '获取用户列表失败');
} finally {
setLoading(false);
}
}, [page, pageSize, search, roleFilter, statusFilter]);
useEffect(() => { fetchUsers(); }, [fetchUsers]);
return { users, total, loading, fetchUsers };
}