1. 登录返回总公司,分公司,部门信息。
2. 修改角色权限管理的分配用户的数据渲染和接口。 3. 交叉评查任务的创建的组织架构组件的重构。
This commit is contained in:
+10
-2
@@ -239,7 +239,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
user_id: savedUserInfo.user_id,
|
||||
user_role: savedUserInfo.user_role, // 使用后端返回的角色
|
||||
area: savedUserInfo.area, // 🔑 用户所属地区
|
||||
frontend_jwt: frontendJWT
|
||||
frontend_jwt: frontendJWT,
|
||||
// 🔑 包含后端返回的组织信息字段(可能为null)
|
||||
tenant_name: savedUserInfo.tenant_name,
|
||||
dep_name: savedUserInfo.dep_name,
|
||||
dep_short_name: savedUserInfo.dep_short_name,
|
||||
};
|
||||
|
||||
// 🔑 重要:将 token 和用户信息作为 URL 参数传递给客户端
|
||||
@@ -257,7 +261,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
is_leader: savedUserInfo.is_leader,
|
||||
user_role: savedUserInfo.user_role,
|
||||
area: savedUserInfo.area, // 🔑 用户所属地区
|
||||
sub: userInfo.data.sub
|
||||
sub: userInfo.data.sub,
|
||||
// 🔑 包含后端返回的组织信息字段(可能为null)
|
||||
tenant_name: savedUserInfo.tenant_name,
|
||||
dep_name: savedUserInfo.dep_name,
|
||||
dep_short_name: savedUserInfo.dep_short_name,
|
||||
})));
|
||||
callbackUrl.searchParams.set('redirectTo', redirectTo);
|
||||
|
||||
|
||||
@@ -21,10 +21,16 @@ import {
|
||||
getCrossCheckingDocumentTypes,
|
||||
type DocumentType
|
||||
} from "~/api/cross-checking/cross-files";
|
||||
import {
|
||||
getOrganizationTree,
|
||||
convertToTreeData
|
||||
} from "~/api/user";
|
||||
import {
|
||||
getOrganizationTree,
|
||||
convertToTreeData,
|
||||
extractUsersFromNode,
|
||||
convertUserToTreeNode,
|
||||
convertSearchResultsToTreeNodes,
|
||||
searchUsers,
|
||||
type TreeNodeItem,
|
||||
type UserInfo
|
||||
} from "~/api/user/user-management";
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
@@ -52,82 +58,53 @@ const STEPS = [
|
||||
{ id: 3, label: "选择卷宗" }
|
||||
];
|
||||
|
||||
// 1. TreeNode类型和MOCK_TREE
|
||||
export interface TreeNode {
|
||||
label: string;
|
||||
value: string;
|
||||
children?: TreeNode[];
|
||||
}
|
||||
|
||||
// 默认的空组织架构数据(作为备用)
|
||||
const DEFAULT_TREE: TreeNode[] = [];
|
||||
const DEFAULT_TREE: TreeNodeItem[] = [];
|
||||
|
||||
// 用户选择状态管理
|
||||
interface UserSelectionState {
|
||||
treeData: TreeNode[];
|
||||
treeData: TreeNodeItem[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
|
||||
function isAllChildrenChecked(node: TreeNode, checked: string[]): boolean {
|
||||
if (!node.children || node.children.length === 0) return checked.includes(node.value);
|
||||
return node.children.every(child => isAllChildrenChecked(child, checked));
|
||||
}
|
||||
function isSomeChildrenChecked(node: TreeNode, checked: string[]): boolean {
|
||||
if (!node.children || node.children.length === 0) return checked.includes(node.value);
|
||||
return node.children.some(child => isSomeChildrenChecked(child, checked));
|
||||
}
|
||||
const TreeNodeCheckbox: React.FC<{
|
||||
node: TreeNode;
|
||||
checked: string[];
|
||||
onCheck: (node: TreeNode, checked: boolean) => void;
|
||||
level?: number;
|
||||
}> = ({ node, checked, onCheck, level = 0 }) => {
|
||||
const [expanded, setExpanded] = React.useState(true);
|
||||
const allChecked = isAllChildrenChecked(node, checked);
|
||||
const someChecked = isSomeChildrenChecked(node, checked);
|
||||
const isLeaf = !node.children || node.children.length === 0;
|
||||
return (
|
||||
<div style={{ marginLeft: level * 18 }}>
|
||||
<div className="flex items-center">
|
||||
{!isLeaf && (
|
||||
<span
|
||||
className="mr-1 cursor-pointer select-none"
|
||||
onClick={() => setExpanded(e => !e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={e => (e.key === 'Enter' || e.key === ' ') && setExpanded(e => !e)}
|
||||
style={{ width: 16, display: "inline-block", textAlign: "center" }}
|
||||
>
|
||||
<i className={expanded ? "ri-arrow-down-s-line" : "ri-arrow-right-s-line"}></i>
|
||||
</span>
|
||||
)}
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-checkbox"
|
||||
checked={allChecked}
|
||||
ref={el => { if (el) el.indeterminate = !allChecked && someChecked; }}
|
||||
onChange={e => onCheck(node, e.target.checked)}
|
||||
id={node.value}
|
||||
/>
|
||||
<label htmlFor={node.value} className="ml-2">{node.label}</label>
|
||||
</div>
|
||||
{expanded && node.children && (
|
||||
<div>
|
||||
{node.children.map(child => (
|
||||
<TreeNodeCheckbox
|
||||
key={child.value}
|
||||
node={child}
|
||||
checked={checked}
|
||||
onCheck={onCheck}
|
||||
level={level + 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
// 获取用户完整信息
|
||||
const getUserFullName = (value: string, treeData: TreeNodeItem[]): string => {
|
||||
for (const node of treeData) {
|
||||
if (node.value === value && node.isUser) {
|
||||
return `${node.userInfo?.nick_name || node.label} (${node.userInfo?.tenant_name} · ${node.userInfo?.ou_name})`;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = getUserFullName(value, node.children);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 从用户ID获取用户信息(优先从搜索用户Map中查找,再从树数据中查找)
|
||||
const getUserInfoById = (
|
||||
userId: string,
|
||||
treeData: TreeNodeItem[],
|
||||
searchedUsersMap?: Map<string, UserInfo>
|
||||
): UserInfo | null => {
|
||||
// 先从搜索用户Map中查找
|
||||
if (searchedUsersMap && searchedUsersMap.has(userId)) {
|
||||
return searchedUsersMap.get(userId)!;
|
||||
}
|
||||
|
||||
// 再从树数据中查找
|
||||
for (const node of treeData) {
|
||||
if (node.value === userId && node.isUser && node.userInfo) {
|
||||
return node.userInfo;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = getUserInfoById(userId, node.children, searchedUsersMap);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
/**
|
||||
* 获取用户会话和前端JWT,以及文档类型列表
|
||||
@@ -153,7 +130,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const caseType = formData.get("caseType") as string;
|
||||
const uploadType = formData.get("uploadType") as string;
|
||||
|
||||
console.log("交叉评查上传:", { caseType, uploadType });
|
||||
// console.log("交叉评查上传:", { caseType, uploadType });
|
||||
|
||||
// 这里可以处理上传后的业务逻辑
|
||||
// 例如创建任务记录等
|
||||
@@ -188,6 +165,8 @@ export default function CrossCheckingUpload() {
|
||||
loading: false,
|
||||
error: null
|
||||
});
|
||||
// 存储从搜索结果添加的用户信息(这些用户不在 treeData 中)
|
||||
const [searchedUsers, setSearchedUsers] = useState<Map<string, UserInfo>>(new Map());
|
||||
|
||||
// 上传配置状态 - 设置默认值
|
||||
const [priority] = useState<string>("normal");
|
||||
@@ -215,7 +194,7 @@ export default function CrossCheckingUpload() {
|
||||
// 清空已选择的文件和重置上传方式
|
||||
clearAllFiles();
|
||||
const selectedType = documentTypes?.find((dt: DocumentType) => dt.id === docTypeId);
|
||||
console.log("案卷类型切换为:", selectedType?.name, "ID:", docTypeId);
|
||||
// console.log("案卷类型切换为:", selectedType?.name, "ID:", docTypeId);
|
||||
};
|
||||
|
||||
// 清空文件
|
||||
@@ -264,7 +243,7 @@ export default function CrossCheckingUpload() {
|
||||
uploadType
|
||||
});
|
||||
|
||||
console.log("选择文件:", file.name, "类型:", uploadType);
|
||||
// console.log("选择文件:", file.name, "类型:", uploadType);
|
||||
};
|
||||
|
||||
// 删除文件
|
||||
@@ -322,7 +301,7 @@ export default function CrossCheckingUpload() {
|
||||
}
|
||||
|
||||
// 第一步:上传文件并自动分配任务(新接口)
|
||||
console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", selectedDocType.name);
|
||||
// console.log("开始批量上传文件并分配任务:", filesToUpload.length, "个,案卷类型:", selectedDocType.name);
|
||||
|
||||
// 提取用户ID(从选中的组织架构中获取用户)
|
||||
const userIds = groupChecked.filter(id => {
|
||||
@@ -349,7 +328,7 @@ export default function CrossCheckingUpload() {
|
||||
// frontendJWT
|
||||
// }
|
||||
|
||||
// console.log("requireParam", requireParam)
|
||||
// // console.log("requireParam", requireParam)
|
||||
// return;
|
||||
|
||||
// 构建负责人ID数组:当前用户(主要负责人)+ 额外负责人
|
||||
@@ -499,44 +478,30 @@ export default function CrossCheckingUpload() {
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 加载组织架构数据
|
||||
// 加载组织架构数据(首次只加载组织结构,不含用户)
|
||||
useEffect(() => {
|
||||
const loadOrganizationData = async () => {
|
||||
// 只在步骤2且数据为空且未在加载时执行
|
||||
if (currentStep === 2 && userSelectionState.treeData.length === 0 && !userSelectionState.loading) {
|
||||
setUserSelectionState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
|
||||
try {
|
||||
console.log('开始加载组织架构数据');
|
||||
// 传递JWT token到API调用
|
||||
const response = await getOrganizationTree(true, frontendJWT);
|
||||
|
||||
// console.log('[loadOrganizationData] 开始加载组织架构数据');
|
||||
// 首次只加载组织结构,不含用户(方案2)
|
||||
const response = await getOrganizationTree(false, frontendJWT);
|
||||
|
||||
if (response.success && response.data) {
|
||||
console.log('原始API数据:', response.data);
|
||||
// console.log('[loadOrganizationData] 原始API数据:', response.data);
|
||||
const treeData = convertToTreeData(response.data.organizations);
|
||||
console.log('转换后的树形数据:', treeData);
|
||||
|
||||
// 验证数据转换是否正确
|
||||
treeData.forEach(org => {
|
||||
console.log(`组织: ${org.label} (${org.value})`);
|
||||
if (org.children) {
|
||||
org.children.forEach(child => {
|
||||
if (child.isUser) {
|
||||
console.log(` - 用户: ${child.label} (${child.value})`);
|
||||
} else {
|
||||
console.log(` - 子组织: ${child.label} (${child.value})`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// console.log('[loadOrganizationData] 转换后的树形数据:', treeData);
|
||||
|
||||
setUserSelectionState({
|
||||
treeData,
|
||||
loading: false,
|
||||
error: null
|
||||
});
|
||||
} else {
|
||||
console.error('获取组织架构失败:', response.error);
|
||||
console.error('[loadOrganizationData] 获取组织架构失败:', response.error);
|
||||
setUserSelectionState({
|
||||
treeData: DEFAULT_TREE,
|
||||
loading: false,
|
||||
@@ -545,7 +510,7 @@ export default function CrossCheckingUpload() {
|
||||
toastService.error('获取组织架构失败,请刷新页面重试');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载组织架构数据失败:', error);
|
||||
console.error('[loadOrganizationData] 加载组织架构数据失败:', error);
|
||||
setUserSelectionState({
|
||||
treeData: DEFAULT_TREE,
|
||||
loading: false,
|
||||
@@ -555,23 +520,108 @@ export default function CrossCheckingUpload() {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadOrganizationData();
|
||||
}, [currentStep]); // 只依赖 currentStep,避免无限循环
|
||||
|
||||
// 在 CrossCheckingUpload 组件内添加工具函数
|
||||
function findUserNameById(tree: TreeNode[], userId: string): string | null {
|
||||
for (const node of tree) {
|
||||
if (node.value === userId && (node as { isUser?: boolean }).isUser) {
|
||||
return node.label;
|
||||
loadOrganizationData();
|
||||
}, [currentStep, frontendJWT]);
|
||||
|
||||
|
||||
// 懒加载子节点(点击部门时加载该部门的用户)
|
||||
const handleLoadChildren = async (node: TreeNodeItem): Promise<TreeNodeItem[]> => {
|
||||
// console.log('[handleLoadChildren] 懒加载子节点:', node.label, 'ou_id:', node.value);
|
||||
|
||||
try {
|
||||
// 调用接口获取该部门的子树(含用户)
|
||||
const response = await getOrganizationTree(true, frontendJWT, node.value);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const organizations = response.data.organizations;
|
||||
if (organizations.length > 0) {
|
||||
// 从返回的树中找到目标节点,提取其用户数据
|
||||
const users = extractUsersFromNode(organizations, node.value);
|
||||
// console.log('[handleLoadChildren] 提取到的用户数量:', users.length);
|
||||
return users;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('[handleLoadChildren] 加载子节点失败:', error);
|
||||
throw error;
|
||||
}
|
||||
if (node.children) {
|
||||
const found = findUserNameById(node.children, userId);
|
||||
if (found) return found;
|
||||
};
|
||||
|
||||
// 搜索用户
|
||||
const handleSearchUsers = async (keyword: string): Promise<TreeNodeItem[]> => {
|
||||
// console.log('[handleSearchUsers] 搜索用户:', keyword);
|
||||
|
||||
try {
|
||||
const response = await searchUsers(keyword, 1, 50, frontendJWT);
|
||||
|
||||
if (response.success && response.data?.users) {
|
||||
return convertSearchResultsToTreeNodes(response.data.users);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
console.error('[handleSearchUsers] 搜索用户失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 从localStorage读取当前登录用户的完整信息
|
||||
useEffect(() => {
|
||||
const loadCurrentUserInfoFromStorage = () => {
|
||||
if (!currentUserId || !userInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果已经获取过,跳过
|
||||
if (searchedUsers.has(currentUserId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 从localStorage读取user_info
|
||||
const storedUserInfo = typeof window !== 'undefined'
|
||||
? localStorage.getItem('user_info')
|
||||
: null;
|
||||
|
||||
if (storedUserInfo) {
|
||||
const parsedUserInfo = JSON.parse(storedUserInfo);
|
||||
// console.log('[loadCurrentUserInfoFromStorage] 从localStorage读取用户信息:', parsedUserInfo);
|
||||
|
||||
// 构建符合UserInfo接口的数据
|
||||
const fullUserInfo: UserInfo = {
|
||||
id: parsedUserInfo.user_id || userInfo.user_id,
|
||||
username: parsedUserInfo.username || userInfo.username,
|
||||
nick_name: parsedUserInfo.nick_name || userInfo.nick_name,
|
||||
area: parsedUserInfo.area || userInfo.area,
|
||||
ou_id: parsedUserInfo.ou_id || userInfo.ou_id,
|
||||
ou_name: parsedUserInfo.ou_name || userInfo.ou_name,
|
||||
is_leader: parsedUserInfo.is_leader ?? false,
|
||||
status: 0,
|
||||
// 以下字段可能存在于localStorage中
|
||||
tenant_name: parsedUserInfo.tenant_name || null,
|
||||
dep_name: parsedUserInfo.dep_name || null,
|
||||
dep_short_name: parsedUserInfo.dep_short_name || null,
|
||||
email: parsedUserInfo.email,
|
||||
phone_number: parsedUserInfo.phone_number
|
||||
};
|
||||
|
||||
// 存储到searchedUsers中,供渲染时使用(使用currentUserId作为key)
|
||||
setSearchedUsers(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(currentUserId, fullUserInfo);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[loadCurrentUserInfoFromStorage] 读取localStorage用户信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadCurrentUserInfoFromStorage();
|
||||
}, [currentUserId, userInfo]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
@@ -630,7 +680,7 @@ export default function CrossCheckingUpload() {
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => {
|
||||
console.log('点击返回列表按钮');
|
||||
// console.log('点击返回列表按钮');
|
||||
navigate('/cross-checking');
|
||||
}}
|
||||
>
|
||||
@@ -646,7 +696,7 @@ export default function CrossCheckingUpload() {
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
<div className="flex justify-center">
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1000px'}}>
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6" style={{width: '1200px'}}>
|
||||
<div className="flex flex-row justify-center gap-12">
|
||||
{/* 左侧树状多选 */}
|
||||
<div style={{ minWidth: 300, width: '40%' }}>
|
||||
@@ -665,21 +715,54 @@ export default function CrossCheckingUpload() {
|
||||
) : (
|
||||
<MultiCascader
|
||||
options={userSelectionState.treeData}
|
||||
selectedUsers={groupChecked}
|
||||
placeholder="请选择评查小组成员"
|
||||
value={groupChecked}
|
||||
onChange={(values: string[]) => {
|
||||
// 确保当前用户始终被选中
|
||||
let newValues = values;
|
||||
if (currentUserId && !values.includes(currentUserId)) {
|
||||
newValues = [currentUserId, ...values];
|
||||
}
|
||||
setGroupChecked(newValues);
|
||||
// 移除已被取消选中的负责人
|
||||
setLeaderIds(prev => prev.filter(id => newValues.includes(id)));
|
||||
}}
|
||||
maxHeight={460}
|
||||
searchable={true}
|
||||
searchPlaceholder="搜索成员..."
|
||||
onLoadChildren={handleLoadChildren}
|
||||
onSearchUsers={handleSearchUsers}
|
||||
onTreeDataChange={(newTreeData) => {
|
||||
// 同步懒加载后的树数据到父组件
|
||||
setUserSelectionState(prev => ({
|
||||
...prev,
|
||||
treeData: newTreeData
|
||||
}));
|
||||
}}
|
||||
onAddUser={(userNode) => {
|
||||
// 从树中添加用户
|
||||
if (groupChecked.includes(userNode.value)) {
|
||||
toastService.warning('该成员已添加');
|
||||
return;
|
||||
}
|
||||
const newGroupChecked = [...groupChecked, userNode.value];
|
||||
setGroupChecked(newGroupChecked);
|
||||
// 存储用户信息
|
||||
if (userNode.userInfo) {
|
||||
setSearchedUsers(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(userNode.value, userNode.userInfo);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSearchUserAdded={(userNode) => {
|
||||
// 从搜索结果添加用户
|
||||
if (groupChecked.includes(userNode.value)) {
|
||||
toastService.warning('该成员已添加');
|
||||
return;
|
||||
}
|
||||
const newGroupChecked = [...groupChecked, userNode.value];
|
||||
setGroupChecked(newGroupChecked);
|
||||
// 存储用户信息
|
||||
if (userNode.userInfo) {
|
||||
setSearchedUsers(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(userNode.value, userNode.userInfo);
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -696,22 +779,37 @@ export default function CrossCheckingUpload() {
|
||||
if (b === currentUserId) return 1;
|
||||
return 0;
|
||||
}).map((member, index) => {
|
||||
let displayName: string = member;
|
||||
let displayOrg = '';
|
||||
const isUser = member.startsWith('user_');
|
||||
const isCurrentUser = member === currentUserId;
|
||||
const isLeader = leaderIds.includes(member);
|
||||
|
||||
let displayName: string;
|
||||
let tenantName: string;
|
||||
let depName: string;
|
||||
let ouName: string;
|
||||
let fullOrgInfo: string; // 完整组织信息(用于tooltip)
|
||||
|
||||
if (isUser) {
|
||||
// 查找真实用户名
|
||||
const userName = findUserNameById(userSelectionState.treeData, member) || member.replace('user_', '');
|
||||
displayName = userName;
|
||||
displayOrg = '用户';
|
||||
// 获取用户完整信息(优先从搜索用户Map中查找)
|
||||
const userInfo = getUserInfoById(member, userSelectionState.treeData, searchedUsers);
|
||||
displayName = userInfo?.nick_name || member.replace('user_', '');
|
||||
|
||||
// 优先使用 organization_path 中的值(懒加载接口),为空则使用顶层字段(搜索接口)
|
||||
const orgPath = userInfo?.organization_path;
|
||||
tenantName = orgPath?.tenant_name || userInfo?.tenant_name || '';
|
||||
depName = orgPath?.dep_name || userInfo?.dep_name || '';
|
||||
ouName = userInfo?.ou_name || '';
|
||||
|
||||
// 完整信息:分公司-部门
|
||||
const depOuName = depName && ouName ? `${depName}-${ouName}` : (depName || ouName);
|
||||
fullOrgInfo = depOuName;
|
||||
} else {
|
||||
// 组织
|
||||
const parts = member.split('-');
|
||||
displayName = parts[parts.length - 1];
|
||||
displayOrg = parts.slice(0, -1).join(' - ') || '组织';
|
||||
// 组织(不应该有这种情况,因为只有用户才会被选中)
|
||||
displayName = member;
|
||||
tenantName = '';
|
||||
depName = '';
|
||||
ouName = '';
|
||||
fullOrgInfo = '组织';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -725,46 +823,78 @@ export default function CrossCheckingUpload() {
|
||||
: ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-gray-800 flex items-center gap-2">
|
||||
{displayName}
|
||||
{isCurrentUser && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-500 text-white">
|
||||
<i className="ri-user-star-fill mr-0.5"></i>主要负责人
|
||||
</span>
|
||||
)}
|
||||
{!isCurrentUser && isLeader && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)] text-white">
|
||||
<i className="ri-star-fill mr-0.5"></i>负责人
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 第一行:成员名称 + 总公司 */}
|
||||
<div className="font-medium text-gray-800 flex items-center gap-5">
|
||||
<span className="flex items-center gap-2">
|
||||
{displayName}
|
||||
{isCurrentUser && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-500 text-white">
|
||||
<i className="ri-user-star-fill mr-0.5"></i>主要负责人
|
||||
</span>
|
||||
)}
|
||||
{!isCurrentUser && isLeader && (
|
||||
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-[var(--color-primary)] text-white">
|
||||
<i className="ri-star-fill mr-0.5"></i>负责人
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{tenantName && (
|
||||
<span className="text-gray-500 text-[10px]">{tenantName}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-gray-500 mt-1">{displayOrg}</div>
|
||||
{/* 第二行:分公司-部门(省略显示) */}
|
||||
{(depName || ouName) && (
|
||||
<div
|
||||
className="text-gray-500 text-[10px] truncate mt-1"
|
||||
title={fullOrgInfo}
|
||||
>
|
||||
{depName}-{ouName}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* 当前用户不能取消选中,也不显示设为负责人按钮 */}
|
||||
{/* 当前用户不能删除,也不显示设为负责人按钮 */}
|
||||
{isCurrentUser ? (
|
||||
<span className="ml-2 px-2 py-1 rounded text-[10px] bg-gray-100 text-gray-400 cursor-not-allowed">
|
||||
不可更改
|
||||
</span>
|
||||
) : isUser ? (
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-2 px-2 py-1 rounded text-[10px] transition-colors ${
|
||||
isLeader
|
||||
? 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
||||
: 'bg-[var(--color-primary-light)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (isLeader) {
|
||||
<>
|
||||
{/* 设为负责人/取消负责人按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
className={`ml-2 px-2 py-1 rounded text-[10px] transition-colors ${
|
||||
isLeader
|
||||
? 'bg-gray-100 text-gray-500 hover:bg-gray-200'
|
||||
: 'bg-[var(--color-primary-light)] text-[var(--color-primary)] hover:bg-[var(--color-primary)] hover:text-white'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (isLeader) {
|
||||
setLeaderIds(prev => prev.filter(id => id !== member));
|
||||
} else {
|
||||
setLeaderIds(prev => [...prev, member]);
|
||||
}
|
||||
}}
|
||||
title={isLeader ? '取消负责人' : '设为负责人'}
|
||||
>
|
||||
{isLeader ? '取消负责人' : '设为负责人'}
|
||||
</button>
|
||||
{/* 删除按钮 */}
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 w-6 h-6 flex items-center justify-center rounded-full text-gray-400 hover:text-red-500 hover:bg-red-50 transition-colors"
|
||||
onClick={() => {
|
||||
// 从已选择列表中移除
|
||||
const newGroupChecked = groupChecked.filter(id => id !== member);
|
||||
setGroupChecked(newGroupChecked);
|
||||
// 同时从负责人列表中移除
|
||||
setLeaderIds(prev => prev.filter(id => id !== member));
|
||||
} else {
|
||||
setLeaderIds(prev => [...prev, member]);
|
||||
}
|
||||
}}
|
||||
title={isLeader ? '取消负责人' : '设为负责人'}
|
||||
>
|
||||
{isLeader ? '取消负责人' : '设为负责人'}
|
||||
</button>
|
||||
}}
|
||||
title="删除"
|
||||
>
|
||||
<i className="ri-close-line text-sm"></i>
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
@@ -805,7 +935,7 @@ export default function CrossCheckingUpload() {
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => {
|
||||
console.log('点击返回列表按钮');
|
||||
// console.log('点击返回列表按钮');
|
||||
navigate('/cross-checking');
|
||||
}}
|
||||
>
|
||||
@@ -974,7 +1104,7 @@ export default function CrossCheckingUpload() {
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => {
|
||||
console.log('点击返回列表按钮');
|
||||
// console.log('点击返回列表按钮');
|
||||
navigate('/cross-checking');
|
||||
}}
|
||||
>
|
||||
|
||||
+13
-4
@@ -148,6 +148,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}
|
||||
|
||||
console.log("✅ [Login Action] 登录成功,准备创建 session");
|
||||
// console.log("📦 [Login Action] 后端返回完整数据:", response.data);
|
||||
console.log("👤 [Login Action] 用户角色:", user_info.user_role); // 应该是 "admin"
|
||||
console.log("⏰ [Login Action] Token 有效期:", expires_in, "秒 (", expires_in / 3600, "小时)");
|
||||
|
||||
@@ -168,8 +169,12 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
ou_name: user_info.ou_name,
|
||||
is_leader: user_info.is_leader,
|
||||
user_role: user_info.user_role,
|
||||
area: user_info.area, // 🔑 用户所属地区
|
||||
sub: user_info.sub
|
||||
area: user_info.area,
|
||||
sub: user_info.sub,
|
||||
// 🔑 包含后端返回的组织信息字段(可能为null)
|
||||
tenant_name: user_info.tenant_name,
|
||||
dep_name: user_info.dep_name,
|
||||
dep_short_name: user_info.dep_short_name,
|
||||
})));
|
||||
callbackUrl.searchParams.set('redirectTo', redirectTo);
|
||||
|
||||
@@ -191,8 +196,12 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
ou_name: user_info.ou_name,
|
||||
is_leader: user_info.is_leader,
|
||||
user_role: user_info.user_role,
|
||||
area: user_info.area, // 🔑 用户所属地区
|
||||
sub: user_info.sub
|
||||
area: user_info.area,
|
||||
sub: user_info.sub,
|
||||
// 🔑 包含后端返回的组织信息字段(可能为null)
|
||||
tenant_name: user_info.tenant_name,
|
||||
dep_name: user_info.dep_name,
|
||||
dep_short_name: user_info.dep_short_name,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { ClientLoaderFunctionArgs, ClientActionFunctionArgs } from "@remix-run/react";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getRolePermissions,
|
||||
getRoleUsers,
|
||||
getAllUsers,
|
||||
getUsersWithRoles,
|
||||
assignUserRoles,
|
||||
createRole,
|
||||
updateRole,
|
||||
@@ -536,52 +537,76 @@ interface AssignUserModalProps {
|
||||
}
|
||||
|
||||
function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, currentUserArea }: AssignUserModalProps) {
|
||||
// 用户列表数据
|
||||
const [allUsers, setAllUsers] = useState<UserInfo[]>([]);
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
||||
|
||||
// 搜索和分页状态
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize] = useState(50); // 默认50条
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [loadingUsers, setLoadingUsers] = useState(false);
|
||||
// 存储每个用户的角色信息
|
||||
const [userRolesMap, setUserRolesMap] = useState<Map<number, RoleInfo[]>>(new Map());
|
||||
|
||||
// 防抖搜索:使用 useCallback 和 useRef 实现
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 当搜索词变化时,500ms 后触发搜索
|
||||
useEffect(() => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
setDebouncedSearchTerm(searchTerm);
|
||||
setPage(1); // 搜索时重置到第一页
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [searchTerm]);
|
||||
|
||||
// 当模态框打开时,加载用户列表
|
||||
useEffect(() => {
|
||||
if (isOpen && role) {
|
||||
loadUsers();
|
||||
}
|
||||
}, [isOpen, role]);
|
||||
}, [isOpen, role, debouncedSearchTerm, page]); // 添加 page 依赖,分页变化时重新加载
|
||||
|
||||
// 加载所有用户及其角色信息
|
||||
// 加载用户及其角色信息(使用新的 v3 API)
|
||||
const loadUsers = async () => {
|
||||
setLoadingUsers(true);
|
||||
try {
|
||||
const users = await getAllUsers();
|
||||
const params: { page: number; page_size: number; area?: string; nick_name?: string } = {
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
|
||||
// v3.3: 市级管理员只能看到同地区的用户(使用 area 字段)
|
||||
let filteredUsers = users;
|
||||
// 市级管理员只能看到同地区的用户
|
||||
if (isCityAdmin && currentUserArea) {
|
||||
filteredUsers = users.filter(user => user.area === currentUserArea);
|
||||
console.log('🔒 [AssignUserModal v3.3] 市级管理员用户过滤:', {
|
||||
当前地区: currentUserArea,
|
||||
原始用户数: users.length,
|
||||
过滤后用户数: filteredUsers.length
|
||||
});
|
||||
params.area = currentUserArea;
|
||||
}
|
||||
|
||||
setAllUsers(filteredUsers);
|
||||
// 搜索关键词
|
||||
if (debouncedSearchTerm.trim()) {
|
||||
params.nick_name = debouncedSearchTerm.trim();
|
||||
}
|
||||
|
||||
// 批量获取每个用户的角色
|
||||
const rolesMap = new Map<number, RoleInfo[]>();
|
||||
await Promise.all(
|
||||
filteredUsers.map(async (user) => {
|
||||
const roles = await getUserRoles(user.id);
|
||||
rolesMap.set(user.id, roles);
|
||||
})
|
||||
);
|
||||
setUserRolesMap(rolesMap);
|
||||
const result = await getUsersWithRoles(params);
|
||||
|
||||
setAllUsers(result.items);
|
||||
setTotal(result.total);
|
||||
} catch (error) {
|
||||
console.error('加载用户列表失败:', error);
|
||||
console.error('❌ [AssignUserModal] 加载用户列表失败:', error);
|
||||
toastService.error('加载用户列表失败');
|
||||
setAllUsers([]);
|
||||
setTotal(0);
|
||||
} finally {
|
||||
setLoadingUsers(false);
|
||||
}
|
||||
@@ -591,6 +616,9 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
||||
const resetState = () => {
|
||||
setSelectedUserIds([]);
|
||||
setSearchTerm('');
|
||||
setDebouncedSearchTerm('');
|
||||
setPage(1);
|
||||
setTotal(0);
|
||||
};
|
||||
|
||||
// 提交分配
|
||||
@@ -604,17 +632,17 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
// 检查每个用户是否已有角色
|
||||
// 检查每个用户是否已有角色(使用已加载的角色数据)
|
||||
const usersWithRoles: Array<{ userId: number; userName: string; roleName: string }> = [];
|
||||
|
||||
for (const userId of selectedUserIds) {
|
||||
const userRoles = await getUserRoles(userId);
|
||||
if (userRoles.length > 0) {
|
||||
const user = allUsers.find(u => u.id === userId);
|
||||
const user = allUsers.find(u => u.id === userId);
|
||||
// 使用新 API 返回的 roles 数据
|
||||
if (user && user.roles && user.roles.length > 0) {
|
||||
usersWithRoles.push({
|
||||
userId,
|
||||
userName: user?.nick_name || user?.username || `用户${userId}`,
|
||||
roleName: userRoles.map(r => r.role_name).join('、')
|
||||
userName: user.nick_name || user.username || `用户${userId}`,
|
||||
roleName: user.roles.map(r => r.role_name).join('、')
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -665,28 +693,69 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 全选/取消全选
|
||||
// 全选/取消全选(当前页)
|
||||
const handleToggleAll = () => {
|
||||
const filteredUserIds = filteredUsers.map(u => u.id);
|
||||
if (selectedUserIds.length === filteredUserIds.length) {
|
||||
setSelectedUserIds([]);
|
||||
const currentPageUserIds = allUsers.map(u => u.id);
|
||||
if (currentPageUserIds.every(id => selectedUserIds.includes(id))) {
|
||||
// 当前页已全选,则取消全选
|
||||
setSelectedUserIds(selectedUserIds.filter(id => !currentPageUserIds.includes(id)));
|
||||
} else {
|
||||
setSelectedUserIds(filteredUserIds);
|
||||
// 全选当前页
|
||||
const newSelectedIds = new Set([...selectedUserIds, ...currentPageUserIds]);
|
||||
setSelectedUserIds(Array.from(newSelectedIds));
|
||||
}
|
||||
};
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = Math.ceil(total / pageSize);
|
||||
|
||||
// 生成页码列表
|
||||
const getPageNumbers = (): (number | string)[] => {
|
||||
const pages: (number | string)[] = [];
|
||||
const maxVisiblePages = 7; // 最多显示7个页码按钮
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
// 总页数小于等于最大显示数,显示所有页码
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
// 总页数较多,需要省略号
|
||||
pages.push(1); // 第一页
|
||||
|
||||
if (page > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
// 当前页附近的页码
|
||||
const startPage = Math.max(2, page - 1);
|
||||
const endPage = Math.min(totalPages - 1, page + 1);
|
||||
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (page < totalPages - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
pages.push(totalPages); // 最后一页
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage >= 1 && newPage <= totalPages && newPage !== page) {
|
||||
setPage(newPage);
|
||||
// 滚动到顶部
|
||||
document.querySelector('.users-checkbox-list')?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
if (!role) return null;
|
||||
|
||||
// 过滤用户
|
||||
const filteredUsers = allUsers.filter(user => {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
return (
|
||||
user.nick_name.toLowerCase().includes(searchLower) ||
|
||||
user.username.toLowerCase().includes(searchLower) ||
|
||||
user.ou_name.toLowerCase().includes(searchLower)
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -718,25 +787,38 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
||||
<i className="ri-search-line"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索用户(姓名、用户名、单位)"
|
||||
placeholder="搜索用户(姓名)"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
type="button"
|
||||
className="search-clear"
|
||||
onClick={() => setSearchTerm('')}
|
||||
title="清除搜索"
|
||||
>
|
||||
<i className="ri-close-line"></i>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 全选按钮 */}
|
||||
{/* 全选按钮和统计信息 */}
|
||||
<div className="select-all-bar">
|
||||
<label className="user-checkbox-item select-all">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={filteredUsers.length > 0 && selectedUserIds.length === filteredUsers.length}
|
||||
checked={allUsers.length > 0 && allUsers.every(u => selectedUserIds.includes(u.id))}
|
||||
onChange={handleToggleAll}
|
||||
disabled={filteredUsers.length === 0}
|
||||
disabled={allUsers.length === 0}
|
||||
/>
|
||||
<span className="user-name">
|
||||
全选 ({selectedUserIds.length} / {filteredUsers.length})
|
||||
全选当前页 ({allUsers.length > 0 ? allUsers.filter(u => selectedUserIds.includes(u.id)).length : 0} / {allUsers.length})
|
||||
</span>
|
||||
</label>
|
||||
<span className="page-info">
|
||||
共 {total} 个用户,第 {page} / {totalPages} 页
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 用户复选框列表 */}
|
||||
@@ -746,65 +828,109 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, curren
|
||||
<span>加载用户中...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="users-checkbox-list" style={{ maxHeight: '400px', minHeight: '400px', overflow: 'auto' }}>
|
||||
{filteredUsers.length > 0 ? (
|
||||
filteredUsers.map(user => {
|
||||
const userRoles = userRolesMap.get(user.id) || [];
|
||||
const hasRoles = userRoles.length > 0;
|
||||
<>
|
||||
<div className="users-checkbox-list" style={{ maxHeight: '400px', minHeight: '400px', overflow: 'auto' }}>
|
||||
{allUsers.length > 0 ? (
|
||||
allUsers.map(user => {
|
||||
const userRoles = user.roles || [];
|
||||
const hasRoles = userRoles.length > 0;
|
||||
|
||||
return (
|
||||
<label key={user.id} className="user-checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedUserIds.includes(user.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedUserIds([...selectedUserIds, user.id]);
|
||||
} else {
|
||||
setSelectedUserIds(selectedUserIds.filter(id => id !== user.id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="user-info">
|
||||
<div className="user-name">
|
||||
{user.nick_name}
|
||||
{user.is_leader && (
|
||||
<span className="leader-badge" style={{ marginLeft: '8px' }}>负责人</span>
|
||||
)}
|
||||
{hasRoles && (
|
||||
<span
|
||||
className="role-badge"
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
padding: '2px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
color: '#0284c7',
|
||||
border: '1px solid #bae6fd',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
title={`当前角色: ${userRoles.map(r => r.role_name).join('、')}`}
|
||||
>
|
||||
{userRoles.map(r => r.role_name).join('、')}
|
||||
</span>
|
||||
)}
|
||||
return (
|
||||
<label key={user.id} className="user-checkbox-item">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedUserIds.includes(user.id)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedUserIds([...selectedUserIds, user.id]);
|
||||
} else {
|
||||
setSelectedUserIds(selectedUserIds.filter(id => id !== user.id));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="user-info">
|
||||
<div className="user-name">
|
||||
{user.nick_name}
|
||||
{user.is_leader && (
|
||||
<span className="leader-badge" style={{ marginLeft: '8px' }}>负责人</span>
|
||||
)}
|
||||
{hasRoles && (
|
||||
<span
|
||||
className="role-badge"
|
||||
style={{
|
||||
marginLeft: '8px',
|
||||
padding: '2px 8px',
|
||||
fontSize: '12px',
|
||||
backgroundColor: '#f0f9ff',
|
||||
color: '#0284c7',
|
||||
border: '1px solid #bae6fd',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
title={`当前角色: ${userRoles.map(r => r.role_name).join('、')}`}
|
||||
>
|
||||
{userRoles.map(r => r.role_name).join('、')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="user-meta">
|
||||
{user.tenant_name}
|
||||
{user.dep_name && ` • ${user.dep_name}`}
|
||||
{user.ou_name && ` • ${user.ou_name}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-meta">
|
||||
@{user.username} • {user.ou_name}
|
||||
{user.area && ` • ${user.area}`}
|
||||
{user.phone_number && ` • ${user.phone_number}`}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="empty-state" style={{ padding: '40px 20px' }}>
|
||||
<i className="ri-user-search-line"></i>
|
||||
<p>{searchTerm ? '没有找到匹配的用户' : '没有可分配的用户'}</p>
|
||||
</label>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="empty-state" style={{ padding: '40px 20px' }}>
|
||||
<i className="ri-user-search-line"></i>
|
||||
<p>{searchTerm ? '没有找到匹配的用户' : '没有可分配的用户'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页组件 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="pagination-container">
|
||||
<button
|
||||
type="button"
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(page - 1)}
|
||||
disabled={page === 1}
|
||||
title="上一页"
|
||||
>
|
||||
<i className="ri-arrow-left-s-line"></i>
|
||||
</button>
|
||||
|
||||
{getPageNumbers().map((pageNum, index) => (
|
||||
typeof pageNum === 'number' ? (
|
||||
<button
|
||||
key={index}
|
||||
type="button"
|
||||
className={`pagination-page ${page === pageNum ? 'active' : ''}`}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
) : (
|
||||
<span key={index} className="pagination-ellipsis">
|
||||
{pageNum}
|
||||
</span>
|
||||
)
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="pagination-btn"
|
||||
onClick={() => handlePageChange(page + 1)}
|
||||
disabled={page === totalPages}
|
||||
title="下一页"
|
||||
>
|
||||
<i className="ri-arrow-right-s-line"></i>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1961,7 +2087,7 @@ export default function RolePermissions() {
|
||||
</div>
|
||||
<div className="user-username">@{user.username}</div>
|
||||
<div className="user-org">
|
||||
{/* {JSON.stringify(user)} */}
|
||||
{JSON.stringify(user)}
|
||||
{user.ou_name}
|
||||
{user.area && <span style={{ marginLeft: '8px', color: '#666' }}>• {user.area}</span>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user