1. 登录返回总公司,分公司,部门信息。
2. 修改角色权限管理的分配用户的数据渲染和接口。 3. 交叉评查任务的创建的组织架构组件的重构。
This commit is contained in:
@@ -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