452 lines
14 KiB
TypeScript
452 lines
14 KiB
TypeScript
/**
|
||
* filekorolheader: 用户管理页面 - 用户查询和管理页面
|
||
* 功能:用户列表查询、搜索筛选、详情查看、用户管理
|
||
* 路径:/central-config/tenant/user-management
|
||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SearchFormPagination公共组件,shadcn语义化样式
|
||
*/
|
||
'use client';
|
||
|
||
import { useReducer, useEffect, useState, useCallback, useMemo } from 'react';
|
||
import { toast } from 'sonner';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react';
|
||
import { UserDetailDialog } from './components/UserDetailDialog';
|
||
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
||
|
||
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
||
import { UserManagementHeader } from './components/UserManagementHeader';
|
||
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
||
import { UserFilters } from './types';
|
||
|
||
// 移除了Enterprise的引用,因为新实现中不再需要
|
||
|
||
// 用户管理状态管理
|
||
interface UserManagementState {
|
||
users: User[];
|
||
loading: boolean;
|
||
error: string | null;
|
||
pagination: PaginationState;
|
||
filters: UserFilters;
|
||
sortBy?: string;
|
||
sortOrder: 'asc' | 'desc';
|
||
selectedUser: User | null;
|
||
showDetailDialog: boolean;
|
||
}
|
||
|
||
type UserManagementAction =
|
||
| { type: 'SET_USERS'; payload: { data: User[]; pagination: PaginationState } }
|
||
| { type: 'SET_LOADING'; payload: boolean }
|
||
| { type: 'SET_ERROR'; payload: string | null }
|
||
| { type: 'SET_FILTERS'; payload: Partial<UserFilters> }
|
||
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||
| { type: 'SET_PAGINATION'; payload: Partial<PaginationState> }
|
||
| { type: 'SET_SELECTED_USER'; payload: User | null }
|
||
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||
| { type: 'REFRESH_DATA' };
|
||
|
||
const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => {
|
||
switch (action.type) {
|
||
case 'SET_USERS':
|
||
return {
|
||
...state,
|
||
users: action.payload.data,
|
||
pagination: action.payload.pagination,
|
||
loading: false,
|
||
error: null,
|
||
};
|
||
case 'SET_LOADING':
|
||
return { ...state, loading: action.payload };
|
||
case 'SET_ERROR':
|
||
return { ...state, error: action.payload, loading: false };
|
||
case 'SET_FILTERS':
|
||
return { ...state, filters: { ...state.filters, ...action.payload } };
|
||
case 'SET_SORT':
|
||
return { ...state, sortBy: action.payload.sortBy, sortOrder: action.payload.sortOrder };
|
||
case 'SET_PAGINATION':
|
||
return { ...state, pagination: { ...state.pagination, ...action.payload } };
|
||
case 'SET_SELECTED_USER':
|
||
return { ...state, selectedUser: action.payload };
|
||
case 'TOGGLE_DETAIL_DIALOG':
|
||
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||
case 'REFRESH_DATA':
|
||
return { ...state, error: null };
|
||
default:
|
||
return state;
|
||
}
|
||
};
|
||
|
||
const initialState: UserManagementState = {
|
||
users: [],
|
||
loading: false,
|
||
error: null,
|
||
pagination: {
|
||
page: 1,
|
||
size: 10,
|
||
total: 0,
|
||
totalPages: 0,
|
||
hasNext: false,
|
||
hasPrev: false,
|
||
},
|
||
filters: {
|
||
searchKeyword: '',
|
||
statusFilter: 'all',
|
||
typeFilter: 'all'
|
||
},
|
||
sortBy: 'created_at',
|
||
sortOrder: 'desc',
|
||
selectedUser: null,
|
||
showDetailDialog: false,
|
||
};
|
||
|
||
export default function TenantUserManagementPage() {
|
||
const [state, dispatch] = useReducer(userManagementReducer, initialState);
|
||
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({});
|
||
|
||
// 搜索字段配置
|
||
const searchFields: SearchFieldConfig[] = useMemo(() => [
|
||
{
|
||
key: 'search',
|
||
label: '搜索',
|
||
type: 'text',
|
||
placeholder: '搜索用户名、姓名、邮箱...',
|
||
},
|
||
{
|
||
key: 'status',
|
||
label: '用户状态',
|
||
type: 'select',
|
||
defaultValue: 'all',
|
||
options: [
|
||
{ value: 'all', label: '全部状态' },
|
||
{ value: 'active', label: '活跃' },
|
||
{ value: 'inactive', label: '未激活' },
|
||
],
|
||
},
|
||
{
|
||
key: 'type',
|
||
label: '用户类型',
|
||
type: 'select',
|
||
defaultValue: 'all',
|
||
options: [
|
||
{ value: 'all', label: '全部类型' },
|
||
{ value: 'admin', label: '管理员' },
|
||
{ value: 'user', label: '普通用户' },
|
||
{ value: 'staff', label: '员工' },
|
||
],
|
||
},
|
||
], []);
|
||
|
||
// 表格列配置
|
||
const columns: TableColumnConfig[] = useMemo(() => [
|
||
{
|
||
key: 'username',
|
||
label: '用户名',
|
||
sortable: true,
|
||
render: (value: string, user: User) => (
|
||
<div className="font-medium">{value}</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'fullName',
|
||
label: '姓名',
|
||
sortable: true,
|
||
render: (value: string) => value || '-',
|
||
},
|
||
{
|
||
key: 'email',
|
||
label: '邮箱',
|
||
sortable: true,
|
||
render: (value: string) => value || '-',
|
||
},
|
||
{
|
||
key: 'isActive',
|
||
label: '状态',
|
||
sortable: true,
|
||
render: (value: boolean) => (
|
||
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
||
value
|
||
? 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400'
|
||
: 'bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400'
|
||
}`}>
|
||
<div className={`w-2 h-2 rounded-full ${value ? 'bg-green-500' : 'bg-red-500'}`} />
|
||
{value ? '活跃' : '未激活'}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'isSuperuser',
|
||
label: '角色',
|
||
sortable: true,
|
||
render: (value: boolean, user: User) => {
|
||
if (value) {
|
||
return (
|
||
<div className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400">
|
||
<div className="w-2 h-2 rounded-full bg-purple-500" />
|
||
超级管理员
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400">
|
||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||
普通用户
|
||
</div>
|
||
);
|
||
},
|
||
},
|
||
{
|
||
key: 'isVerified',
|
||
label: '验证',
|
||
sortable: true,
|
||
render: (value: boolean) => (
|
||
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
||
value
|
||
? 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400'
|
||
: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400'
|
||
}`}>
|
||
{value ? '已验证' : '未验证'}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'lastLoginAt',
|
||
label: '最后登录',
|
||
sortable: true,
|
||
render: (value: string) => {
|
||
if (!value) return '-';
|
||
try {
|
||
const date = new Date(value);
|
||
return date.toLocaleDateString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
});
|
||
} catch {
|
||
return value;
|
||
}
|
||
},
|
||
},
|
||
{
|
||
key: 'actions',
|
||
label: '操作',
|
||
render: (_, user: User) => (
|
||
<div className="flex items-center gap-2">
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleViewDetail(user)}
|
||
>
|
||
<Eye className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleEdit(user)}
|
||
>
|
||
<Edit className="w-4 h-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleToggleStatus(user)}
|
||
>
|
||
{user.isActive ? (
|
||
<UserX className="w-4 h-4 text-orange-600" />
|
||
) : (
|
||
<UserCheck className="w-4 h-4 text-green-600" />
|
||
)}
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleResetPassword(user)}
|
||
>
|
||
<Lock className="w-4 h-4" />
|
||
</Button>
|
||
</div>
|
||
),
|
||
},
|
||
], []);
|
||
|
||
// 加载用户数据
|
||
const loadUsers = useCallback(async (resetPage = false) => {
|
||
try {
|
||
dispatch({ type: 'SET_LOADING', payload: true });
|
||
|
||
const params: UsersQueryParams = {
|
||
page: resetPage ? 1 : state.pagination.page,
|
||
size: state.pagination.size,
|
||
is_active: true,
|
||
};
|
||
|
||
// 添加搜索条件
|
||
if (searchFilters.search) {
|
||
params.search = searchFilters.search;
|
||
}
|
||
|
||
if (searchFilters.status && searchFilters.status !== 'all') {
|
||
params.is_active = searchFilters.status === 'active';
|
||
}
|
||
|
||
if (searchFilters.type && searchFilters.type !== 'all') {
|
||
// For user type filtering, we'll need to handle this differently based on the API
|
||
// For now, we'll filter on the client side if needed
|
||
}
|
||
|
||
if (state.sortBy) {
|
||
params.order_by = state.sortBy;
|
||
params.sort_order = state.sortOrder;
|
||
}
|
||
|
||
const response: UsersApiResponse = await fetchUsers(params);
|
||
const transformedUsers = response.data.map(transformUserData);
|
||
|
||
dispatch({
|
||
type: 'SET_USERS',
|
||
payload: {
|
||
data: transformedUsers,
|
||
pagination: {
|
||
page: response.page,
|
||
size: response.size,
|
||
total: response.total,
|
||
totalPages: response.total_pages,
|
||
hasNext: response.has_next,
|
||
hasPrev: response.has_prev,
|
||
}
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to load users:', error);
|
||
dispatch({
|
||
type: 'SET_ERROR',
|
||
payload: error instanceof Error ? error.message : '加载用户数据失败'
|
||
});
|
||
}
|
||
}, [state.pagination.page, state.pagination.size, state.sortBy, state.sortOrder, searchFilters]);
|
||
|
||
// 搜索处理
|
||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||
setSearchFilters(filters);
|
||
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
|
||
}, []);
|
||
|
||
// 排序处理
|
||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
|
||
}, []);
|
||
|
||
// 分页处理
|
||
const handlePageChange = useCallback((page: number) => {
|
||
if (page < 1) {
|
||
page = 1;
|
||
} else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) {
|
||
page = state.pagination.totalPages;
|
||
}
|
||
dispatch({ type: 'SET_PAGINATION', payload: { page } });
|
||
}, [state.pagination.totalPages]);
|
||
|
||
// 每页条数变化处理
|
||
const handleSizeChange = useCallback((size: number) => {
|
||
dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } });
|
||
}, []);
|
||
|
||
// 查看详情
|
||
const handleViewDetail = (user: User) => {
|
||
dispatch({ type: 'SET_SELECTED_USER', payload: user });
|
||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||
};
|
||
|
||
// 编辑用户
|
||
const handleEdit = (user: User) => {
|
||
toast.info('编辑功能开发中...');
|
||
};
|
||
|
||
// 切换用户状态
|
||
const handleToggleStatus = (user: User) => {
|
||
const newStatus = !user.isActive;
|
||
const statusText = newStatus ? '激活' : '停用';
|
||
if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return;
|
||
toast.info(`${statusText}功能开发中...`);
|
||
};
|
||
|
||
// 重置密码
|
||
const handleResetPassword = (user: User) => {
|
||
if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return;
|
||
toast.info('重置密码功能开发中...');
|
||
};
|
||
|
||
// 统计数据计算
|
||
const stats = useMemo(() => [
|
||
{
|
||
label: '总用户数',
|
||
value: state.pagination.total,
|
||
color: 'text-blue-600 dark:text-blue-400',
|
||
bg: 'bg-blue-50 dark:bg-blue-950',
|
||
},
|
||
{
|
||
label: '活跃用户',
|
||
value: state.users.filter(u => u.isActive).length,
|
||
color: 'text-green-600 dark:text-green-400',
|
||
bg: 'bg-green-50 dark:bg-green-950',
|
||
},
|
||
{
|
||
label: '管理员',
|
||
value: state.users.filter(u => u.isSuperuser).length,
|
||
color: 'text-purple-600 dark:text-purple-400',
|
||
bg: 'bg-purple-50 dark:bg-purple-950',
|
||
},
|
||
{
|
||
label: '已验证',
|
||
value: state.users.filter(u => u.isVerified).length,
|
||
color: 'text-orange-600 dark:text-orange-400',
|
||
bg: 'bg-orange-50 dark:bg-orange-950',
|
||
},
|
||
], [state.users, state.pagination.total]);
|
||
|
||
// 加载数据
|
||
useEffect(() => {
|
||
loadUsers();
|
||
}, [loadUsers]);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 页面标题 */}
|
||
<UserManagementHeader stats={stats} />
|
||
|
||
{/* 统计卡片 */}
|
||
<UserManagementStatsCards stats={stats} />
|
||
|
||
{/* 搜索表单、数据表格和分页 */}
|
||
<SearchFormPagination
|
||
formTitle="用户列表"
|
||
formRightContent={
|
||
<Button onClick={() => toast.info('新建用户功能开发中...')}>
|
||
新建用户
|
||
</Button>
|
||
}
|
||
searchFields={searchFields}
|
||
columns={columns}
|
||
data={state.users}
|
||
loading={state.loading}
|
||
error={state.error}
|
||
pagination={state.pagination}
|
||
sortBy={state.sortBy}
|
||
sortOrder={state.sortOrder}
|
||
onPageChange={handlePageChange}
|
||
onSizeChange={handleSizeChange}
|
||
onSearch={handleSearch}
|
||
onSort={handleSort}
|
||
emptyText="暂无用户数据"
|
||
sizeOptions={[10, 20, 50, 100]}
|
||
/>
|
||
|
||
{/* 用户详情对话框 */}
|
||
<UserDetailDialog
|
||
open={state.showDetailDialog}
|
||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||
user={state.selectedUser}
|
||
/>
|
||
</div>
|
||
);
|
||
} |