1. 登录返回总公司,分公司,部门信息。

2. 修改角色权限管理的分配用户的数据渲染和接口。
3. 交叉评查任务的创建的组织架构组件的重构。
This commit is contained in:
2026-01-21 10:04:04 +08:00
parent 9951f16e50
commit b97d0e1a0b
12 changed files with 1348 additions and 14006 deletions
+301 -171
View File
@@ -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');
}}
>