Files
smart-crop-ui/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx

456 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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)}
title="查看"
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(user)}
title="编辑"
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleToggleStatus(user)}
title={user.isActive ? "冻结用户" : "激活用户"}
>
{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)}
title="重置密码"
>
<Lock className="w-4 h-4" />
</Button>
</div>
),
},
], []);
// 加载用户数据
const loadUsers = useCallback(async (resetPage = false) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
debugger
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 : '加载用户数据失败'
});
}
}, []);
// 搜索处理
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();
}, []);
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>
);
}