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
+233 -107
View File
@@ -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>