Compare commits
2 Commits
c690d50baa
...
f1c3c23127
| Author | SHA1 | Date | |
|---|---|---|---|
| f1c3c23127 | |||
| 394e6d8342 |
@@ -0,0 +1,240 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 审核历史API接口 - 企业审核记录查询接口服务
|
||||||
|
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||||
|
* 路径:/central-config/tenant/audit-history/components/auditHistoryApi
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
// API响应数据类型定义
|
||||||
|
import { getAuthToken } from "@/utils/token.ts";
|
||||||
|
import {
|
||||||
|
getTenantAuditLogsApiV1TenantsAuditLogsGet,
|
||||||
|
} from "@/lib/api/sdk.gen";
|
||||||
|
|
||||||
|
export interface AuditLogData {
|
||||||
|
id: string;
|
||||||
|
action: string;
|
||||||
|
action_by: string;
|
||||||
|
action_time: string;
|
||||||
|
snapshot_company_name: string;
|
||||||
|
snapshot_company_type: string | null;
|
||||||
|
snapshot_province: string | null;
|
||||||
|
snapshot_city: string | null;
|
||||||
|
snapshot_district: string | null;
|
||||||
|
snapshot_detailed_address: string | null;
|
||||||
|
snapshot_registrant: string | null;
|
||||||
|
snapshot_contact_phone: string | null;
|
||||||
|
snapshot_bank_account: string | null;
|
||||||
|
snapshot_bank_name: string | null;
|
||||||
|
snapshot_bank_full_name: string | null;
|
||||||
|
snapshot_bank_address: string | null;
|
||||||
|
snapshot_social_credit_code: string | null;
|
||||||
|
snapshot_legal_person_name: string | null;
|
||||||
|
snapshot_audit_status: string;
|
||||||
|
snapshot_audit_comment: string | null;
|
||||||
|
snapshot_company_scale: string | null;
|
||||||
|
snapshot_registered_capital: string | null;
|
||||||
|
change_summary: string;
|
||||||
|
ip_address: string | null;
|
||||||
|
user_agent: string | null;
|
||||||
|
request_id: string | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应接口
|
||||||
|
export interface AuditLogsApiResponse {
|
||||||
|
data: AuditLogData[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_prev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数接口
|
||||||
|
export interface AuditLogsQueryParams {
|
||||||
|
tenant_id?: string;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
order_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 审核记录页面数据类型(转换后的)
|
||||||
|
export interface AuditRecord {
|
||||||
|
id: string;
|
||||||
|
enterpriseId?: string;
|
||||||
|
enterpriseName: string;
|
||||||
|
action: 'SUBMIT' | 'AUDIT';
|
||||||
|
auditType: 'register' | 'update';
|
||||||
|
submitTime: string;
|
||||||
|
actionTime: string;
|
||||||
|
actionBy: string;
|
||||||
|
result: 'pending' | 'approved' | 'rejected' | 'draft';
|
||||||
|
auditStatus: string;
|
||||||
|
auditComment?: string;
|
||||||
|
changeSummary: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
// 快照数据
|
||||||
|
snapshot: {
|
||||||
|
companyName: string;
|
||||||
|
companyType: string | null;
|
||||||
|
province: string | null;
|
||||||
|
city: string | null;
|
||||||
|
district: string | null;
|
||||||
|
detailedAddress: string | null;
|
||||||
|
registrant: string | null;
|
||||||
|
contactPhone: string | null;
|
||||||
|
bankAccount: string | null;
|
||||||
|
bankName: string | null;
|
||||||
|
bankFullName: string | null;
|
||||||
|
bankAddress: string | null;
|
||||||
|
socialCreditCode: string | null;
|
||||||
|
legalPersonName: string | null;
|
||||||
|
auditStatus: string;
|
||||||
|
auditComment: string | null;
|
||||||
|
companyScale: string | null;
|
||||||
|
registeredCapital: string | null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审核历史记录数据
|
||||||
|
*/
|
||||||
|
export async function fetchAuditLogs(params: AuditLogsQueryParams = {}): Promise<AuditLogsApiResponse> {
|
||||||
|
try {
|
||||||
|
// 构建查询参数对象
|
||||||
|
const queryParams: any = {};
|
||||||
|
|
||||||
|
queryParams.tenant_id = "";
|
||||||
|
if (params.page) queryParams.page = params.page;
|
||||||
|
if (params.size) queryParams.size = params.size;
|
||||||
|
if (params.order_by) queryParams.order_by = params.order_by;
|
||||||
|
if (params.sort_order) queryParams.sort_order = params.sort_order;
|
||||||
|
|
||||||
|
// 默认参数
|
||||||
|
if (!params.page) queryParams.page = 1;
|
||||||
|
if (!params.size) queryParams.size = 10;
|
||||||
|
|
||||||
|
// 使用SDK API调用审核历史查询接口,添加缓存破坏器和认证头部
|
||||||
|
const token = getAuthToken();
|
||||||
|
const response = await getTenantAuditLogsApiV1TenantsAuditLogsGet({
|
||||||
|
query: {
|
||||||
|
...queryParams,
|
||||||
|
// 添加时间戳防止缓存
|
||||||
|
_t: Date.now(),
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
|
||||||
|
// 转换响应数据格式以匹配现有的接口
|
||||||
|
return {
|
||||||
|
data: data?.data || [],
|
||||||
|
total: data?.total || 0,
|
||||||
|
page: data?.page || 1,
|
||||||
|
size: data?.size || 10,
|
||||||
|
total_pages: data?.total_pages || 0,
|
||||||
|
has_next: data?.has_next || false,
|
||||||
|
has_prev: data?.has_prev || false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch audit logs:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将API数据转换为页面所需的审核记录格式
|
||||||
|
*/
|
||||||
|
export function transformAuditLogData(log: AuditLogData): AuditRecord {
|
||||||
|
// 判断审核类型
|
||||||
|
let auditType: 'register' | 'update' = 'register';
|
||||||
|
if (log.change_summary.includes('更新') || log.change_summary.includes('修改')) {
|
||||||
|
auditType = 'update';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射审核状态
|
||||||
|
let result: 'pending' | 'approved' | 'rejected' | 'draft' = 'pending';
|
||||||
|
const status = log.snapshot_audit_status.toLowerCase();
|
||||||
|
if (status.includes('草稿') || status.includes('draft')) {
|
||||||
|
result = 'draft';
|
||||||
|
} else if (status.includes('待审核') || status.includes('pending')) {
|
||||||
|
result = 'pending';
|
||||||
|
} else if (status.includes('已通过') || status.includes('approved')) {
|
||||||
|
result = 'approved';
|
||||||
|
} else if (status.includes('已拒绝') || status.includes('rejected')) {
|
||||||
|
result = 'rejected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: log.id,
|
||||||
|
enterpriseId: log.action_by, // 使用操作人作为企业ID关联
|
||||||
|
enterpriseName: log.snapshot_company_name,
|
||||||
|
action: log.action as 'SUBMIT' | 'AUDIT',
|
||||||
|
auditType,
|
||||||
|
submitTime: formatDate(log.action_time),
|
||||||
|
actionTime: formatDate(log.action_time),
|
||||||
|
actionBy: log.action_by,
|
||||||
|
result,
|
||||||
|
auditStatus: log.snapshot_audit_status,
|
||||||
|
auditComment: log.snapshot_audit_comment,
|
||||||
|
changeSummary: log.change_summary,
|
||||||
|
ipAddress: log.ip_address,
|
||||||
|
userAgent: log.user_agent,
|
||||||
|
requestId: log.request_id,
|
||||||
|
createdAt: formatDate(log.created_at),
|
||||||
|
|
||||||
|
// 快照数据
|
||||||
|
snapshot: {
|
||||||
|
companyName: log.snapshot_company_name,
|
||||||
|
companyType: log.snapshot_company_type,
|
||||||
|
province: log.snapshot_province,
|
||||||
|
city: log.snapshot_city,
|
||||||
|
district: log.snapshot_district,
|
||||||
|
detailedAddress: log.snapshot_detailed_address,
|
||||||
|
registrant: log.snapshot_registrant,
|
||||||
|
contactPhone: log.snapshot_contact_phone,
|
||||||
|
bankAccount: log.snapshot_bank_account,
|
||||||
|
bankName: log.snapshot_bank_name,
|
||||||
|
bankFullName: log.snapshot_bank_full_name,
|
||||||
|
bankAddress: log.snapshot_bank_address,
|
||||||
|
socialCreditCode: log.snapshot_social_credit_code,
|
||||||
|
legalPersonName: log.snapshot_legal_person_name,
|
||||||
|
auditStatus: log.snapshot_audit_status,
|
||||||
|
auditComment: log.snapshot_audit_comment,
|
||||||
|
companyScale: log.snapshot_company_scale,
|
||||||
|
registeredCapital: log.snapshot_registered_capital,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 审核历史状态管理 - 审核记录数据状态管理核心
|
||||||
|
* 功能:API数据管理、分页状态、加载状态、错误处理、筛选状态
|
||||||
|
* 路径:/central-config/tenant/audit-history/components/auditHistoryReducer
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理模式
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AuditRecord } from '../types';
|
||||||
|
|
||||||
|
export interface FilterOptions {
|
||||||
|
searchKeyword: string;
|
||||||
|
resultFilter: string;
|
||||||
|
typeFilter: string;
|
||||||
|
dateRange: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditHistoryState {
|
||||||
|
records: AuditRecord[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
};
|
||||||
|
filters: FilterOptions;
|
||||||
|
showDetailDialog: boolean;
|
||||||
|
selectedRecord: AuditRecord | null;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuditHistoryAction =
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'SET_RECORDS'; payload: { data: AuditRecord[]; pagination: AuditHistoryState['pagination'] } }
|
||||||
|
| { type: 'SET_FILTERS'; payload: Partial<FilterOptions> }
|
||||||
|
| { type: 'SET_PAGINATION'; payload: Partial<AuditHistoryState['pagination']> }
|
||||||
|
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||||
|
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_SELECTED_RECORD'; payload: AuditRecord | null }
|
||||||
|
| { type: 'REFRESH_DATA' };
|
||||||
|
|
||||||
|
// 初始状态
|
||||||
|
export const initialState: AuditHistoryState = {
|
||||||
|
records: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
searchKeyword: '',
|
||||||
|
resultFilter: 'all',
|
||||||
|
typeFilter: 'all',
|
||||||
|
dateRange: 'all',
|
||||||
|
},
|
||||||
|
showDetailDialog: false,
|
||||||
|
selectedRecord: null,
|
||||||
|
sortBy: undefined,
|
||||||
|
sortOrder: 'desc',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reducer
|
||||||
|
export function auditHistoryReducer(state: AuditHistoryState, action: AuditHistoryAction): AuditHistoryState {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_LOADING':
|
||||||
|
return { ...state, loading: action.payload };
|
||||||
|
|
||||||
|
case 'SET_ERROR':
|
||||||
|
return { ...state, error: action.payload, loading: false };
|
||||||
|
|
||||||
|
case 'SET_RECORDS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
records: action.payload.data,
|
||||||
|
pagination: action.payload.pagination,
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_FILTERS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
filters: { ...state.filters, ...action.payload },
|
||||||
|
pagination: { ...state.pagination, page: 1 }, // 重置到第一页
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_PAGINATION':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
pagination: { ...state.pagination, ...action.payload },
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'SET_SORT':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
sortBy: action.payload.sortBy,
|
||||||
|
sortOrder: action.payload.sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'TOGGLE_DETAIL_DIALOG':
|
||||||
|
return { ...state, showDetailDialog: action.payload };
|
||||||
|
|
||||||
|
case 'SET_SELECTED_RECORD':
|
||||||
|
return { ...state, selectedRecord: action.payload };
|
||||||
|
|
||||||
|
case 'REFRESH_DATA':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: null, // 清除错误状态
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,211 +1,634 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 审核历史页面 - 企业审核记录查询和管理页面
|
||||||
|
* 功能:审核历史记录查询、搜索筛选、详情查看、数据分析
|
||||||
|
* 路径:/central-config/tenant/audit-history
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式
|
||||||
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useReducer, useEffect, useMemo, useState } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Download } from 'lucide-react';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { AuditHistoryStatsCards } from './components/AuditHistoryStatsCards';
|
import { Input } from '@/components/ui/input';
|
||||||
import { AuditHistoryFilters } from './components/AuditHistoryFilters';
|
import { Label } from '@/components/ui/label';
|
||||||
import { AuditHistoryList } from './components/AuditHistoryList';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
import { AuditHistoryDetailDialog } from './components/AuditHistoryDetailDialog';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { AuditHistoryInstructions } from './components/AuditHistoryInstructions';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { AuditRecord, Enterprise, FilterOptions } from './types';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
History,
|
||||||
|
Search,
|
||||||
|
Calendar,
|
||||||
|
FileText,
|
||||||
|
AlertCircle,
|
||||||
|
RefreshCw,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Building,
|
||||||
|
User,
|
||||||
|
CreditCard,
|
||||||
|
Smartphone
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
import { fetchAuditLogs, transformAuditLogData, AuditLogsQueryParams, AuditLogData } from './components/auditHistoryApi';
|
||||||
|
|
||||||
|
// 审核历史状态管理
|
||||||
|
interface AuditHistoryState {
|
||||||
|
records: AuditLogData[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
};
|
||||||
|
filters: {
|
||||||
|
search_keyword: string;
|
||||||
|
typeFilter: string;
|
||||||
|
resultFilter: string;
|
||||||
|
dateRange: string;
|
||||||
|
};
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder: 'asc' | 'desc';
|
||||||
|
selectedRecord: AuditLogData | null;
|
||||||
|
showDetailDialog: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditHistoryAction =
|
||||||
|
| { type: 'SET_RECORDS'; payload: { data: AuditLogData[]; pagination: AuditHistoryState['pagination'] } }
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'SET_FILTERS'; payload: Partial<AuditHistoryState['filters']> }
|
||||||
|
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||||
|
| { type: 'SET_PAGINATION'; payload: Partial<AuditHistoryState['pagination']> }
|
||||||
|
| { type: 'SET_SELECTED_RECORD'; payload: AuditLogData | null }
|
||||||
|
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'REFRESH_DATA' };
|
||||||
|
|
||||||
|
const auditHistoryReducer = (state: AuditHistoryState, action: AuditHistoryAction): AuditHistoryState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_RECORDS':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
records: 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_RECORD':
|
||||||
|
return { ...state, selectedRecord: action.payload };
|
||||||
|
case 'TOGGLE_DETAIL_DIALOG':
|
||||||
|
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||||||
|
case 'REFRESH_DATA':
|
||||||
|
return { ...state, error: null };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: AuditHistoryState = {
|
||||||
|
records: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
search_keyword: '',
|
||||||
|
typeFilter: 'all',
|
||||||
|
resultFilter: 'all',
|
||||||
|
dateRange: 'all',
|
||||||
|
},
|
||||||
|
sortBy: 'action_time',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
selectedRecord: null,
|
||||||
|
showDetailDialog: false,
|
||||||
|
};
|
||||||
|
|
||||||
export default function AuditHistoryPage() {
|
export default function AuditHistoryPage() {
|
||||||
const [records, setRecords] = useState<AuditRecord[]>([]);
|
const [state, dispatch] = useReducer(auditHistoryReducer, initialState);
|
||||||
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
|
|
||||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
|
||||||
const [selectedRecord, setSelectedRecord] = useState<AuditRecord | null>(null);
|
|
||||||
const [selectedEnterprise, setSelectedEnterprise] = useState<Enterprise | null>(null);
|
|
||||||
|
|
||||||
const [filters, setFilters] = useState<FilterOptions>({
|
// 加载审核历史数据
|
||||||
searchKeyword: '',
|
const loadAuditHistory = async (resetPage = false) => {
|
||||||
resultFilter: 'all',
|
try {
|
||||||
typeFilter: 'all',
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
dateRange: 'all'
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const params: AuditLogsQueryParams = {
|
||||||
|
search: state.filters.search_keyword || undefined,
|
||||||
|
page: resetPage ? 1 : state.pagination.page,
|
||||||
|
size: state.pagination.size
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetchAuditLogs(params);
|
||||||
|
const transformedRecords = response.data.map(transformAuditLogData);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_RECORDS',
|
||||||
|
payload: {
|
||||||
|
data: transformedRecords,
|
||||||
|
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 audit history:', error);
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ERROR',
|
||||||
|
payload: error instanceof Error ? error.message : '加载审核历史失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { search_keyword: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 类型筛选
|
||||||
|
const handleTypeFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 结果筛选
|
||||||
|
const handleResultFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { resultFilter: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 时间筛选
|
||||||
|
const handleDateFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { dateRange: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
|
const handleSort = (sortBy: string) => {
|
||||||
|
const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handlePageChange = (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 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleViewDetail = (record: AuditLogData) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_RECORD', payload: record });
|
||||||
|
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const handleRefresh = () => {
|
||||||
|
dispatch({ type: 'REFRESH_DATA' });
|
||||||
|
loadAuditHistory(true);
|
||||||
|
toast.success('数据已刷新');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化和监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadEnterprises();
|
|
||||||
loadAuditHistory();
|
loadAuditHistory();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadEnterprises = () => {
|
useEffect(() => {
|
||||||
const data = localStorage.getItem('smart_agriculture_enterprises');
|
const timer = setTimeout(() => {
|
||||||
if (data) {
|
loadAuditHistory();
|
||||||
setEnterprises(JSON.parse(data));
|
}, 300);
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [state.filters.search_keyword, state.filters.typeFilter, state.filters.resultFilter, state.filters.dateRange, state.sortBy, state.sortOrder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.pagination.page > 1) {
|
||||||
|
loadAuditHistory();
|
||||||
|
}
|
||||||
|
}, [state.pagination.page]);
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
const getActionBadge = (action: string) => {
|
||||||
|
switch (action) {
|
||||||
|
case 'register':
|
||||||
|
return <Badge className="bg-blue-100 text-blue-700">注册审核</Badge>;
|
||||||
|
case 'update':
|
||||||
|
return <Badge className="bg-orange-100 text-orange-700">变更审核</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge variant="outline">{action}</Badge>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAuditHistory = () => {
|
const getResultBadge = (result: string) => {
|
||||||
const data = localStorage.getItem('smart_agriculture_audit_records');
|
switch (result) {
|
||||||
if (data) {
|
case 'approved':
|
||||||
setRecords(JSON.parse(data));
|
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||||
} else {
|
case 'rejected':
|
||||||
// 初始化审核历史数据
|
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||||
const mockRecords: AuditRecord[] = [
|
case 'pending':
|
||||||
{
|
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||||
id: 'audit-1',
|
default:
|
||||||
enterpriseId: 'ent-2',
|
return <Badge variant="outline">{result}</Badge>;
|
||||||
enterpriseName: '丰收现代农业集团',
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: '2024-10-05T10:00:00',
|
|
||||||
auditTime: '2024-10-08T14:30:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'approved',
|
|
||||||
remarks: '企业资质完整,审核通过',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-2',
|
|
||||||
enterpriseId: 'ent-3',
|
|
||||||
enterpriseName: '金穗农机服务中心',
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: '2024-10-06T09:00:00',
|
|
||||||
auditTime: '2024-10-09T16:00:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'rejected',
|
|
||||||
reason: '资质材料不完整,请补充营业执照副本',
|
|
||||||
remarks: '缺少必要的资质证明文件',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-3',
|
|
||||||
enterpriseId: 'ent-1',
|
|
||||||
enterpriseName: '绿野农业科技有限公司',
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: '2024-10-10T08:00:00',
|
|
||||||
result: 'pending',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-4',
|
|
||||||
enterpriseId: 'ent-2',
|
|
||||||
enterpriseName: '丰收现代农业集团',
|
|
||||||
auditType: 'update',
|
|
||||||
submitTime: '2024-10-12T15:30:00',
|
|
||||||
auditTime: '2024-10-13T10:00:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'approved',
|
|
||||||
remarks: '企业地址变更审核通过',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-5',
|
|
||||||
enterpriseId: 'ent-4',
|
|
||||||
enterpriseName: '智慧农田科技公司',
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: '2024-09-28T11:00:00',
|
|
||||||
auditTime: '2024-09-30T09:30:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'approved',
|
|
||||||
remarks: '优质企业,快速审核通过',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'audit-6',
|
|
||||||
enterpriseId: 'ent-5',
|
|
||||||
enterpriseName: '农业机械租赁中心',
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: '2024-10-03T14:20:00',
|
|
||||||
auditTime: '2024-10-05T11:00:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'rejected',
|
|
||||||
reason: '企业经营范围与平台业务不符',
|
|
||||||
remarks: '建议企业完善相关资质后重新申请',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
localStorage.setItem('smart_agriculture_audit_records', JSON.stringify(mockRecords));
|
|
||||||
setRecords(mockRecords);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredRecords = records.filter(record => {
|
|
||||||
const matchKeyword = !filters.searchKeyword ||
|
|
||||||
record.enterpriseName.includes(filters.searchKeyword) ||
|
|
||||||
(record.auditor && record.auditor.includes(filters.searchKeyword));
|
|
||||||
|
|
||||||
const matchResult = filters.resultFilter === 'all' || record.result === filters.resultFilter;
|
|
||||||
const matchType = filters.typeFilter === 'all' || record.auditType === filters.typeFilter;
|
|
||||||
|
|
||||||
// 日期筛选
|
|
||||||
let matchDate = true;
|
|
||||||
if (filters.dateRange !== 'all' && record.auditTime) {
|
|
||||||
const auditDate = new Date(record.auditTime);
|
|
||||||
const now = new Date();
|
|
||||||
const diffDays = Math.floor((now.getTime() - auditDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
||||||
|
|
||||||
switch (filters.dateRange) {
|
|
||||||
case 'today':
|
|
||||||
matchDate = diffDays === 0;
|
|
||||||
break;
|
|
||||||
case 'week':
|
|
||||||
matchDate = diffDays <= 7;
|
|
||||||
break;
|
|
||||||
case 'month':
|
|
||||||
matchDate = diffDays <= 30;
|
|
||||||
break;
|
|
||||||
case 'quarter':
|
|
||||||
matchDate = diffDays <= 90;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return matchKeyword && matchResult && matchType && matchDate;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleViewDetail = (record: AuditRecord) => {
|
|
||||||
setSelectedRecord(record);
|
|
||||||
// 查找对应的企业信息
|
|
||||||
const enterprise = enterprises.find(e => e.id === record.enterpriseId);
|
|
||||||
setSelectedEnterprise(enterprise || null);
|
|
||||||
setShowDetailDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = () => {
|
|
||||||
const dataStr = JSON.stringify(filteredRecords, null, 2);
|
|
||||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `audit_history_${new Date().getTime()}.json`;
|
|
||||||
link.click();
|
|
||||||
toast.success('审核历史数据导出成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
{/* Page Header */}
|
||||||
<div>
|
<Card className="p-6 bg-gradient-to-r from-purple-50 dark:from-purple-950 to-pink-50 dark:to-pink-950 border-purple-200 dark:border-purple-800">
|
||||||
<h2 className="text-green-800">审核历史</h2>
|
<div className="flex items-start justify-between">
|
||||||
<p className="text-muted-foreground">追溯查询全部企业的历史审核记录</p>
|
<div className="flex items-start gap-3">
|
||||||
|
<History className="w-6 h-6 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="mb-2">审核历史</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
追溯查询全部企业的历史审核记录
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
|
||||||
|
<Search className="w-3 h-3 mr-1" />
|
||||||
|
智能查询
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
|
||||||
|
<Calendar className="w-3 h-3 mr-1" />
|
||||||
|
时间筛选
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
详细记录
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={state.loading}>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-1 ${state.loading ? 'animate-spin' : ''}`} />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button onClick={handleExport}>
|
</Card>
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
导出记录
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* Filters */}
|
||||||
<AuditHistoryStatsCards records={records} />
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-sm">搜索关键词</Label>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索企业名称、变更摘要..."
|
||||||
|
value={state.filters.search_keyword}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 搜索和筛选 */}
|
<div className="flex gap-4">
|
||||||
<AuditHistoryFilters
|
<div>
|
||||||
filters={filters}
|
<Label className="text-sm">全部类型</Label>
|
||||||
onFiltersChange={setFilters}
|
<Select value={state.filters.typeFilter} onValueChange={handleTypeFilter}>
|
||||||
/>
|
<SelectTrigger className="w-32 mt-2">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部类型</SelectItem>
|
||||||
|
<SelectItem value="register">注册审核</SelectItem>
|
||||||
|
<SelectItem value="update">变更审核</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 审核历史列表 */}
|
<div>
|
||||||
<AuditHistoryList
|
<Label className="text-sm">全部结果</Label>
|
||||||
records={filteredRecords}
|
<Select value={state.filters.resultFilter} onValueChange={handleResultFilter}>
|
||||||
onViewDetail={handleViewDetail}
|
<SelectTrigger className="w-32 mt-2">
|
||||||
/>
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部结果</SelectItem>
|
||||||
|
<SelectItem value="approved">已通过</SelectItem>
|
||||||
|
<SelectItem value="rejected">已驳回</SelectItem>
|
||||||
|
<SelectItem value="pending">待审核</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 详情对话框 */}
|
{/* Time Range Filter */}
|
||||||
<AuditHistoryDetailDialog
|
<Card className="p-4 bg-card">
|
||||||
record={selectedRecord}
|
<Label className="text-sm text-muted-foreground mb-2 block">时间范围</Label>
|
||||||
enterprise={selectedEnterprise}
|
<div className="flex gap-2">
|
||||||
open={showDetailDialog}
|
{[
|
||||||
onOpenChange={setShowDetailDialog}
|
{ value: 'all', label: '全部' },
|
||||||
/>
|
{ value: 'today', label: '今天' },
|
||||||
|
{ value: 'week', label: '近7天' },
|
||||||
|
{ value: 'month', label: '近30天' },
|
||||||
|
{ value: 'quarter', label: '近90天' },
|
||||||
|
].map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
variant={state.filters.dateRange === option.value ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDateFilter(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* 使用说明 */}
|
{/* Error Display */}
|
||||||
<AuditHistoryInstructions />
|
{state.error && (
|
||||||
|
<div className="mb-4 p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
<span>{state.error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{state.loading && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<RefreshCw className="w-8 h-8 mx-auto mb-2 animate-spin" />
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Data Table */}
|
||||||
|
{!state.loading && !state.error && (
|
||||||
|
<Card>
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => handleSort('enterprise_name')}
|
||||||
|
>
|
||||||
|
企业名称
|
||||||
|
{state.sortBy === 'enterprise_name' && (
|
||||||
|
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => handleSort('action')}
|
||||||
|
>
|
||||||
|
操作类型
|
||||||
|
{state.sortBy === 'action' && (
|
||||||
|
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>操作人</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => handleSort('action_time')}
|
||||||
|
>
|
||||||
|
操作时间
|
||||||
|
{state.sortBy === 'action_time' && (
|
||||||
|
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>结果</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{state.records.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
||||||
|
暂无审核记录
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
state.records.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building className="w-4 h-4 text-blue-500" />
|
||||||
|
<span className="font-medium">{record.snapshot_company_name}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getActionBadge(record.action)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<User className="w-4 h-4 text-gray-500" />
|
||||||
|
<span>{record.action_by}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm">{record.action_time}</TableCell>
|
||||||
|
<TableCell>{getResultBadge(record.result)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleViewDetail(record)}
|
||||||
|
>
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
详情
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{state.pagination.totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
显示第 {state.pagination.page} 页,共 {state.pagination.totalPages} 页
|
||||||
|
<span className="ml-2">总计 {state.pagination.total} 条记录</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(state.pagination.page - 1)}
|
||||||
|
disabled={!state.pagination.hasPrev || state.loading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium px-2">
|
||||||
|
{state.pagination.page} / {state.pagination.totalPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(state.pagination.page + 1)}
|
||||||
|
disabled={!state.pagination.hasNext || state.loading}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Detail Dialog */}
|
||||||
|
<Dialog open={state.showDetailDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}>
|
||||||
|
<DialogContent className="w-[90vw] max-w-6xl max-h-[90vh]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>审核记录详情</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
查看审核记录的详细信息和企业快照数据
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{state.selectedRecord && (
|
||||||
|
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||||
|
<Tabs defaultValue="basic" className="space-y-4">
|
||||||
|
<TabsList className="grid grid-cols-3 w-full">
|
||||||
|
<TabsTrigger value="basic">
|
||||||
|
<FileText className="w-4 h-4 mr-2" />
|
||||||
|
基本信息
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="snapshot">
|
||||||
|
<Building className="w-4 h-4 mr-2" />
|
||||||
|
企业快照
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="system">
|
||||||
|
<Smartphone className="w-4 h-4 mr-2" />
|
||||||
|
系统信息
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<TabsContent value="basic" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label>企业名称</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_company_name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>操作类型</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{getActionBadge(state.selectedRecord.action)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>操作人</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.action_by}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>操作时间</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.action_time}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>操作结果</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{getResultBadge(state.selectedRecord.result)}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>操作摘要</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
|
||||||
|
{state.selectedRecord.action_summary || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 企业快照 */}
|
||||||
|
<TabsContent value="snapshot" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label>企业类型</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_company_type || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>所在地区</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
|
{state.selectedRecord.snapshot_province} {state.selectedRecord.snapshot_city}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label>详细地址</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_address || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>登记人</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_registrant || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>联系电话</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_contact_phone || '-'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 系统信息 */}
|
||||||
|
<TabsContent value="system" className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label>记录ID</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
|
<code className="text-sm">{state.selectedRecord.id}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>企业ID</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
|
<code className="text-sm">{state.selectedRecord.tenant_id}</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>IP地址</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.ip_address || '-'}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>用户代理</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md text-sm">
|
||||||
|
{state.selectedRecord.user_agent || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false })}>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,525 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 审核历史 - 企业审核记录查询与详情查看页面
|
||||||
|
* 功能:审核历史查询、筛选过滤、详情查看、数据导出、分页控制
|
||||||
|
* 路径:/central-config/tenant/audit-history
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式
|
||||||
|
*/
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useReducer, useEffect, useMemo } from 'react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import {
|
||||||
|
History,
|
||||||
|
Download,
|
||||||
|
Search,
|
||||||
|
Eye,
|
||||||
|
AlertCircle,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
FileText,
|
||||||
|
Building2,
|
||||||
|
MapPin,
|
||||||
|
CreditCard,
|
||||||
|
Phone
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { auditHistoryReducer, initialState, AuditHistoryState, AuditHistoryAction } from './components/auditHistoryReducer';
|
||||||
|
import { fetchAuditLogs, transformAuditLogData, AuditLogsQueryParams } from './components/auditHistoryApi';
|
||||||
|
import { AuditRecord, FilterOptions, AuditStatus } from './types';
|
||||||
|
|
||||||
|
// Utility functions
|
||||||
|
const getStatusBadge = (status: AuditStatus) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'draft':
|
||||||
|
return <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">草稿</Badge>;
|
||||||
|
case 'pending':
|
||||||
|
return <Badge className="bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 font-light">待审核</Badge>;
|
||||||
|
case 'approved':
|
||||||
|
return <Badge className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light">审核通过</Badge>;
|
||||||
|
case 'rejected':
|
||||||
|
return <Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 font-light">审核拒绝</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">未知</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionBadge = (action: string) => {
|
||||||
|
if (action === 'SUBMIT') {
|
||||||
|
return <Badge className="bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 font-light">提交</Badge>;
|
||||||
|
} else if (action === 'AUDIT') {
|
||||||
|
return <Badge className="bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800 font-light">审核</Badge>;
|
||||||
|
}
|
||||||
|
return <Badge variant="outline" className="font-light">{action}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AuditHistoryPage() {
|
||||||
|
const [state, dispatch] = useReducer(auditHistoryReducer, initialState);
|
||||||
|
|
||||||
|
// 加载审核历史数据
|
||||||
|
const loadAuditHistory = async (resetPage = false) => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
|
const params: AuditLogsQueryParams = {
|
||||||
|
page: resetPage ? 1 : state.pagination.page,
|
||||||
|
size: state.pagination.size,
|
||||||
|
order_by: state.sortBy,
|
||||||
|
sort_order: state.sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetchAuditLogs(params);
|
||||||
|
const transformedData = response.data.map(transformAuditLogData);
|
||||||
|
|
||||||
|
console.log('Audit Logs API Response:', response);
|
||||||
|
console.log('Transformed Audit Data:', transformedData);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_RECORDS',
|
||||||
|
payload: {
|
||||||
|
data: transformedData,
|
||||||
|
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 audit history:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '加载审核历史失败';
|
||||||
|
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
||||||
|
toast.error(errorMessage);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始加载
|
||||||
|
useEffect(() => {
|
||||||
|
loadAuditHistory(true);
|
||||||
|
}, [state.sortBy, state.sortOrder]);
|
||||||
|
|
||||||
|
// 分页加载
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.pagination.page > 1) {
|
||||||
|
loadAuditHistory(false);
|
||||||
|
}
|
||||||
|
}, [state.pagination.page]);
|
||||||
|
|
||||||
|
// 计算统计数据 - 按照参考组件的顺序
|
||||||
|
const stats = useMemo(() => [
|
||||||
|
{
|
||||||
|
label: '审核总数',
|
||||||
|
value: state.records.length,
|
||||||
|
color: 'text-blue-600 dark:text-blue-400',
|
||||||
|
bg: 'bg-blue-50 dark:bg-blue-950',
|
||||||
|
borderColor: 'border-blue-200 dark:border-blue-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已通过',
|
||||||
|
value: state.records.filter(r => r.result === 'approved').length,
|
||||||
|
color: 'text-green-600 dark:text-green-400',
|
||||||
|
bg: 'bg-green-50 dark:bg-green-950',
|
||||||
|
borderColor: 'border-green-200 dark:border-green-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已驳回',
|
||||||
|
value: state.records.filter(r => r.result === 'rejected').length,
|
||||||
|
color: 'text-red-600 dark:text-red-400',
|
||||||
|
bg: 'bg-red-50 dark:bg-red-950',
|
||||||
|
borderColor: 'border-red-200 dark:border-red-800',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '待审核',
|
||||||
|
value: state.records.filter(r => r.result === 'pending').length,
|
||||||
|
color: 'text-yellow-600 dark:text-yellow-400',
|
||||||
|
bg: 'bg-yellow-50 dark:bg-yellow-950',
|
||||||
|
borderColor: 'border-yellow-200 dark:border-yellow-800',
|
||||||
|
},
|
||||||
|
], [state.records]);
|
||||||
|
|
||||||
|
// 筛选记录
|
||||||
|
const filteredRecords = useMemo(() => {
|
||||||
|
return state.records.filter(record => {
|
||||||
|
const matchKeyword = !state.filters.searchKeyword ||
|
||||||
|
record.enterpriseName.toLowerCase().includes(state.filters.searchKeyword.toLowerCase()) ||
|
||||||
|
record.changeSummary.toLowerCase().includes(state.filters.searchKeyword.toLowerCase());
|
||||||
|
|
||||||
|
const matchResult = state.filters.resultFilter === 'all' || record.result === state.filters.resultFilter;
|
||||||
|
const matchType = state.filters.typeFilter === 'all' || record.auditType === state.filters.typeFilter;
|
||||||
|
|
||||||
|
// 日期筛选
|
||||||
|
let matchDate = true;
|
||||||
|
if (state.filters.dateRange !== 'all' && record.actionTime) {
|
||||||
|
const auditDate = new Date(record.actionTime);
|
||||||
|
const now = new Date();
|
||||||
|
const diffDays = Math.floor((now.getTime() - auditDate.getTime()) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
switch (state.filters.dateRange) {
|
||||||
|
case 'today':
|
||||||
|
matchDate = diffDays === 0;
|
||||||
|
break;
|
||||||
|
case 'week':
|
||||||
|
matchDate = diffDays <= 7;
|
||||||
|
break;
|
||||||
|
case 'month':
|
||||||
|
matchDate = diffDays <= 30;
|
||||||
|
break;
|
||||||
|
case 'quarter':
|
||||||
|
matchDate = diffDays <= 90;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchKeyword && matchResult && matchType && matchDate;
|
||||||
|
});
|
||||||
|
}, [state.records, state.filters]);
|
||||||
|
|
||||||
|
// 事件处理器
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { searchKeyword: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResultFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { resultFilter: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTypeFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDateFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { dateRange: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (sortBy?: string) => {
|
||||||
|
const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (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 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetail = (record: AuditRecord) => {
|
||||||
|
dispatch({ type: 'SET_SELECTED_RECORD', payload: record });
|
||||||
|
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
const exportData = filteredRecords.map(record => ({
|
||||||
|
企业名称: record.enterpriseName,
|
||||||
|
操作类型: record.action === 'SUBMIT' ? '提交' : '审核',
|
||||||
|
审核类型: record.auditType === 'register' ? '注册' : '更新',
|
||||||
|
操作时间: record.actionTime,
|
||||||
|
操作人: record.actionBy,
|
||||||
|
审核结果: record.result === 'approved' ? '审核通过' :
|
||||||
|
record.result === 'rejected' ? '审核拒绝' :
|
||||||
|
record.result === 'pending' ? '待审核' : '草稿',
|
||||||
|
变更摘要: record.changeSummary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dataStr = JSON.stringify(exportData, null, 2);
|
||||||
|
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(dataBlob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = `audit_history_${new Date().getTime()}.json`;
|
||||||
|
link.click();
|
||||||
|
toast.success('审核历史数据导出成功');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Page Header */}
|
||||||
|
<Card className="p-6 bg-gradient-to-r from-purple-50 dark:from-purple-950 to-pink-50 dark:to-pink-950 border-purple-200 dark:border-purple-800">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<History className="w-6 h-6 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="mb-2">审核历史</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
追溯查询全部企业的历史审核记录
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
|
||||||
|
<Search className="w-3 h-3 mr-1" />
|
||||||
|
智能查询
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
|
||||||
|
<Calendar className="w-3 h-3 mr-1" />
|
||||||
|
时间筛选
|
||||||
|
</Badge>
|
||||||
|
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
|
||||||
|
<FileText className="w-3 h-3 mr-1" />
|
||||||
|
详情查看
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button onClick={handleExport}>
|
||||||
|
<Download className="w-4 h-4 mr-2" />
|
||||||
|
导出记录
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Statistics Cards */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<Card key={index} className={`p-6 bg-card hover:bg-muted transition-colors ${stat.borderColor} border-2`}>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||||
|
<History className="w-5 h-5 text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<div className={`text-3xl font-bold mb-1 ${stat.color}`}>{stat.value}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
条记录
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters */}
|
||||||
|
<Card className="p-6 bg-card">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-4 mb-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Label className="text-sm">搜索关键词</Label>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索企业名称、变更摘要..."
|
||||||
|
value={state.filters.searchKeyword}
|
||||||
|
onChange={(e) => handleSearch(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm">全部类型</Label>
|
||||||
|
<Select value={state.filters.typeFilter} onValueChange={handleTypeFilter}>
|
||||||
|
<SelectTrigger className="w-32 mt-2">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部类型</SelectItem>
|
||||||
|
<SelectItem value="register">注册审核</SelectItem>
|
||||||
|
<SelectItem value="update">变更审核</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-sm">全部结果</Label>
|
||||||
|
<Select value={state.filters.resultFilter} onValueChange={handleResultFilter}>
|
||||||
|
<SelectTrigger className="w-32 mt-2">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部结果</SelectItem>
|
||||||
|
<SelectItem value="pending">待审核</SelectItem>
|
||||||
|
<SelectItem value="approved">已通过</SelectItem>
|
||||||
|
<SelectItem value="rejected">已驳回</SelectItem>
|
||||||
|
<SelectItem value="draft">草稿</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Time Range Filter */}
|
||||||
|
<Card className="p-4 bg-card">
|
||||||
|
<Label className="text-sm text-muted-foreground mb-2 block">时间范围</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{[
|
||||||
|
{ value: 'all', label: '全部' },
|
||||||
|
{ value: 'today', label: '今天' },
|
||||||
|
{ value: 'week', label: '近7天' },
|
||||||
|
{ value: 'month', label: '近30天' },
|
||||||
|
{ value: 'quarter', label: '近90天' },
|
||||||
|
].map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
variant={state.filters.dateRange === option.value ? 'default' : 'outline'}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleDateFilter(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
{state.error && (
|
||||||
|
<div className="mb-4 p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
||||||
|
<AlertCircle className="w-4 h-4" />
|
||||||
|
<span>{state.error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Loading State */}
|
||||||
|
{state.loading && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<RefreshCw className="w-8 h-8 mx-auto mb-2 animate-spin" />
|
||||||
|
<p>加载中...</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Data Table */}
|
||||||
|
{!state.loading && !state.error && (
|
||||||
|
<>
|
||||||
|
<div className="border rounded-lg overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => handleSort('enterprise_name')}
|
||||||
|
>
|
||||||
|
企业名称
|
||||||
|
{state.sortBy === 'enterprise_name' && (
|
||||||
|
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => handleSort('action')}
|
||||||
|
>
|
||||||
|
操作类型
|
||||||
|
{state.sortBy === 'action' && (
|
||||||
|
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>操作人</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className="cursor-pointer hover:bg-muted"
|
||||||
|
onClick={() => handleSort('action_time')}
|
||||||
|
>
|
||||||
|
操作时间
|
||||||
|
{state.sortBy === 'action_time' && (
|
||||||
|
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>审核结果</TableHead>
|
||||||
|
<TableHead>变更摘要</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filteredRecords.map((record) => (
|
||||||
|
<TableRow key={record.id}>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Building2 className="w-4 h-4 text-purple-500" />
|
||||||
|
<span className="font-medium">{record.enterpriseName}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getActionBadge(record.action)}
|
||||||
|
{record.auditType === 'register' && (
|
||||||
|
<Badge variant="outline" className="font-light">注册</Badge>
|
||||||
|
)}
|
||||||
|
{record.auditType === 'update' && (
|
||||||
|
<Badge variant="outline" className="font-light">更新</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<User className="w-4 h-4 text-gray-500" />
|
||||||
|
<span className="text-sm">{record.actionBy}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm">{record.actionTime}</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(record.result)}</TableCell>
|
||||||
|
<TableCell className="text-sm max-w-xs truncate" title={record.changeSummary}>
|
||||||
|
{record.changeSummary}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => handleViewDetail(record)}
|
||||||
|
>
|
||||||
|
<Eye className="w-3 h-3 mr-1" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filteredRecords.length === 0 && (
|
||||||
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
|
<History className="w-12 h-12 mx-auto mb-4 opacity-20" />
|
||||||
|
<p>暂无审核历史记录</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Pagination */}
|
||||||
|
{state.pagination.totalPages > 1 && (
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
显示第 {state.pagination.page} 页,共 {state.pagination.totalPages} 页
|
||||||
|
<span className="ml-2">总计 {state.pagination.total} 条记录</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(state.pagination.page - 1)}
|
||||||
|
disabled={!state.pagination.hasPrev || state.loading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
<span className="text-sm font-medium px-2">
|
||||||
|
{state.pagination.page} / {state.pagination.totalPages}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(state.pagination.page + 1)}
|
||||||
|
disabled={!state.pagination.hasNext || state.loading}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
@@ -46,20 +46,48 @@ export interface Enterprise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type EnterpriseStatus = 'active' | 'inactive' | 'suspended';
|
export type EnterpriseStatus = 'active' | 'inactive' | 'suspended';
|
||||||
export type AuditStatus = 'pending' | 'approved' | 'rejected';
|
export type AuditStatus = 'pending' | 'approved' | 'rejected' | 'draft';
|
||||||
|
|
||||||
// 审核记录
|
// 审核记录(基于API响应结构)
|
||||||
export interface AuditRecord {
|
export interface AuditRecord {
|
||||||
id: string;
|
id: string;
|
||||||
enterpriseId: string;
|
enterpriseId?: string;
|
||||||
enterpriseName: string;
|
enterpriseName: string;
|
||||||
|
action: 'SUBMIT' | 'AUDIT';
|
||||||
auditType: 'register' | 'update';
|
auditType: 'register' | 'update';
|
||||||
submitTime: string;
|
submitTime: string;
|
||||||
auditTime?: string;
|
actionTime: string;
|
||||||
auditor?: string;
|
actionBy: string;
|
||||||
result: AuditStatus;
|
result: AuditStatus;
|
||||||
reason?: string;
|
auditStatus: string;
|
||||||
remarks?: string;
|
auditComment?: string;
|
||||||
|
changeSummary: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
requestId?: string;
|
||||||
|
createdAt: string;
|
||||||
|
|
||||||
|
// 快照数据
|
||||||
|
snapshot: {
|
||||||
|
companyName: string;
|
||||||
|
companyType: string | null;
|
||||||
|
province: string | null;
|
||||||
|
city: string | null;
|
||||||
|
district: string | null;
|
||||||
|
detailedAddress: string | null;
|
||||||
|
registrant: string | null;
|
||||||
|
contactPhone: string | null;
|
||||||
|
bankAccount: string | null;
|
||||||
|
bankName: string | null;
|
||||||
|
bankFullName: string | null;
|
||||||
|
bankAddress: string | null;
|
||||||
|
socialCreditCode: string | null;
|
||||||
|
legalPersonName: string | null;
|
||||||
|
auditStatus: string;
|
||||||
|
auditComment: string | null;
|
||||||
|
companyScale: string | null;
|
||||||
|
registeredCapital: string | null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 企业审核分页组件 - 分页控制界面
|
||||||
|
* 功能:分页导航、页面跳转、分页信息展示
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit/components/AuditPagination
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
|
||||||
|
interface PaginationInfo {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AuditPaginationProps {
|
||||||
|
pagination: PaginationInfo;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuditPagination({
|
||||||
|
pagination,
|
||||||
|
onPageChange,
|
||||||
|
loading = false
|
||||||
|
}: AuditPaginationProps) {
|
||||||
|
const { page, size, total, totalPages, hasNext, hasPrev } = pagination;
|
||||||
|
|
||||||
|
if (total === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
显示第 {(page - 1) * size + 1} - {Math.min(page * size, total)} 条,共 {total} 条记录
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(page - 1)}
|
||||||
|
disabled={!hasPrev || loading}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="w-4 h-4" />
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-sm text-muted-foreground">第</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={totalPages}
|
||||||
|
value={page}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newPage = parseInt(e.target.value);
|
||||||
|
if (!isNaN(newPage)) {
|
||||||
|
onPageChange(newPage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-16 h-8 text-center"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-muted-foreground">/ {totalPages} 页</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(page + 1)}
|
||||||
|
disabled={!hasNext || loading}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
<ChevronRight className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 企业审核搜索筛选组件 - 搜索和筛选功能界面
|
||||||
|
* 功能:关键词搜索、状态筛选、搜索功能实现
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit/components/AuditSearchAndFilter
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Search, RotateCcw } from 'lucide-react';
|
||||||
|
|
||||||
|
interface AuditSearchAndFilterProps {
|
||||||
|
searchKeyword: string;
|
||||||
|
onSearchChange: (keyword: string) => void;
|
||||||
|
statusFilter: string;
|
||||||
|
onStatusFilterChange: (status: string) => void;
|
||||||
|
onRefresh: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AuditSearchAndFilter({
|
||||||
|
searchKeyword,
|
||||||
|
onSearchChange,
|
||||||
|
statusFilter,
|
||||||
|
onStatusFilterChange,
|
||||||
|
onRefresh,
|
||||||
|
loading = false
|
||||||
|
}: AuditSearchAndFilterProps) {
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="搜索企业名称、信用代码、登记人..."
|
||||||
|
value={searchKeyword}
|
||||||
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
|
className="pl-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
|
||||||
|
<SelectTrigger className="w-40">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">全部状态</SelectItem>
|
||||||
|
<SelectItem value="草稿">草稿</SelectItem>
|
||||||
|
<SelectItem value="待审核">待审核</SelectItem>
|
||||||
|
<SelectItem value="已通过">已通过</SelectItem>
|
||||||
|
<SelectItem value="已驳回">已驳回</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={onRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<RotateCcw className="w-4 h-4 mr-2" />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,30 +1,38 @@
|
|||||||
'use client';
|
/**
|
||||||
|
* filekorolheader: 企业审核统计卡片组件 - 统计数据展示界面
|
||||||
|
* 功能:待审核、已通过、已驳回、总企业数统计展示
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit/components/AuditStatsCards
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import { Enterprise } from './enterpriseAuditApi';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { AuditStats, Enterprise } from '../types';
|
|
||||||
|
|
||||||
interface AuditStatsCardsProps {
|
interface AuditStatsCardsProps {
|
||||||
enterprises: Enterprise[];
|
enterprises: Enterprise[];
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AuditStatsCards({ enterprises }: AuditStatsCardsProps) {
|
export function AuditStatsCards({
|
||||||
const stats: AuditStats[] = [
|
enterprises,
|
||||||
|
loading = false
|
||||||
|
}: AuditStatsCardsProps) {
|
||||||
|
const stats = [
|
||||||
{
|
{
|
||||||
label: '待审核',
|
label: '待审核',
|
||||||
value: enterprises.filter(e => e.auditStatus === 'pending').length,
|
value: enterprises.filter(e => e.auditStatus === '待审核').length,
|
||||||
color: 'text-yellow-600',
|
color: 'text-yellow-600',
|
||||||
bg: 'bg-yellow-100',
|
bg: 'bg-yellow-100',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '已通过',
|
label: '已通过',
|
||||||
value: enterprises.filter(e => e.auditStatus === 'approved').length,
|
value: enterprises.filter(e => e.auditStatus === '已通过').length,
|
||||||
color: 'text-green-600',
|
color: 'text-green-600',
|
||||||
bg: 'bg-green-100',
|
bg: 'bg-green-100',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '已驳回',
|
label: '已驳回',
|
||||||
value: enterprises.filter(e => e.auditStatus === 'rejected').length,
|
value: enterprises.filter(e => e.auditStatus === '已驳回').length,
|
||||||
color: 'text-red-600',
|
color: 'text-red-600',
|
||||||
bg: 'bg-red-100',
|
bg: 'bg-red-100',
|
||||||
},
|
},
|
||||||
@@ -36,12 +44,27 @@ export function AuditStatsCards({ enterprises }: AuditStatsCardsProps) {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{stats.map((_, index) => (
|
||||||
|
<Card key={index} className="p-4">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="h-4 bg-gray-200 rounded mb-2"></div>
|
||||||
|
<div className="h-8 bg-gray-200 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
<Card key={index} className="p-4">
|
<Card key={index} className="p-4">
|
||||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||||
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div>
|
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 企业审核表格组件 - 企业数据列表展示和管理界面
|
||||||
|
* 功能:企业数据表格展示、搜索过滤、分页管理、操作按钮
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit/components/EnterpriseAuditTable
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Enterprise } from './enterpriseAuditApi';
|
||||||
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Eye } from 'lucide-react';
|
||||||
|
|
||||||
|
interface EnterpriseAuditTableProps {
|
||||||
|
enterprises: Enterprise[];
|
||||||
|
loading: boolean;
|
||||||
|
onViewDetails: (enterprise: Enterprise) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EnterpriseAuditTable({
|
||||||
|
enterprises,
|
||||||
|
loading,
|
||||||
|
onViewDetails
|
||||||
|
}: EnterpriseAuditTableProps) {
|
||||||
|
const getAuditStatusBadge = (status: Enterprise['auditStatus']) => {
|
||||||
|
switch (status) {
|
||||||
|
case '草稿':
|
||||||
|
return <Badge variant="secondary">草稿</Badge>;
|
||||||
|
case '待审核':
|
||||||
|
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||||
|
case '已通过':
|
||||||
|
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||||
|
case '已驳回':
|
||||||
|
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||||
|
default:
|
||||||
|
return <Badge variant="outline">{status}</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (status: Enterprise['status']) => {
|
||||||
|
return status === 'active' ? (
|
||||||
|
<Badge variant="default">启用</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="secondary">禁用</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<div className="text-muted-foreground">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>企业名称</TableHead>
|
||||||
|
<TableHead>企业类型</TableHead>
|
||||||
|
<TableHead>社会信用代码</TableHead>
|
||||||
|
<TableHead>法人</TableHead>
|
||||||
|
<TableHead>所在地区</TableHead>
|
||||||
|
<TableHead>审核状态</TableHead>
|
||||||
|
<TableHead>企业状态</TableHead>
|
||||||
|
<TableHead>提交时间</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{enterprises.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={9} className="text-center text-muted-foreground py-8">
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
enterprises.map((enterprise) => (
|
||||||
|
<TableRow key={enterprise.id}>
|
||||||
|
<TableCell className="font-medium">{enterprise.name}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{enterprise.type}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">
|
||||||
|
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
|
||||||
|
{enterprise.socialCreditCode}
|
||||||
|
</code>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{enterprise.legalPerson || '-'}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">
|
||||||
|
{enterprise.province && enterprise.city ?
|
||||||
|
`${enterprise.province} ${enterprise.city}` :
|
||||||
|
'-'
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{getAuditStatusBadge(enterprise.auditStatus)}</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(enterprise.status)}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">
|
||||||
|
{enterprise.submitTime || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onViewDetails(enterprise)}
|
||||||
|
>
|
||||||
|
<Eye className="w-4 h-4 mr-1" />
|
||||||
|
查看
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,46 +1,54 @@
|
|||||||
'use client';
|
/**
|
||||||
|
* filekorolheader: 企业详情对话框组件 - 企业详情展示和审核操作界面
|
||||||
|
* 功能:企业详细信息展示、多标签页布局、审核操作界面
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit/components/EnterpriseDetailDialog
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import { Enterprise } from './enterpriseAuditApi';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Building2, FileText, CreditCard, User, CheckCircle, XCircle } from 'lucide-react';
|
||||||
import { Building, FileText, CreditCard, User, CheckCircle, XCircle, Image as ImageIcon } from 'lucide-react';
|
|
||||||
import { Enterprise, AuditStatus } from '../types';
|
|
||||||
|
|
||||||
interface EnterpriseDetailDialogProps {
|
interface EnterpriseDetailDialogProps {
|
||||||
enterprise: Enterprise | null;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
|
enterprise: Enterprise | null;
|
||||||
auditReason: string;
|
auditReason: string;
|
||||||
onAuditReasonChange: (reason: string) => void;
|
onAuditReasonChange: (reason: string) => void;
|
||||||
onApprove: () => void;
|
onApprove: () => void;
|
||||||
onReject: () => void;
|
onReject: () => void;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EnterpriseDetailDialog({
|
export function EnterpriseDetailDialog({
|
||||||
enterprise,
|
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
|
enterprise,
|
||||||
auditReason,
|
auditReason,
|
||||||
onAuditReasonChange,
|
onAuditReasonChange,
|
||||||
onApprove,
|
onApprove,
|
||||||
onReject
|
onReject,
|
||||||
|
loading = false
|
||||||
}: EnterpriseDetailDialogProps) {
|
}: EnterpriseDetailDialogProps) {
|
||||||
const getAuditStatusBadge = (status: AuditStatus) => {
|
const getAuditStatusBadge = (status: Enterprise['auditStatus']) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'pending':
|
case '草稿':
|
||||||
|
return <Badge variant="secondary">草稿</Badge>;
|
||||||
|
case '待审核':
|
||||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||||
case 'approved':
|
case '已通过':
|
||||||
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||||
case 'rejected':
|
case '已驳回':
|
||||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||||
default:
|
default:
|
||||||
return <Badge>{status}</Badge>;
|
return <Badge variant="outline">{status}</Badge>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,14 +56,16 @@ export function EnterpriseDetailDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-5xl max-h-[90vh]">
|
<DialogContent className="w-[80vw] max-w-6xl max-h-[90vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<div className="flex items-center justify-between pr-8">
|
<div className="flex items-center justify-between pr-8">
|
||||||
<DialogTitle>企业详情审核</DialogTitle>
|
<DialogTitle>企业详情审核</DialogTitle>
|
||||||
{getAuditStatusBadge(enterprise.auditStatus)}
|
<div className="flex gap-2">
|
||||||
|
{getAuditStatusBadge(enterprise.auditStatus)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogDescription className="sr-only">
|
<DialogDescription className="sr-only">
|
||||||
查看企业的详细信息和审核状态
|
查看企业的详细信息和进行审核操作
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -63,7 +73,7 @@ export function EnterpriseDetailDialog({
|
|||||||
<Tabs defaultValue="basic" className="space-y-4">
|
<Tabs defaultValue="basic" className="space-y-4">
|
||||||
<TabsList className="grid grid-cols-4 w-full">
|
<TabsList className="grid grid-cols-4 w-full">
|
||||||
<TabsTrigger value="basic">
|
<TabsTrigger value="basic">
|
||||||
<Building className="w-4 h-4 mr-2" />
|
<Building2 className="w-4 h-4 mr-2" />
|
||||||
基本信息
|
基本信息
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="other">
|
<TabsTrigger value="other">
|
||||||
@@ -80,36 +90,103 @@ export function EnterpriseDetailDialog({
|
|||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* 企业基本信息 */}
|
{/* 基本信息 */}
|
||||||
<TabsContent value="basic" className="space-y-4">
|
<TabsContent value="basic" className="space-y-6">
|
||||||
<div className="grid grid-cols-2 gap-6">
|
{/* 企业基本信息 */}
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<Label>企业名称</Label>
|
<h4 className="font-semibold text-lg">企业基本信息</h4>
|
||||||
<div className="field-value">{enterprise.name}</div>
|
<div className="grid grid-cols-2 gap-6">
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<Label>企业名称</Label>
|
||||||
<Label>企业类型</Label>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.name}</div>
|
||||||
<div className="field-value">{enterprise.type}</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<Label>企业类型</Label>
|
||||||
<Label>所在地区</Label>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.type}</div>
|
||||||
<div className="field-value">
|
</div>
|
||||||
{enterprise.province} {enterprise.city} {enterprise.district}
|
<div>
|
||||||
|
<Label>所在地区</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
|
{enterprise.province} {enterprise.city} {enterprise.district}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>详细地址</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.address}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>登记人</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registrant}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>联系电话</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.contactPhone}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Label>详细地址</Label>
|
|
||||||
<div className="field-value">{enterprise.address}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>登记人</Label>
|
|
||||||
<div className="field-value">{enterprise.registrant}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>联系电话</Label>
|
|
||||||
<div className="field-value">{enterprise.contactPhone}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 审核信息 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-semibold text-lg">审核信息</h4>
|
||||||
|
<Card className="p-6 bg-gray-50 border">
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">创建时间</Label>
|
||||||
|
<div className="mt-1.5 text-base">{enterprise.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">更新时间</Label>
|
||||||
|
<div className="mt-1.5 text-base">{enterprise.updatedAt}</div>
|
||||||
|
</div>
|
||||||
|
{enterprise.submitTime && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">提交时间</Label>
|
||||||
|
<div className="mt-1.5 text-base">{enterprise.submitTime}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{enterprise.auditTime && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">审核时间</Label>
|
||||||
|
<div className="mt-1.5 text-base">{enterprise.auditTime}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{enterprise.auditor && (
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs">审核人</Label>
|
||||||
|
<div className="mt-1.5 text-base">{enterprise.auditor}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{enterprise.auditComment && (
|
||||||
|
<div className="col-span-2 pt-4 mt-2 border-t">
|
||||||
|
<Label className="text-xs">已有审核意见</Label>
|
||||||
|
<div className="mt-1.5 text-base p-3 bg-white rounded-md">
|
||||||
|
{enterprise.auditComment}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 审核操作区 - 仅待审核状态显示 */}
|
||||||
|
{enterprise.auditStatus === '待审核' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="font-semibold text-lg">审核操作</h4>
|
||||||
|
<div>
|
||||||
|
<Label>审核意见</Label>
|
||||||
|
<div className="text-xs text-muted-foreground mt-1">
|
||||||
|
驳回时必填,通过时可选择性填写
|
||||||
|
</div>
|
||||||
|
<Textarea
|
||||||
|
value={auditReason}
|
||||||
|
onChange={(e) => onAuditReasonChange(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
placeholder="请填写审核意见..."
|
||||||
|
className="mt-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 其他信息 */}
|
{/* 其他信息 */}
|
||||||
@@ -117,44 +194,30 @@ export function EnterpriseDetailDialog({
|
|||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label>公司规模</Label>
|
<Label>公司规模</Label>
|
||||||
<div className="field-value">{enterprise.companySize || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.companySize || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>注册资本</Label>
|
<Label>注册资本</Label>
|
||||||
<div className="field-value">{enterprise.registeredCapital || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registeredCapital || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>成立时间</Label>
|
<Label>成立时间</Label>
|
||||||
<div className="field-value">{enterprise.establishmentDate || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.establishmentDate || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>发票类型</Label>
|
<Label>发票类型</Label>
|
||||||
<div className="field-value">{enterprise.invoiceType || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.invoiceType || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label>社会信用代码</Label>
|
<Label>社会信用代码</Label>
|
||||||
<div className="field-value">
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
<code className="text-sm font-mono">
|
<code className="text-sm font-mono">{enterprise.socialCreditCode || '-'}</code>
|
||||||
{enterprise.socialCreditCode}
|
|
||||||
</code>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label>经营范围</Label>
|
<Label>经营范围</Label>
|
||||||
<div className="field-value">{enterprise.businessScope || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
|
||||||
</div>
|
{enterprise.businessScope || '-'}
|
||||||
<div className="col-span-2">
|
|
||||||
<Label>营业执照</Label>
|
|
||||||
<div className="mt-2">
|
|
||||||
{enterprise.businessLicense ? (
|
|
||||||
<img
|
|
||||||
src={enterprise.businessLicense}
|
|
||||||
alt="营业执照"
|
|
||||||
className="w-64 h-auto border rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">未上传</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,153 +228,69 @@ export function EnterpriseDetailDialog({
|
|||||||
<div className="grid grid-cols-2 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label>银行账号</Label>
|
<Label>银行账号</Label>
|
||||||
<div className="field-value">
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
{enterprise.bankAccount ? (
|
{enterprise.bankAccount ? (
|
||||||
<code className="text-sm font-mono">
|
<code className="text-sm font-mono">{enterprise.bankAccount}</code>
|
||||||
{enterprise.bankAccount}
|
|
||||||
</code>
|
|
||||||
) : '-'}
|
) : '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>开户行</Label>
|
<Label>开户行</Label>
|
||||||
<div className="field-value">{enterprise.bankName || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankName || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label>开户行全称</Label>
|
<Label>开户行全称</Label>
|
||||||
<div className="field-value">{enterprise.bankFullName || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankFullName || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
<Label>开户行地址</Label>
|
<Label>开户行地址</Label>
|
||||||
<div className="field-value">{enterprise.bankAddress || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankAddress || '-'}</div>
|
||||||
</div>
|
|
||||||
<div className="col-span-2">
|
|
||||||
<Label>开户许可证</Label>
|
|
||||||
<div className="mt-2">
|
|
||||||
{enterprise.bankLicense ? (
|
|
||||||
<img
|
|
||||||
src={enterprise.bankLicense}
|
|
||||||
alt="开户许可证"
|
|
||||||
className="w-64 h-auto border rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">未上传</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
{/* 法人信息 */}
|
{/* 法人信息 */}
|
||||||
<TabsContent value="legal" className="space-y-4">
|
<TabsContent value="legal" className="space-y-4">
|
||||||
<div className="grid grid-cols-1 gap-6">
|
<div className="grid grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Label>法人名称</Label>
|
<Label>法人姓名</Label>
|
||||||
<div className="field-value">{enterprise.legalPerson || '-'}</div>
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.legalPerson || '-'}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>身份证正面</Label>
|
<Label>登记人</Label>
|
||||||
<div className="mt-2">
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registrant || '-'}</div>
|
||||||
{enterprise.idCardFront ? (
|
|
||||||
<img
|
|
||||||
src={enterprise.idCardFront}
|
|
||||||
alt="身份证正面"
|
|
||||||
className="w-80 h-auto border rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">未上传</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>身份证反面</Label>
|
|
||||||
<div className="mt-2">
|
|
||||||
{enterprise.idCardBack ? (
|
|
||||||
<img
|
|
||||||
src={enterprise.idCardBack}
|
|
||||||
alt="身份证反面"
|
|
||||||
className="w-80 h-auto border rounded-lg"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">未上传</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
{/* 审核信息 */}
|
</ScrollArea>
|
||||||
<div className="mt-6 pt-6 border-t">
|
|
||||||
<h4 className="mb-4 font-bold">审核信息</h4>
|
|
||||||
<Card className="p-6 bg-gray-50 border">
|
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">提交时间</Label>
|
|
||||||
<div className="mt-1.5 text-base">
|
|
||||||
{new Date(enterprise.createdAt).toLocaleString('zh-CN')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{enterprise.auditTime && (
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">审核时间</Label>
|
|
||||||
<div className="mt-1.5 text-base">
|
|
||||||
{new Date(enterprise.auditTime).toLocaleString('zh-CN')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{enterprise.auditor && (
|
|
||||||
<div>
|
|
||||||
<Label className="text-xs">审核人</Label>
|
|
||||||
<div className="mt-1.5 text-base">
|
|
||||||
{enterprise.auditor}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{enterprise.auditReason && (
|
|
||||||
<div className="col-span-2 pt-4 mt-2 border-t">
|
|
||||||
<Label className="text-xs">审核意见</Label>
|
|
||||||
<div className="mt-1.5 text-base">
|
|
||||||
{enterprise.auditReason}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 审核操作区 - 仅待审核状态显示 */}
|
{/* 底部操作按钮 */}
|
||||||
{enterprise.auditStatus === 'pending' && (
|
<div className="flex justify-end gap-3 mt-6 pt-4 border-t">
|
||||||
<div className="mt-6">
|
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
|
||||||
<Label>审核意见</Label>
|
|
||||||
<Textarea
|
|
||||||
value={auditReason}
|
|
||||||
onChange={(e) => onAuditReasonChange(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
placeholder="请填写审核意见(驳回时必填)..."
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ScrollArea>
|
|
||||||
|
|
||||||
<DialogFooter className="border-t pt-4">
|
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
||||||
关闭
|
关闭
|
||||||
</Button>
|
</Button>
|
||||||
{enterprise.auditStatus === 'pending' && (
|
{enterprise.auditStatus === '待审核' && (
|
||||||
<>
|
<>
|
||||||
<Button variant="destructive" onClick={onReject}>
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={onReject}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
<XCircle className="w-4 h-4 mr-2" />
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
驳回
|
驳回
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onApprove}>
|
<Button
|
||||||
|
onClick={onApprove}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
<CheckCircle className="w-4 h-4 mr-2" />
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
通过
|
通过
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DialogFooter>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 企业审核API接口 - 企业审核数据查询接口服务
|
||||||
|
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit/components/enterpriseAuditApi
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAuthToken } from "@/utils/token";
|
||||||
|
import { listTenantsApiV1TenantsGet, auditTenantApiV1TenantsTenantIdAuditPost } from "@/lib/api/sdk.gen";
|
||||||
|
|
||||||
|
// 企业数据类型(与企业管理页面相同)
|
||||||
|
export interface TenantData {
|
||||||
|
id: string;
|
||||||
|
tenant_code: string;
|
||||||
|
is_active: boolean;
|
||||||
|
company_name: string;
|
||||||
|
company_type: string | null;
|
||||||
|
province: string | null;
|
||||||
|
city: string | null;
|
||||||
|
district: string | null;
|
||||||
|
detailed_address: string | null;
|
||||||
|
registrant: string | null;
|
||||||
|
contact_phone: string | null;
|
||||||
|
bank_account: string | null;
|
||||||
|
bank_name: string | null;
|
||||||
|
bank_full_name: string | null;
|
||||||
|
bank_address: string | null;
|
||||||
|
social_credit_code: string | null;
|
||||||
|
legal_person_name: string | null;
|
||||||
|
company_scale: string | null;
|
||||||
|
registered_capital: string | null;
|
||||||
|
established_date: string | null;
|
||||||
|
invoice_type: string | null;
|
||||||
|
business_scope: string | null;
|
||||||
|
submit_time: string | null;
|
||||||
|
audit_time: string | null;
|
||||||
|
auditor: string | null;
|
||||||
|
audit_status: string;
|
||||||
|
audit_comment: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应接口
|
||||||
|
export interface TenantsApiResponse {
|
||||||
|
data: TenantData[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_prev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数接口
|
||||||
|
export interface TenantsQueryParams {
|
||||||
|
search?: string;
|
||||||
|
audit_status?: string;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
order_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 企业页面数据类型(转换后的)
|
||||||
|
export interface Enterprise {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
type: string;
|
||||||
|
status: 'active' | 'inactive';
|
||||||
|
auditStatus: '草稿' | '待审核' | '已通过' | '已驳回';
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
contact?: string;
|
||||||
|
phone?: string;
|
||||||
|
contactPhone?: string;
|
||||||
|
province?: string;
|
||||||
|
city?: string;
|
||||||
|
district?: string;
|
||||||
|
address?: string;
|
||||||
|
registrant?: string;
|
||||||
|
companySize?: string;
|
||||||
|
registeredCapital?: string;
|
||||||
|
establishmentDate?: string;
|
||||||
|
invoiceType?: string;
|
||||||
|
socialCreditCode?: string;
|
||||||
|
businessScope?: string;
|
||||||
|
legalPerson?: string;
|
||||||
|
bankAccount?: string;
|
||||||
|
bankName?: string;
|
||||||
|
bankFullName?: string;
|
||||||
|
bankAddress?: string;
|
||||||
|
submitTime?: string;
|
||||||
|
auditTime?: string;
|
||||||
|
auditor?: string;
|
||||||
|
auditComment?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取待审核企业列表数据
|
||||||
|
*/
|
||||||
|
export async function fetchTenantsForAudit(params: TenantsQueryParams = {}): Promise<TenantsApiResponse> {
|
||||||
|
try {
|
||||||
|
// 构建查询参数对象
|
||||||
|
const queryParams: any = {};
|
||||||
|
|
||||||
|
if (params.search) queryParams.search = params.search;
|
||||||
|
if (params.audit_status) queryParams.audit_status = params.audit_status;
|
||||||
|
if (params.page) queryParams.page = params.page;
|
||||||
|
if (params.size) queryParams.size = params.size;
|
||||||
|
if (params.order_by) queryParams.order_by = params.order_by;
|
||||||
|
if (params.sort_order) queryParams.sort_order = params.sort_order;
|
||||||
|
|
||||||
|
// 默认参数
|
||||||
|
if (!params.page) queryParams.page = 1;
|
||||||
|
if (!params.size) queryParams.size = 10;
|
||||||
|
if (!params.sort_order) queryParams.sort_order = 'desc';
|
||||||
|
|
||||||
|
// 使用SDK API调用企业查询接口,添加缓存破坏器和认证头部
|
||||||
|
const token = getAuthToken();
|
||||||
|
console.log('审核页面API调用参数:', queryParams);
|
||||||
|
|
||||||
|
const response = await listTenantsApiV1TenantsGet({
|
||||||
|
query: {
|
||||||
|
...queryParams,
|
||||||
|
// 添加时间戳防止缓存
|
||||||
|
_t: Date.now(),
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
console.log('审核页面API响应:', data);
|
||||||
|
|
||||||
|
// 转换响应数据格式以匹配现有的接口
|
||||||
|
// API返回的数据结构: { data: [...], total: 25, page: 1, size: 10, ... }
|
||||||
|
return {
|
||||||
|
data: data?.data || [], // 注意:实际数据在 data.data 中
|
||||||
|
total: data?.total || 0,
|
||||||
|
page: data?.page || 1,
|
||||||
|
size: data?.size || 10,
|
||||||
|
total_pages: data?.total_pages || 0,
|
||||||
|
has_next: data?.has_next || false,
|
||||||
|
has_prev: data?.has_prev || false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch tenants for audit:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审核企业
|
||||||
|
*/
|
||||||
|
export async function auditTenant(
|
||||||
|
tenantId: string,
|
||||||
|
auditData: {
|
||||||
|
audit_status: '草稿' | '待审核' | '已通过' | '已驳回';
|
||||||
|
audit_comment?: string;
|
||||||
|
}
|
||||||
|
): Promise<TenantData> {
|
||||||
|
try {
|
||||||
|
const token = getAuthToken();
|
||||||
|
console.log('审核企业API调用:', tenantId, auditData);
|
||||||
|
|
||||||
|
const response = await auditTenantApiV1TenantsTenantIdAuditPost({
|
||||||
|
path: {
|
||||||
|
tenant_id: tenantId,
|
||||||
|
},
|
||||||
|
body: auditData,
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`审核企业失败: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as TenantData;
|
||||||
|
console.log('审核企业API响应:', data);
|
||||||
|
|
||||||
|
// 验证返回的数据中audit_status是否正确
|
||||||
|
if (data.audit_status !== auditData.audit_status) {
|
||||||
|
throw new Error('审核企业失败:返回数据状态不正确');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to audit tenant:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将API数据转换为页面所需的企业数据格式
|
||||||
|
*/
|
||||||
|
export function transformTenantData(tenant: TenantData): Enterprise {
|
||||||
|
return {
|
||||||
|
id: tenant.id,
|
||||||
|
name: tenant.company_name,
|
||||||
|
code: tenant.tenant_code,
|
||||||
|
type: tenant.company_type || '未分类',
|
||||||
|
status: tenant.is_active ? 'active' : 'inactive',
|
||||||
|
auditStatus: mapAuditStatus(tenant.audit_status),
|
||||||
|
createdAt: formatDate(tenant.created_at),
|
||||||
|
updatedAt: formatDate(tenant.updated_at),
|
||||||
|
contact: tenant.registrant,
|
||||||
|
phone: tenant.contact_phone,
|
||||||
|
contactPhone: tenant.contact_phone,
|
||||||
|
province: tenant.province,
|
||||||
|
city: tenant.city,
|
||||||
|
district: tenant.district,
|
||||||
|
address: tenant.detailed_address,
|
||||||
|
registrant: tenant.registrant,
|
||||||
|
companySize: tenant.company_scale,
|
||||||
|
registeredCapital: tenant.registered_capital,
|
||||||
|
establishmentDate: tenant.established_date ?
|
||||||
|
new Date(tenant.established_date).toLocaleDateString('zh-CN') : undefined,
|
||||||
|
invoiceType: tenant.invoice_type,
|
||||||
|
socialCreditCode: tenant.social_credit_code,
|
||||||
|
businessScope: tenant.business_scope,
|
||||||
|
legalPerson: tenant.legal_person_name,
|
||||||
|
bankAccount: tenant.bank_account,
|
||||||
|
bankName: tenant.bank_name,
|
||||||
|
bankFullName: tenant.bank_full_name,
|
||||||
|
bankAddress: tenant.bank_address,
|
||||||
|
submitTime: tenant.submit_time ? formatDate(tenant.submit_time) : undefined,
|
||||||
|
auditTime: tenant.audit_time ? formatDate(tenant.audit_time) : undefined,
|
||||||
|
auditor: tenant.auditor,
|
||||||
|
auditComment: tenant.audit_comment,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 映射审核状态
|
||||||
|
*/
|
||||||
|
function mapAuditStatus(status: string): Enterprise['auditStatus'] {
|
||||||
|
switch (status) {
|
||||||
|
case '未提交':
|
||||||
|
case '草稿':
|
||||||
|
return '草稿';
|
||||||
|
case '待审核':
|
||||||
|
return '待审核';
|
||||||
|
case '已通过':
|
||||||
|
case '审核通过':
|
||||||
|
return '已通过';
|
||||||
|
case '已拒绝':
|
||||||
|
case '已驳回':
|
||||||
|
return '已驳回';
|
||||||
|
default:
|
||||||
|
return '草稿';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,256 +1,354 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 企业审核页面 - 企业注册审核管理页面
|
||||||
|
* 功能:企业审核列表、搜索筛选、审核操作、详情查看
|
||||||
|
* 路径:/central-config/tenant/enterprise-audit
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,模块化组件
|
||||||
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useReducer, useEffect, useMemo } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { Building2, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
import { fetchTenantsForAudit, auditTenant, transformTenantData, TenantsQueryParams, Enterprise } from './components/enterpriseAuditApi';
|
||||||
import { AuditStatsCards } from './components/AuditStatsCards';
|
import { AuditStatsCards } from './components/AuditStatsCards';
|
||||||
import { SearchFilters } from './components/SearchFilters';
|
import { AuditSearchAndFilter } from './components/AuditSearchAndFilter';
|
||||||
import { EnterpriseList } from './components/EnterpriseList';
|
import { EnterpriseAuditTable } from './components/EnterpriseAuditTable';
|
||||||
import { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog';
|
import { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog';
|
||||||
import { Enterprise, AuditStatus } from './types';
|
import { AuditPagination } from './components/AuditPagination';
|
||||||
|
|
||||||
|
// 审核状态管理
|
||||||
|
interface AuditState {
|
||||||
|
enterprises: Enterprise[];
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
};
|
||||||
|
filters: {
|
||||||
|
search: string;
|
||||||
|
audit_status: string;
|
||||||
|
};
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder: 'asc' | 'desc';
|
||||||
|
selectedEnterprise: Enterprise | null;
|
||||||
|
showDetailDialog: boolean;
|
||||||
|
auditReason: string;
|
||||||
|
actionLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuditAction =
|
||||||
|
| { type: 'SET_ENTERPRISES'; payload: { data: Enterprise[]; pagination: AuditState['pagination'] } }
|
||||||
|
| { type: 'SET_LOADING'; payload: boolean }
|
||||||
|
| { type: 'SET_ERROR'; payload: string | null }
|
||||||
|
| { type: 'SET_FILTERS'; payload: Partial<AuditState['filters']> }
|
||||||
|
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||||
|
| { type: 'SET_PAGINATION'; payload: Partial<AuditState['pagination']> }
|
||||||
|
| { type: 'SET_SELECTED_ENTERPRISE'; payload: Enterprise | null }
|
||||||
|
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||||
|
| { type: 'SET_AUDIT_REASON'; payload: string }
|
||||||
|
| { type: 'SET_ACTION_LOADING'; payload: boolean }
|
||||||
|
| { type: 'REFRESH_DATA' };
|
||||||
|
|
||||||
|
const auditReducer = (state: AuditState, action: AuditAction): AuditState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case 'SET_ENTERPRISES':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
enterprises: 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_ENTERPRISE':
|
||||||
|
return { ...state, selectedEnterprise: action.payload };
|
||||||
|
case 'TOGGLE_DETAIL_DIALOG':
|
||||||
|
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||||||
|
case 'SET_AUDIT_REASON':
|
||||||
|
return { ...state, auditReason: action.payload };
|
||||||
|
case 'SET_ACTION_LOADING':
|
||||||
|
return { ...state, actionLoading: action.payload };
|
||||||
|
case 'REFRESH_DATA':
|
||||||
|
return { ...state, error: null };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialState: AuditState = {
|
||||||
|
enterprises: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
search: '',
|
||||||
|
audit_status: 'all',
|
||||||
|
},
|
||||||
|
sortBy: 'created_at',
|
||||||
|
sortOrder: 'desc',
|
||||||
|
selectedEnterprise: null,
|
||||||
|
showDetailDialog: false,
|
||||||
|
auditReason: '',
|
||||||
|
actionLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function EnterpriseAuditPage() {
|
export default function EnterpriseAuditPage() {
|
||||||
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
|
const [state, dispatch] = useReducer(auditReducer, initialState);
|
||||||
const [searchKeyword, setSearchKeyword] = useState('');
|
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
|
||||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
|
||||||
const [selectedEnterprise, setSelectedEnterprise] = useState<Enterprise | null>(null);
|
|
||||||
const [auditReason, setAuditReason] = useState('');
|
|
||||||
|
|
||||||
useEffect(() => {
|
// 加载企业数据
|
||||||
loadEnterprises();
|
const loadEnterprises = async (resetPage = false) => {
|
||||||
}, []);
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
const loadEnterprises = () => {
|
const params: TenantsQueryParams = {
|
||||||
const data = localStorage.getItem('smart_agriculture_enterprises');
|
search: state.filters.search || undefined,
|
||||||
if (data) {
|
audit_status: state.filters.audit_status === 'all' ? undefined : state.filters.audit_status,
|
||||||
setEnterprises(JSON.parse(data));
|
page: resetPage ? 1 : state.pagination.page,
|
||||||
} else {
|
size: state.pagination.size,
|
||||||
// 初始化示例数据
|
order_by: state.sortBy,
|
||||||
const mockEnterprises: Enterprise[] = [
|
sort_order: state.sortOrder,
|
||||||
{
|
};
|
||||||
id: 'ent-1',
|
|
||||||
name: '绿野农业科技有限公司',
|
const response = await fetchTenantsForAudit(params);
|
||||||
type: '有限责任公司',
|
const transformedData = response.data.map(transformTenantData);
|
||||||
province: '北京市',
|
|
||||||
city: '海淀区',
|
dispatch({
|
||||||
district: '中关村街道',
|
type: 'SET_ENTERPRISES',
|
||||||
companySize: '50-200人',
|
payload: {
|
||||||
registeredCapital: '1000万元',
|
data: transformedData,
|
||||||
establishmentDate: '2020-03-15',
|
pagination: {
|
||||||
invoiceType: '增值税专用发票',
|
page: response.page,
|
||||||
socialCreditCode: '91110000123456789X',
|
size: response.size,
|
||||||
businessScope: '农业技术开发、技术咨询、技术服务;销售机械设备、电子产品。',
|
total: response.total,
|
||||||
bankAccount: '1234567890123456789',
|
totalPages: response.total_pages,
|
||||||
bankName: '中国工商银行',
|
hasNext: response.has_next,
|
||||||
bankFullName: '中国工商银行股份有限公司北京中关村支行',
|
hasPrev: response.has_prev,
|
||||||
bankAddress: '北京市海淀区中关村大街1号',
|
}
|
||||||
legalPerson: '张伟',
|
}
|
||||||
registrant: '张经理',
|
});
|
||||||
contactPhone: '13800138001',
|
} catch (error) {
|
||||||
address: '北京市海淀区中关村大街1号科技大厦',
|
console.error('Failed to load enterprises for audit:', error);
|
||||||
status: 'active',
|
const errorMessage = error instanceof Error ? error.message : '加载企业审核数据失败';
|
||||||
auditStatus: 'pending',
|
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
||||||
createdAt: '2024-10-10T08:00:00',
|
toast.error(errorMessage);
|
||||||
updatedAt: '2024-10-10T08:00:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent-2',
|
|
||||||
name: '丰收现代农业集团',
|
|
||||||
type: '股份有限公司',
|
|
||||||
province: '江苏省',
|
|
||||||
city: '南京市',
|
|
||||||
district: '江宁区',
|
|
||||||
companySize: '200-500人',
|
|
||||||
registeredCapital: '5000万元',
|
|
||||||
establishmentDate: '2018-06-20',
|
|
||||||
invoiceType: '增值税专用发票',
|
|
||||||
socialCreditCode: '91320000987654321Y',
|
|
||||||
businessScope: '现代农业种植、农产品加工与销售、农业技术推广服务。',
|
|
||||||
bankAccount: '9876543210987654321',
|
|
||||||
bankName: '中国农业银行',
|
|
||||||
bankFullName: '中国农业银行股份有限公司南京江宁支行',
|
|
||||||
bankAddress: '江苏省南京市江宁区农业大道88号',
|
|
||||||
legalPerson: '李明',
|
|
||||||
registrant: '李总',
|
|
||||||
contactPhone: '13900139002',
|
|
||||||
address: '江苏省南京市江宁区农业大道88号',
|
|
||||||
status: 'active',
|
|
||||||
auditStatus: 'approved',
|
|
||||||
auditTime: '2024-10-08T14:30:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
createdAt: '2024-10-05T10:00:00',
|
|
||||||
updatedAt: '2024-10-08T14:30:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'ent-3',
|
|
||||||
name: '金穗农机服务中心',
|
|
||||||
type: '个人独资企业',
|
|
||||||
province: '山东省',
|
|
||||||
city: '济南市',
|
|
||||||
district: '历城区',
|
|
||||||
companySize: '1-50人',
|
|
||||||
registeredCapital: '200万元',
|
|
||||||
establishmentDate: '2021-09-10',
|
|
||||||
invoiceType: '增值税普通发票',
|
|
||||||
socialCreditCode: '91370000456789012Z',
|
|
||||||
businessScope: '农业机械租赁、维修服务、农机作业服务。',
|
|
||||||
bankAccount: '5555666677778888',
|
|
||||||
bankName: '中国建设银行',
|
|
||||||
bankFullName: '中国建设银行股份有限公司济南历城支行',
|
|
||||||
bankAddress: '山东省济南市历城区农机路66号',
|
|
||||||
legalPerson: '王刚',
|
|
||||||
registrant: '王主任',
|
|
||||||
contactPhone: '13700137003',
|
|
||||||
address: '山东省济南市历城区农机路66号',
|
|
||||||
status: 'inactive',
|
|
||||||
auditStatus: 'rejected',
|
|
||||||
auditReason: '资质材料不完整,请补充营业执照副本和法人身份证复印件',
|
|
||||||
auditTime: '2024-10-09T16:00:00',
|
|
||||||
auditor: '系统管理员',
|
|
||||||
createdAt: '2024-10-06T09:00:00',
|
|
||||||
updatedAt: '2024-10-09T16:00:00',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
localStorage.setItem('smart_agriculture_enterprises', JSON.stringify(mockEnterprises));
|
|
||||||
setEnterprises(mockEnterprises);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredEnterprises = enterprises.filter(ent => {
|
// 初始加载
|
||||||
const matchKeyword = !searchKeyword ||
|
useEffect(() => {
|
||||||
ent.name.includes(searchKeyword) ||
|
loadEnterprises(true);
|
||||||
ent.socialCreditCode.includes(searchKeyword) ||
|
}, [state.filters.search, state.filters.audit_status, state.sortBy, state.sortOrder]);
|
||||||
ent.registrant.includes(searchKeyword);
|
|
||||||
|
|
||||||
const matchStatus = statusFilter === 'all' || ent.auditStatus === statusFilter;
|
// 分页加载
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.pagination.page > 1) {
|
||||||
|
loadEnterprises(false);
|
||||||
|
}
|
||||||
|
}, [state.pagination.page]);
|
||||||
|
|
||||||
return matchKeyword && matchStatus;
|
// 计算统计数据
|
||||||
});
|
const stats = useMemo(() => ({
|
||||||
|
total: state.pagination.total,
|
||||||
|
pending: state.enterprises.filter(e => e.auditStatus === 'pending').length,
|
||||||
|
approved: state.enterprises.filter(e => e.auditStatus === 'approved').length,
|
||||||
|
rejected: state.enterprises.filter(e => e.auditStatus === 'rejected').length,
|
||||||
|
}), [state.enterprises, state.pagination.total]);
|
||||||
|
|
||||||
|
// 事件处理器
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { search: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAuditStatusFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { audit_status: value === 'all' ? 'all' : value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = (sortBy?: string) => {
|
||||||
|
const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (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 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRefresh = () => {
|
||||||
|
dispatch({ type: 'REFRESH_DATA' });
|
||||||
|
loadEnterprises(true);
|
||||||
|
toast.success('数据已刷新');
|
||||||
|
};
|
||||||
|
|
||||||
const handleViewDetail = (enterprise: Enterprise) => {
|
const handleViewDetail = (enterprise: Enterprise) => {
|
||||||
setSelectedEnterprise(enterprise);
|
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||||
setAuditReason('');
|
dispatch({ type: 'SET_AUDIT_REASON', payload: '' });
|
||||||
setShowDetailDialog(true);
|
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleApprove = () => {
|
const handleAuditReasonChange = (value: string) => {
|
||||||
if (!selectedEnterprise) return;
|
dispatch({ type: 'SET_AUDIT_REASON', payload: value });
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const updated = enterprises.map(ent =>
|
|
||||||
ent.id === selectedEnterprise.id
|
|
||||||
? {
|
|
||||||
...ent,
|
|
||||||
auditStatus: 'approved' as AuditStatus,
|
|
||||||
status: 'active' as const,
|
|
||||||
auditTime: now,
|
|
||||||
auditor: '系统管理员',
|
|
||||||
auditReason: auditReason || undefined,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
: ent
|
|
||||||
);
|
|
||||||
|
|
||||||
// 创建审核历史记录
|
|
||||||
const auditRecords = JSON.parse(localStorage.getItem('smart_agriculture_audit_records') || '[]');
|
|
||||||
const newRecord = {
|
|
||||||
id: `audit-${Date.now()}`,
|
|
||||||
enterpriseId: selectedEnterprise.id,
|
|
||||||
enterpriseName: selectedEnterprise.name,
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: selectedEnterprise.createdAt,
|
|
||||||
auditTime: now,
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'approved',
|
|
||||||
remarks: auditReason || '审核通过',
|
|
||||||
};
|
|
||||||
auditRecords.push(newRecord);
|
|
||||||
localStorage.setItem('smart_agriculture_audit_records', JSON.stringify(auditRecords));
|
|
||||||
|
|
||||||
setEnterprises(updated);
|
|
||||||
localStorage.setItem('smart_agriculture_enterprises', JSON.stringify(updated));
|
|
||||||
setShowDetailDialog(false);
|
|
||||||
toast.success('审核通过');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReject = () => {
|
const handleApprove = async () => {
|
||||||
if (!selectedEnterprise) return;
|
if (!state.selectedEnterprise) return;
|
||||||
if (!auditReason.trim()) {
|
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_ACTION_LOADING', payload: true });
|
||||||
|
|
||||||
|
const updatedTenant = await auditTenant(state.selectedEnterprise.id, {
|
||||||
|
audit_status: '已通过',
|
||||||
|
audit_comment: state.auditReason || '审核通过',
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
const updatedEnterprise = transformTenantData(updatedTenant);
|
||||||
|
dispatch({
|
||||||
|
type: 'SET_ENTERPRISES',
|
||||||
|
payload: {
|
||||||
|
data: state.enterprises.map(ent =>
|
||||||
|
ent.id === state.selectedEnterprise?.id ? updatedEnterprise : ent
|
||||||
|
),
|
||||||
|
pagination: state.pagination
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false });
|
||||||
|
toast.success('审核通过');
|
||||||
|
|
||||||
|
// 1秒后刷新列表
|
||||||
|
setTimeout(() => {
|
||||||
|
loadEnterprises(true);
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Approve failed:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '审核通过失败';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_ACTION_LOADING', payload: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = async () => {
|
||||||
|
if (!state.selectedEnterprise) return;
|
||||||
|
if (!state.auditReason.trim()) {
|
||||||
toast.error('请填写驳回原因');
|
toast.error('请填写驳回原因');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
try {
|
||||||
const updated = enterprises.map(ent =>
|
dispatch({ type: 'SET_ACTION_LOADING', payload: true });
|
||||||
ent.id === selectedEnterprise.id
|
|
||||||
? {
|
|
||||||
...ent,
|
|
||||||
auditStatus: 'rejected' as AuditStatus,
|
|
||||||
status: 'inactive' as const,
|
|
||||||
auditTime: now,
|
|
||||||
auditor: '系统管理员',
|
|
||||||
auditReason: auditReason,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
: ent
|
|
||||||
);
|
|
||||||
|
|
||||||
// 创建审核历史记录
|
const updatedTenant = await auditTenant(state.selectedEnterprise.id, {
|
||||||
const auditRecords = JSON.parse(localStorage.getItem('smart_agriculture_audit_records') || '[]');
|
audit_status: '已驳回',
|
||||||
const newRecord = {
|
audit_comment: state.auditReason,
|
||||||
id: `audit-${Date.now()}`,
|
});
|
||||||
enterpriseId: selectedEnterprise.id,
|
|
||||||
enterpriseName: selectedEnterprise.name,
|
|
||||||
auditType: 'register',
|
|
||||||
submitTime: selectedEnterprise.createdAt,
|
|
||||||
auditTime: now,
|
|
||||||
auditor: '系统管理员',
|
|
||||||
result: 'rejected',
|
|
||||||
reason: auditReason,
|
|
||||||
remarks: '审核驳回',
|
|
||||||
};
|
|
||||||
auditRecords.push(newRecord);
|
|
||||||
localStorage.setItem('smart_agriculture_audit_records', JSON.stringify(auditRecords));
|
|
||||||
|
|
||||||
setEnterprises(updated);
|
// 更新本地状态
|
||||||
localStorage.setItem('smart_agriculture_enterprises', JSON.stringify(updated));
|
const updatedEnterprise = transformTenantData(updatedTenant);
|
||||||
setShowDetailDialog(false);
|
dispatch({
|
||||||
toast.success('已驳回');
|
type: 'SET_ENTERPRISES',
|
||||||
|
payload: {
|
||||||
|
data: state.enterprises.map(ent =>
|
||||||
|
ent.id === state.selectedEnterprise?.id ? updatedEnterprise : ent
|
||||||
|
),
|
||||||
|
pagination: state.pagination
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false });
|
||||||
|
toast.success('已驳回');
|
||||||
|
|
||||||
|
// 1秒后刷新列表
|
||||||
|
setTimeout(() => {
|
||||||
|
loadEnterprises(true);
|
||||||
|
}, 1000);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reject failed:', error);
|
||||||
|
const errorMessage = error instanceof Error ? error.message : '审核驳回失败';
|
||||||
|
toast.error(errorMessage);
|
||||||
|
} finally {
|
||||||
|
dispatch({ type: 'SET_ACTION_LOADING', payload: false });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* 页面标题和描述 */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-green-800">企业审核</h2>
|
<h2 className="text-green-800">企业审核</h2>
|
||||||
<p className="text-muted-foreground">管理企业注册与变更审核流程</p>
|
<p className="text-muted-foreground">管理企业注册与变更审核流程</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<AuditStatsCards enterprises={enterprises} />
|
<AuditStatsCards
|
||||||
|
enterprises={state.enterprises}
|
||||||
|
loading={state.loading}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 搜索和筛选 */}
|
{/* 搜索和筛选 */}
|
||||||
<SearchFilters
|
<AuditSearchAndFilter
|
||||||
searchKeyword={searchKeyword}
|
searchKeyword={state.filters.search}
|
||||||
setSearchKeyword={setSearchKeyword}
|
onSearchChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { search: value } })}
|
||||||
statusFilter={statusFilter}
|
statusFilter={state.filters.audit_status}
|
||||||
setStatusFilter={setStatusFilter}
|
onStatusFilterChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { audit_status: value } })}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
loading={state.loading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 企业列表 */}
|
{/* 企业列表 */}
|
||||||
<EnterpriseList
|
<EnterpriseAuditTable
|
||||||
enterprises={filteredEnterprises}
|
enterprises={state.enterprises}
|
||||||
onViewDetail={handleViewDetail}
|
loading={state.loading}
|
||||||
|
onViewDetails={handleViewDetail}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 详情审核对话框 */}
|
{/* 分页 */}
|
||||||
|
{state.pagination.total > 0 && (
|
||||||
|
<AuditPagination
|
||||||
|
pagination={state.pagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
loading={state.loading}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 企业详情对话框 */}
|
||||||
<EnterpriseDetailDialog
|
<EnterpriseDetailDialog
|
||||||
enterprise={selectedEnterprise}
|
open={state.showDetailDialog}
|
||||||
open={showDetailDialog}
|
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||||
onOpenChange={setShowDetailDialog}
|
enterprise={state.selectedEnterprise}
|
||||||
auditReason={auditReason}
|
auditReason={state.auditReason}
|
||||||
onAuditReasonChange={setAuditReason}
|
onAuditReasonChange={(reason) => dispatch({ type: 'SET_AUDIT_REASON', payload: reason })}
|
||||||
onApprove={handleApprove}
|
onApprove={handleApprove}
|
||||||
onReject={handleReject}
|
onReject={handleReject}
|
||||||
|
loading={state.actionLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -126,6 +126,12 @@ export default function EnterpriseManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (page: number) => {
|
const handlePageChange = (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 } });
|
dispatch({ type: 'SET_PAGINATION', payload: { page } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,112 +1,282 @@
|
|||||||
'use client';
|
/**
|
||||||
|
* filekorolheader: 用户详情对话框组件 - 用户详细信息展示界面
|
||||||
|
* 功能:用户详细信息展示、多标签页布局、状态和权限信息
|
||||||
|
* 路径:/central-config/tenant/user-management/components/UserDetailDialog
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import { User } from '../types';
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { User, UserStatus, UserType } from '../types';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import { User, Mail, Phone, Calendar, Building, Shield, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||||
|
|
||||||
interface UserDetailDialogProps {
|
interface UserDetailDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
selectedUser: User | null;
|
user: User | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserDetailDialog({
|
export function UserDetailDialog({
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
selectedUser
|
user
|
||||||
}: UserDetailDialogProps) {
|
}: UserDetailDialogProps) {
|
||||||
const getStatusBadge = (status: UserStatus) => {
|
const getStatusBadge = (isActive: boolean) => {
|
||||||
switch (status) {
|
return isActive ? (
|
||||||
case 'active':
|
<Badge className="bg-green-100 text-green-700">正常</Badge>
|
||||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
) : (
|
||||||
case 'frozen':
|
<Badge className="bg-red-100 text-red-700">停用</Badge>
|
||||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
);
|
||||||
case 'inactive':
|
};
|
||||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
|
||||||
default:
|
const getRoleBadge = (isSuperuser: boolean) => {
|
||||||
return <Badge>{status}</Badge>;
|
return isSuperuser ? (
|
||||||
|
<Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge className="bg-blue-100 text-blue-700">普通用户</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVerifiedBadge = (isVerified: boolean) => {
|
||||||
|
return isVerified ? (
|
||||||
|
<Badge className="bg-green-100 text-green-700">已验证</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">未验证</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserTypeBadge = (type: UserType) => {
|
if (!user) return null;
|
||||||
switch (type) {
|
|
||||||
case 'super_admin':
|
|
||||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
|
||||||
case 'enterprise_admin':
|
|
||||||
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
|
||||||
case 'employee':
|
|
||||||
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge>{type}</Badge>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!selectedUser) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="w-[80vw] max-w-4xl max-h-[90vh]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>用户详情</DialogTitle>
|
<div className="flex items-center justify-between pr-8">
|
||||||
<DialogDescription className="sr-only">
|
<DialogTitle>用户详情</DialogTitle>
|
||||||
查看用户的详细信息
|
<div className="flex gap-2">
|
||||||
</DialogDescription>
|
{getRoleBadge(user.isSuperuser)}
|
||||||
</DialogHeader>
|
{getVerifiedBadge(user.isVerified)}
|
||||||
<div className="space-y-4">
|
{getStatusBadge(user.isActive)}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>用户名</Label>
|
|
||||||
<div className="mt-1">{selectedUser.username}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>姓名</Label>
|
|
||||||
<div className="mt-1">{selectedUser.name}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>电话</Label>
|
|
||||||
<div className="mt-1">{selectedUser.phone}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>邮箱</Label>
|
|
||||||
<div className="mt-1">{selectedUser.email || '-'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>用户类型</Label>
|
|
||||||
<div className="mt-1">{getUserTypeBadge(selectedUser.userType)}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>所属企业</Label>
|
|
||||||
<div className="mt-1">{selectedUser.enterpriseName || '-'}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>状态</Label>
|
|
||||||
<div className="mt-1">{getStatusBadge(selectedUser.status)}</div>
|
|
||||||
</div>
|
|
||||||
{selectedUser.lastLoginTime && (
|
|
||||||
<div>
|
|
||||||
<Label>最后登录</Label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{new Date(selectedUser.lastLoginTime).toLocaleString('zh-CN')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<Label>创建时间</Label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{new Date(selectedUser.createdAt).toLocaleString('zh-CN')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>更新时间</Label>
|
|
||||||
<div className="mt-1">
|
|
||||||
{new Date(selectedUser.updatedAt).toLocaleString('zh-CN')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<DialogDescription className="sr-only">
|
||||||
|
查看用户的详细信息和权限
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||||
|
<Tabs defaultValue="basic" className="space-y-4">
|
||||||
|
<TabsList className="grid grid-cols-3 w-full">
|
||||||
|
<TabsTrigger value="basic">
|
||||||
|
<User className="w-4 h-4 mr-2" />
|
||||||
|
基本信息
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="permissions">
|
||||||
|
<Shield className="w-4 h-4 mr-2" />
|
||||||
|
权限信息
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="activity">
|
||||||
|
<Clock className="w-4 h-4 mr-2" />
|
||||||
|
活动信息
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{/* 基本信息 */}
|
||||||
|
<TabsContent value="basic" className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
{user.avatarUrl ? (
|
||||||
|
<img
|
||||||
|
src={user.avatarUrl}
|
||||||
|
alt={user.username}
|
||||||
|
className="w-16 h-16 rounded-full"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="w-16 h-16 rounded-full bg-gray-200 flex items-center justify-center">
|
||||||
|
<User className="w-8 h-8 text-gray-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold">{user.displayName || user.fullName || user.username}</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">@{user.username}</p>
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
{getRoleBadge(user.isSuperuser)}
|
||||||
|
{getVerifiedBadge(user.isVerified)}
|
||||||
|
{getStatusBadge(user.isActive)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<Label>用户名</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{user.username}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>显示名称</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
|
{user.displayName || user.fullName || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>邮箱</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||||
|
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||||
|
{user.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>电话</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||||
|
<Phone className="w-4 h-4 text-muted-foreground" />
|
||||||
|
{user.phone || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>所属企业</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||||
|
<Building className="w-4 h-4 text-muted-foreground" />
|
||||||
|
{user.companyName || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>权限范围</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||||
|
{user.scope === 'tenant' ? '租户级' : user.scope || '-'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{user.departmentName && (
|
||||||
|
<div>
|
||||||
|
<Label>部门</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{user.departmentName}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{user.bio && (
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label>个人简介</Label>
|
||||||
|
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
|
||||||
|
{user.bio}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 权限信息 */}
|
||||||
|
<TabsContent value="permissions" className="space-y-4">
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-4">系统权限</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>超级管理员权限</span>
|
||||||
|
{user.isSuperuser ? (
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-5 h-5 text-red-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>邮箱已验证</span>
|
||||||
|
{user.isVerified ? (
|
||||||
|
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-5 h-5 text-orange-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-4">访问状态</h4>
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>账户状态</span>
|
||||||
|
{getStatusBadge(user.isActive)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{user.tenantId && (
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-4">关联信息</h4>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<Label>租户ID</Label>
|
||||||
|
<div className="mt-1.5 text-sm font-mono bg-gray-100 p-2 rounded">
|
||||||
|
{user.tenantId}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
{user.departmentId && (
|
||||||
|
<Card className="p-4">
|
||||||
|
<Label>部门ID</Label>
|
||||||
|
<div className="mt-1.5 text-sm font-mono bg-gray-100 p-2 rounded">
|
||||||
|
{user.departmentId}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* 活动信息 */}
|
||||||
|
<TabsContent value="activity" className="space-y-4">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-4">时间信息</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<Card className="p-4">
|
||||||
|
<Label className="text-xs">创建时间</Label>
|
||||||
|
<div className="mt-1.5 text-sm">{formatDate(user.createdAt)}</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4">
|
||||||
|
<Label className="text-xs">更新时间</Label>
|
||||||
|
<div className="mt-1.5 text-sm">{formatDate(user.updatedAt)}</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4">
|
||||||
|
<Label className="text-xs">最后登录时间</Label>
|
||||||
|
<div className="mt-1.5 text-sm">
|
||||||
|
{user.lastLoginAt ? formatDate(user.lastLoginAt) : '从未登录'}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card className="p-4">
|
||||||
|
<Label className="text-xs">账户状态</Label>
|
||||||
|
<div className="mt-1.5">
|
||||||
|
{getStatusBadge(user.isActive)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||||
关闭
|
关闭
|
||||||
|
|||||||
@@ -1,140 +1,245 @@
|
|||||||
'use client';
|
/**
|
||||||
|
* filekorolheader: 用户列表组件 - 用户数据表格展示界面
|
||||||
|
* 功能:用户数据表格展示、状态徽章、操作按钮、分页功能
|
||||||
|
* 路径:/central-config/tenant/user-management/components/UserList
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import { User, PaginationState } from '../types';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
import { Eye, Edit, Lock, Trash2, UserX, UserCheck } from 'lucide-react';
|
import { Eye, Edit, Trash2, Lock, UserX, UserCheck } from 'lucide-react';
|
||||||
import { User, UserStatus, UserType } from '../types';
|
|
||||||
|
|
||||||
interface UserListProps {
|
interface UserListProps {
|
||||||
users: User[];
|
users: User[];
|
||||||
|
loading: boolean;
|
||||||
|
pagination: PaginationState;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
onViewDetail: (user: User) => void;
|
onViewDetail: (user: User) => void;
|
||||||
onEdit: (user: User) => void;
|
onEdit?: (user: User) => void;
|
||||||
onResetPassword: (user: User) => void;
|
onDelete?: (user: User) => void;
|
||||||
onToggleStatus: (user: User) => void;
|
onToggleStatus?: (user: User) => void;
|
||||||
onDelete: (id: string) => void;
|
onResetPassword?: (user: User) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserList({
|
export function UserList({
|
||||||
users,
|
users,
|
||||||
|
loading,
|
||||||
|
pagination,
|
||||||
|
onPageChange,
|
||||||
onViewDetail,
|
onViewDetail,
|
||||||
onEdit,
|
onEdit,
|
||||||
onResetPassword,
|
onDelete,
|
||||||
onToggleStatus,
|
onToggleStatus,
|
||||||
onDelete
|
onResetPassword
|
||||||
}: UserListProps) {
|
}: UserListProps) {
|
||||||
const getStatusBadge = (status: UserStatus) => {
|
const getStatusBadge = (user: User) => {
|
||||||
switch (status) {
|
// 根据isSuperuser和isActive判断状态
|
||||||
case 'active':
|
if (user.isSuperuser) {
|
||||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
return user.isActive ? (
|
||||||
case 'frozen':
|
<Badge className="bg-green-100 text-green-700">正常</Badge>
|
||||||
return <Badge className="bg-gray-100 text-gray-700">已冻结</Badge>;
|
) : (
|
||||||
case 'inactive':
|
<Badge className="bg-gray-100 text-gray-700">已冻结</Badge>
|
||||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
);
|
||||||
default:
|
}
|
||||||
return <Badge>{status}</Badge>;
|
|
||||||
|
if (user.isActive) {
|
||||||
|
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
||||||
|
} else {
|
||||||
|
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getUserTypeBadge = (type: UserType) => {
|
const getUserTypeBadge = (user: User) => {
|
||||||
switch (type) {
|
if (user.isSuperuser) {
|
||||||
case 'super_admin':
|
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
||||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
|
||||||
case 'enterprise_admin':
|
|
||||||
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
|
||||||
case 'employee':
|
|
||||||
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
|
||||||
default:
|
|
||||||
return <Badge>{type}</Badge>;
|
|
||||||
}
|
}
|
||||||
|
// 根据scope或其他字段判断用户类型
|
||||||
|
if (user.scope === 'enterprise' || user.companyName) {
|
||||||
|
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
||||||
|
}
|
||||||
|
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getRoleBadge = (user: User) => {
|
||||||
|
if (user.isSuperuser) {
|
||||||
|
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
||||||
|
}
|
||||||
|
return <Badge className="bg-blue-100 text-blue-700">普通用户</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVerifiedBadge = (isVerified: boolean) => {
|
||||||
|
return isVerified ? (
|
||||||
|
<Badge className="bg-green-100 text-green-700">已验证</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">未验证</Badge>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<div className="flex items-center justify-center h-96">
|
||||||
|
<div className="text-muted-foreground">加载中...</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="space-y-4">
|
||||||
<Table>
|
<Card>
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>用户名</TableHead>
|
|
||||||
<TableHead>姓名</TableHead>
|
|
||||||
<TableHead>电话</TableHead>
|
|
||||||
<TableHead>所属企业</TableHead>
|
|
||||||
<TableHead>用户类型</TableHead>
|
|
||||||
<TableHead>角色</TableHead>
|
|
||||||
<TableHead>状态</TableHead>
|
|
||||||
<TableHead>操作</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{users.length === 0 ? (
|
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
<TableHead>用户名</TableHead>
|
||||||
暂无数据
|
<TableHead>姓名</TableHead>
|
||||||
</TableCell>
|
<TableHead>电话</TableHead>
|
||||||
|
<TableHead>所属企业</TableHead>
|
||||||
|
<TableHead>用户类型</TableHead>
|
||||||
|
<TableHead>角色</TableHead>
|
||||||
|
<TableHead>状态</TableHead>
|
||||||
|
<TableHead>操作</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
users.map((user) => (
|
<TableBody>
|
||||||
<TableRow key={user.id}>
|
{users.length === 0 ? (
|
||||||
<TableCell className="text-muted-foreground">{user.username}</TableCell>
|
<TableRow>
|
||||||
<TableCell>{user.name}</TableCell>
|
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
||||||
<TableCell>{user.phone}</TableCell>
|
暂无用户数据
|
||||||
<TableCell className="text-muted-foreground">{user.enterpriseName || '-'}</TableCell>
|
|
||||||
<TableCell>{getUserTypeBadge(user.userType)}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{user.roles && user.roles.length > 0 ? user.roles.join(', ') : '-'}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{getStatusBadge(user.status)}</TableCell>
|
</TableRow>
|
||||||
<TableCell>
|
) : (
|
||||||
<div className="flex gap-1">
|
users.map((user) => (
|
||||||
<Button
|
<TableRow key={user.id}>
|
||||||
variant="ghost"
|
<TableCell className="font-medium">
|
||||||
size="sm"
|
<div className="flex items-center gap-2">
|
||||||
onClick={() => onViewDetail(user)}
|
{user.avatarUrl && (
|
||||||
>
|
<img
|
||||||
<Eye className="w-4 h-4" />
|
src={user.avatarUrl}
|
||||||
</Button>
|
alt={user.username}
|
||||||
<Button
|
className="w-8 h-8 rounded-full"
|
||||||
variant="ghost"
|
/>
|
||||||
size="sm"
|
|
||||||
onClick={() => onEdit(user)}
|
|
||||||
>
|
|
||||||
<Edit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onResetPassword(user)}
|
|
||||||
>
|
|
||||||
<Lock className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => onToggleStatus(user)}
|
|
||||||
>
|
|
||||||
{user.status === 'active' ? (
|
|
||||||
<UserX className="w-4 h-4 text-orange-600" />
|
|
||||||
) : (
|
|
||||||
<UserCheck className="w-4 h-4 text-green-600" />
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
<span>{user.username}</span>
|
||||||
{user.userType !== 'super_admin' && (
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{user.fullName || user.displayName || '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{user.phone || '-'}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{user.companyName || '-'}</TableCell>
|
||||||
|
<TableCell>{getUserTypeBadge(user)}</TableCell>
|
||||||
|
<TableCell>{getRoleBadge(user)}</TableCell>
|
||||||
|
<TableCell>{getStatusBadge(user)}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex gap-1">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onDelete(user.id)}
|
onClick={() => onViewDetail(user)}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-4 h-4 text-destructive" />
|
<Eye className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
{onEdit && (
|
||||||
</div>
|
<Button
|
||||||
</TableCell>
|
variant="ghost"
|
||||||
</TableRow>
|
size="sm"
|
||||||
))
|
onClick={() => onEdit(user)}
|
||||||
)}
|
>
|
||||||
</TableBody>
|
<Edit className="w-4 h-4" />
|
||||||
</Table>
|
</Button>
|
||||||
</Card>
|
)}
|
||||||
|
{onResetPassword && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onResetPassword(user)}
|
||||||
|
>
|
||||||
|
<Lock className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onToggleStatus && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onToggleStatus(user)}
|
||||||
|
>
|
||||||
|
{user.isActive ? (
|
||||||
|
<UserX className="w-4 h-4 text-orange-600" />
|
||||||
|
) : (
|
||||||
|
<UserCheck className="w-4 h-4 text-green-600" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{onDelete && !user.isSuperuser && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onDelete(user)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-destructive" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* 分页 */}
|
||||||
|
{pagination.total > 0 && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
显示第 {(pagination.page - 1) * pagination.size + 1} - {Math.min(pagination.page * pagination.size, pagination.total)} 条,共 {pagination.total} 条记录
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(pagination.page - 1)}
|
||||||
|
disabled={!pagination.hasPrev}
|
||||||
|
>
|
||||||
|
上一页
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-sm text-muted-foreground">第</span>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={pagination.totalPages}
|
||||||
|
value={pagination.page}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newPage = parseInt(e.target.value);
|
||||||
|
if (!isNaN(newPage)) {
|
||||||
|
onPageChange(newPage);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-16 h-8 text-center border rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm text-muted-foreground">/ {pagination.totalPages} 页</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onPageChange(pagination.page + 1)}
|
||||||
|
disabled={!pagination.hasNext}
|
||||||
|
>
|
||||||
|
下一页
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9,16 +9,17 @@ import { UserFilters } from '../types';
|
|||||||
|
|
||||||
interface UserManagementFiltersProps {
|
interface UserManagementFiltersProps {
|
||||||
filters: UserFilters;
|
filters: UserFilters;
|
||||||
onFiltersChange: (filters: UserFilters) => void;
|
onSearchChange: (value: string) => void;
|
||||||
|
onStatusFilterChange: (value: string) => void;
|
||||||
|
onTypeFilterChange: (value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserManagementFilters({ filters, onFiltersChange }: UserManagementFiltersProps) {
|
export function UserManagementFilters({
|
||||||
const updateFilter = (key: keyof UserFilters, value: string) => {
|
filters,
|
||||||
onFiltersChange({
|
onSearchChange,
|
||||||
...filters,
|
onStatusFilterChange,
|
||||||
[key]: value
|
onTypeFilterChange
|
||||||
});
|
}: UserManagementFiltersProps) {
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-4">
|
<Card className="p-4">
|
||||||
@@ -29,12 +30,12 @@ export function UserManagementFilters({ filters, onFiltersChange }: UserManageme
|
|||||||
<Input
|
<Input
|
||||||
placeholder="搜索用户名、姓名、电话、企业..."
|
placeholder="搜索用户名、姓名、电话、企业..."
|
||||||
value={filters.searchKeyword}
|
value={filters.searchKeyword}
|
||||||
onChange={(e) => updateFilter('searchKeyword', e.target.value)}
|
onChange={(e) => onSearchChange(e.target.value)}
|
||||||
className="pl-10"
|
className="pl-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Select value={filters.typeFilter} onValueChange={(value) => updateFilter('typeFilter', value)}>
|
<Select value={filters.typeFilter} onValueChange={onTypeFilterChange}>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-40">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -45,7 +46,7 @@ export function UserManagementFilters({ filters, onFiltersChange }: UserManageme
|
|||||||
<SelectItem value="employee">员工</SelectItem>
|
<SelectItem value="employee">员工</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Select value={filters.statusFilter} onValueChange={(value) => updateFilter('statusFilter', value)}>
|
<Select value={filters.statusFilter} onValueChange={onStatusFilterChange}>
|
||||||
<SelectTrigger className="w-40">
|
<SelectTrigger className="w-40">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -1,31 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 用户管理页面头部组件 - 页面标题和操作按钮
|
||||||
|
* 功能:页面标题显示、刷新功能、统计数据展示
|
||||||
|
* 路径:/central-config/tenant/user-management/components/UserManagementHeader
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Plus, Download } from 'lucide-react';
|
import { Users, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
interface UserManagementHeaderProps {
|
interface UserManagementHeaderProps {
|
||||||
onAddUser: () => void;
|
stats: Array<{
|
||||||
onExport: () => void;
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
bg: string;
|
||||||
|
}>;
|
||||||
|
onRefresh: () => void;
|
||||||
|
loading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserManagementHeader({ onAddUser, onExport }: UserManagementHeaderProps) {
|
export function UserManagementHeader({ stats, onRefresh, loading }: UserManagementHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between">
|
<Card className="p-6 bg-gradient-to-r from-blue-50 dark:from-blue-950 to-indigo-50 dark:to-indigo-950 border-blue-200 dark:border-blue-800">
|
||||||
<div>
|
<div className="flex items-start justify-between">
|
||||||
<h2 className="text-green-800">用户管理</h2>
|
<div className="flex items-start gap-3">
|
||||||
<p className="text-muted-foreground">平台所有用户账户的集中管理</p>
|
<Users className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="mb-2">用户管理</h2>
|
||||||
|
<p className="text-sm text-muted-foreground mb-3">
|
||||||
|
平台所有用户账户的集中管理,支持搜索、筛选和详情查看
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
|
||||||
|
搜索功能
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
|
||||||
|
状态筛选
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center text-sm bg-white dark:bg-gray-800 px-3 py-1 rounded-full">
|
||||||
|
详情查看
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button variant="outline" size="sm" onClick={onRefresh} disabled={loading}>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
</Card>
|
||||||
<Button variant="outline" onClick={onExport}>
|
|
||||||
<Download className="w-4 h-4 mr-2" />
|
|
||||||
导出
|
|
||||||
</Button>
|
|
||||||
<Button onClick={onAddUser}>
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
添加用户
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,47 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 用户管理统计卡片组件 - 用户统计数据展示界面
|
||||||
|
* 功能:总用户数、活跃用户、管理员、已验证用户统计展示
|
||||||
|
* 路径:/central-config/tenant/user-management/components/UserManagementStatsCards
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { UserManagementStats, User } from '../types';
|
|
||||||
|
|
||||||
interface UserManagementStatsCardsProps {
|
interface UserManagementStatsCardsProps {
|
||||||
users: User[];
|
stats: Array<{
|
||||||
|
label: string;
|
||||||
|
value: number;
|
||||||
|
color: string;
|
||||||
|
bg: string;
|
||||||
|
}>;
|
||||||
|
loading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UserManagementStatsCards({ users }: UserManagementStatsCardsProps) {
|
export function UserManagementStatsCards({
|
||||||
const stats: UserManagementStats[] = [
|
stats,
|
||||||
{
|
loading = false
|
||||||
label: '总用户数',
|
}: UserManagementStatsCardsProps) {
|
||||||
value: users.length,
|
if (loading) {
|
||||||
color: 'text-blue-600',
|
return (
|
||||||
bg: 'bg-blue-100',
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
},
|
{stats.map((_, index) => (
|
||||||
{
|
<Card key={index} className="p-4">
|
||||||
label: '超级管理员',
|
<div className="animate-pulse">
|
||||||
value: users.filter(u => u.userType === 'super_admin').length,
|
<div className="h-4 bg-gray-200 rounded mb-2"></div>
|
||||||
color: 'text-purple-600',
|
<div className="h-8 bg-gray-200 rounded"></div>
|
||||||
bg: 'bg-purple-100',
|
</div>
|
||||||
},
|
</Card>
|
||||||
{
|
))}
|
||||||
label: '企业管理员',
|
</div>
|
||||||
value: users.filter(u => u.userType === 'enterprise_admin').length,
|
);
|
||||||
color: 'text-blue-600',
|
}
|
||||||
bg: 'bg-blue-100',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '正常用户',
|
|
||||||
value: users.filter(u => u.status === 'active').length,
|
|
||||||
color: 'text-green-600',
|
|
||||||
bg: 'bg-green-100',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{stats.map((stat, index) => (
|
{stats.map((stat, index) => (
|
||||||
<Card key={index} className="p-4">
|
<Card key={index} className="p-4">
|
||||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||||
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div>
|
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 用户管理API接口 - 用户数据查询接口服务
|
||||||
|
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||||
|
* 路径:/central-config/tenant/user-management/components/userManagementApi
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getAuthToken } from "@/utils/token";
|
||||||
|
import {
|
||||||
|
getUsersApiV1UsersGet,
|
||||||
|
} from "@/lib/api/sdk.gen";
|
||||||
|
|
||||||
|
// API返回的用户数据类型
|
||||||
|
export interface UserData {
|
||||||
|
id: string;
|
||||||
|
tenant_id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
full_name: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
is_active: boolean;
|
||||||
|
is_superuser: boolean;
|
||||||
|
is_verified: boolean;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
last_login_at: string | null;
|
||||||
|
avatar_url: string | null;
|
||||||
|
bio: string | null;
|
||||||
|
display_name: string | null;
|
||||||
|
department_id: string | null;
|
||||||
|
department_name: string | null;
|
||||||
|
scope: string;
|
||||||
|
company_name: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API响应接口
|
||||||
|
export interface UsersApiResponse {
|
||||||
|
data: UserData[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_prev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询参数接口
|
||||||
|
export interface UsersQueryParams {
|
||||||
|
search?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
order_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面使用的用户数据类型(转换后的)
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
fullName: string | null;
|
||||||
|
phone: string | null;
|
||||||
|
isActive: boolean;
|
||||||
|
isSuperuser: boolean;
|
||||||
|
isVerified: boolean;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
lastLoginAt: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
bio: string | null;
|
||||||
|
displayName: string | null;
|
||||||
|
departmentId: string | null;
|
||||||
|
departmentName: string | null;
|
||||||
|
scope: string;
|
||||||
|
companyName: string | null;
|
||||||
|
tenantId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户列表数据
|
||||||
|
*/
|
||||||
|
export async function fetchUsers(params: UsersQueryParams = {}): Promise<UsersApiResponse> {
|
||||||
|
try {
|
||||||
|
// 构建查询参数对象
|
||||||
|
const queryParams: any = {};
|
||||||
|
|
||||||
|
if (params.search) queryParams.search = params.search;
|
||||||
|
if (params.is_active !== undefined) queryParams.is_active = params.is_active;
|
||||||
|
if (params.page) queryParams.page = params.page;
|
||||||
|
if (params.size) queryParams.size = params.size;
|
||||||
|
if (params.order_by) queryParams.order_by = params.order_by;
|
||||||
|
if (params.sort_order) queryParams.sort_order = params.sort_order;
|
||||||
|
|
||||||
|
// 默认参数
|
||||||
|
if (!params.page) queryParams.page = 1;
|
||||||
|
if (!params.size) queryParams.size = 10;
|
||||||
|
if (!params.sort_order) queryParams.sort_order = 'desc';
|
||||||
|
|
||||||
|
// 使用SDK API调用用户查询接口,添加缓存破坏器和认证头部
|
||||||
|
const token = getAuthToken();
|
||||||
|
console.log('用户管理API调用参数:', queryParams);
|
||||||
|
|
||||||
|
const response = await getUsersApiV1UsersGet({
|
||||||
|
query: {
|
||||||
|
...queryParams,
|
||||||
|
// 添加时间戳防止缓存
|
||||||
|
_t: Date.now(),
|
||||||
|
},
|
||||||
|
headers: token ? {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
} : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = response.data as any;
|
||||||
|
console.log('用户管理API响应:', data);
|
||||||
|
|
||||||
|
// 转换响应数据格式以匹配现有的接口
|
||||||
|
// API返回的数据结构: { data: [...], total: 25, page: 1, size: 10, ... }
|
||||||
|
return {
|
||||||
|
data: data?.data || [], // 注意:实际数据在 data.data 中
|
||||||
|
total: data?.total || 0,
|
||||||
|
page: data?.page || 1,
|
||||||
|
size: data?.size || 10,
|
||||||
|
total_pages: data?.total_pages || 0,
|
||||||
|
has_next: data?.has_next || false,
|
||||||
|
has_prev: data?.has_prev || false,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch users:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将API数据转换为页面所需的用户数据格式
|
||||||
|
* 优先显示display_name,其次full_name,最后username
|
||||||
|
*/
|
||||||
|
export function transformUserData(user: UserData): User {
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
fullName: user.full_name,
|
||||||
|
phone: user.phone,
|
||||||
|
isActive: user.is_active,
|
||||||
|
isSuperuser: user.is_superuser,
|
||||||
|
isVerified: user.is_verified,
|
||||||
|
createdAt: formatDate(user.created_at),
|
||||||
|
updatedAt: formatDate(user.updated_at),
|
||||||
|
lastLoginAt: user.last_login_at ? formatDate(user.last_login_at) : null,
|
||||||
|
avatarUrl: user.avatar_url,
|
||||||
|
bio: user.bio,
|
||||||
|
displayName: user.display_name || user.full_name || user.username,
|
||||||
|
departmentId: user.department_id,
|
||||||
|
departmentName: user.department_name,
|
||||||
|
scope: user.scope,
|
||||||
|
companyName: user.company_name,
|
||||||
|
tenantId: user.tenant_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化日期
|
||||||
|
*/
|
||||||
|
function formatDate(dateString: string): string {
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
}).replace(/\//g, '-');
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination state interface for page components
|
||||||
|
export interface PaginationState {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
}
|
||||||
@@ -1,271 +1,330 @@
|
|||||||
|
/**
|
||||||
|
* filekorolheader: 用户管理页面 - 用户查询和管理页面
|
||||||
|
* 功能:用户列表查询、搜索筛选、详情查看、用户管理
|
||||||
|
* 路径:/central-config/tenant/user-management
|
||||||
|
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式
|
||||||
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useReducer, useEffect } from 'react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
||||||
import { UserManagementHeader } from './components/UserManagementHeader';
|
import { UserManagementHeader } from './components/UserManagementHeader';
|
||||||
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
||||||
import { UserManagementFilters } from './components/UserManagementFilters';
|
import { UserManagementFilters } from './components/UserManagementFilters';
|
||||||
import { UserList } from './components/UserList';
|
import { UserList } from './components/UserList';
|
||||||
import { UserFormDialog } from './components/UserFormDialog';
|
|
||||||
import { UserDetailDialog } from './components/UserDetailDialog';
|
import { UserDetailDialog } from './components/UserDetailDialog';
|
||||||
import { User, Enterprise, UserFilters, UserFormData } from './types';
|
import { Enterprise, UserFilters } from './types';
|
||||||
|
|
||||||
export default function TenantUserManagementPage() {
|
// 用户管理状态管理
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
interface UserManagementState {
|
||||||
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
|
users: User[];
|
||||||
const [filters, setFilters] = useState<UserFilters>({
|
enterprises: Enterprise[];
|
||||||
|
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_ENTERPRISES'; payload: Enterprise[] }
|
||||||
|
| { 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_ENTERPRISES':
|
||||||
|
return { ...state, enterprises: 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: [],
|
||||||
|
enterprises: [],
|
||||||
|
loading: false,
|
||||||
|
error: null,
|
||||||
|
pagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10,
|
||||||
|
total: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
hasNext: false,
|
||||||
|
hasPrev: false,
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
searchKeyword: '',
|
searchKeyword: '',
|
||||||
statusFilter: 'all',
|
statusFilter: 'all',
|
||||||
typeFilter: 'all'
|
typeFilter: 'all'
|
||||||
});
|
},
|
||||||
const [showForm, setShowForm] = useState(false);
|
sortBy: 'created_at',
|
||||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
sortOrder: 'desc',
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
selectedUser: null,
|
||||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
showDetailDialog: false,
|
||||||
const [formData, setFormData] = useState<UserFormData>({
|
};
|
||||||
userType: 'enterprise_admin',
|
|
||||||
status: 'active',
|
|
||||||
roleIds: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
|
export default function TenantUserManagementPage() {
|
||||||
|
const [state, dispatch] = useReducer(userManagementReducer, initialState);
|
||||||
|
|
||||||
|
// 加载用户数据
|
||||||
|
const loadUsers = async (resetPage = false) => {
|
||||||
|
try {
|
||||||
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
|
const params: UsersQueryParams = {
|
||||||
|
search: state.filters.searchKeyword || undefined,
|
||||||
|
page: resetPage ? 1 : state.pagination.page,
|
||||||
|
size: state.pagination.size,
|
||||||
|
order_by: state.sortBy,
|
||||||
|
sort_order: state.sortOrder,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据状态筛选器设置 is_active 参数
|
||||||
|
if (state.filters.statusFilter === 'active') {
|
||||||
|
params.is_active = true;
|
||||||
|
} else if (state.filters.statusFilter === 'inactive') {
|
||||||
|
params.is_active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : '加载用户数据失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 加载企业数据(这里暂时使用mock数据,后续可以添加企业API)
|
||||||
|
const loadEnterprises = () => {
|
||||||
|
// 这里可以添加企业API调用,现在使用mock数据
|
||||||
|
const mockEnterprises: Enterprise[] = [
|
||||||
|
{ id: 'ent-1', name: '丰收现代农业集团' },
|
||||||
|
{ id: 'ent-2', name: '绿色种植科技有限公司' },
|
||||||
|
{ id: 'ent-3', name: '智慧农业示范区' },
|
||||||
|
];
|
||||||
|
dispatch({ type: 'SET_ENTERPRISES', payload: mockEnterprises });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索处理
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { searchKeyword: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态筛选
|
||||||
|
const handleStatusFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { statusFilter: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 类型筛选
|
||||||
|
const handleTypeFilter = (value: string) => {
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
|
const handleSort = (sortBy: string) => {
|
||||||
|
const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc';
|
||||||
|
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
const handlePageChange = (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 } });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
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 handleDelete = (user: User) => {
|
||||||
|
if (!confirm(`确定要删除用户 ${user.fullName || user.username} 吗?`)) return;
|
||||||
|
// 这里可以添加删除逻辑,调用API删除用户
|
||||||
|
toast.info('删除功能开发中...');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换用户状态
|
||||||
|
const handleToggleStatus = (user: User) => {
|
||||||
|
const newStatus = !user.isActive;
|
||||||
|
const statusText = newStatus ? '激活' : '停用';
|
||||||
|
if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return;
|
||||||
|
// 这里可以添加状态切换逻辑,调用API更新用户状态
|
||||||
|
toast.info(`${statusText}功能开发中...`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置密码
|
||||||
|
const handleResetPassword = (user: User) => {
|
||||||
|
if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return;
|
||||||
|
// 这里可以添加重置密码逻辑,调用API重置密码
|
||||||
|
toast.info('重置密码功能开发中...');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刷新数据
|
||||||
|
const handleRefresh = () => {
|
||||||
|
dispatch({ type: 'REFRESH_DATA' });
|
||||||
|
loadUsers(true);
|
||||||
|
toast.success('数据已刷新');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 统计数据计算
|
||||||
|
const stats = [
|
||||||
|
{
|
||||||
|
label: '总用户数',
|
||||||
|
value: state.pagination.total,
|
||||||
|
color: 'text-blue-600',
|
||||||
|
bg: 'bg-blue-100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '活跃用户',
|
||||||
|
value: state.users.filter(u => u.isActive).length,
|
||||||
|
color: 'text-green-600',
|
||||||
|
bg: 'bg-green-100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '管理员',
|
||||||
|
value: state.users.filter(u => u.isSuperuser).length,
|
||||||
|
color: 'text-purple-600',
|
||||||
|
bg: 'bg-purple-100',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '已验证',
|
||||||
|
value: state.users.filter(u => u.isVerified).length,
|
||||||
|
color: 'text-orange-600',
|
||||||
|
bg: 'bg-orange-100',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 初始化和监听器
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUsers();
|
loadUsers();
|
||||||
loadEnterprises();
|
loadEnterprises();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadEnterprises = () => {
|
useEffect(() => {
|
||||||
const data = localStorage.getItem('smart_agriculture_enterprises');
|
const timer = setTimeout(() => {
|
||||||
if (data) {
|
loadUsers();
|
||||||
const allEnterprises = JSON.parse(data);
|
}, 300);
|
||||||
setEnterprises(allEnterprises.map((e: any) => ({ id: e.id, name: e.name })));
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [state.filters.searchKeyword, state.filters.statusFilter, state.filters.typeFilter, state.sortBy, state.sortOrder]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.pagination.page > 1) {
|
||||||
|
loadUsers();
|
||||||
}
|
}
|
||||||
};
|
}, [state.pagination.page]);
|
||||||
|
|
||||||
const loadUsers = () => {
|
|
||||||
const data = localStorage.getItem('smart_agriculture_users');
|
|
||||||
if (data) {
|
|
||||||
setUsers(JSON.parse(data));
|
|
||||||
} else {
|
|
||||||
// 初始化示例数据
|
|
||||||
const mockUsers: User[] = [
|
|
||||||
{
|
|
||||||
id: 'user-1',
|
|
||||||
username: 'admin',
|
|
||||||
name: '系统管理员',
|
|
||||||
phone: '13900000000',
|
|
||||||
email: 'admin@system.com',
|
|
||||||
userType: 'super_admin',
|
|
||||||
roleIds: ['role-1'],
|
|
||||||
roles: ['超级管理员'],
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-01-01T00:00:00',
|
|
||||||
updatedAt: '2024-01-01T00:00:00',
|
|
||||||
lastLoginTime: '2024-10-14T10:00:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'user-2',
|
|
||||||
username: 'ent_admin_1',
|
|
||||||
name: '李总',
|
|
||||||
phone: '13900139002',
|
|
||||||
email: 'litotal@fengshou.com',
|
|
||||||
enterpriseId: 'ent-2',
|
|
||||||
enterpriseName: '丰收现代农业集团',
|
|
||||||
userType: 'enterprise_admin',
|
|
||||||
roleIds: ['role-2'],
|
|
||||||
roles: ['企业管理员'],
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-10-05T10:00:00',
|
|
||||||
updatedAt: '2024-10-05T10:00:00',
|
|
||||||
lastLoginTime: '2024-10-14T09:00:00',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'user-3',
|
|
||||||
username: 'zhangsan',
|
|
||||||
name: '张三',
|
|
||||||
phone: '13800138001',
|
|
||||||
email: 'zhangsan@fengshou.com',
|
|
||||||
enterpriseId: 'ent-2',
|
|
||||||
enterpriseName: '丰收现代农业集团',
|
|
||||||
userType: 'employee',
|
|
||||||
roleIds: ['role-3'],
|
|
||||||
roles: ['操作员'],
|
|
||||||
status: 'active',
|
|
||||||
createdAt: '2024-10-01T08:00:00',
|
|
||||||
updatedAt: '2024-10-01T08:00:00',
|
|
||||||
lastLoginTime: '2024-10-14T08:30:00',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(mockUsers));
|
|
||||||
setUsers(mockUsers);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const filteredUsers = users.filter(user => {
|
|
||||||
const matchKeyword = !filters.searchKeyword ||
|
|
||||||
user.name.includes(filters.searchKeyword) ||
|
|
||||||
user.username.includes(filters.searchKeyword) ||
|
|
||||||
user.phone.includes(filters.searchKeyword) ||
|
|
||||||
(user.enterpriseName && user.enterpriseName.includes(filters.searchKeyword));
|
|
||||||
|
|
||||||
const matchStatus = filters.statusFilter === 'all' || user.status === filters.statusFilter;
|
|
||||||
const matchType = filters.typeFilter === 'all' || user.userType === filters.typeFilter;
|
|
||||||
|
|
||||||
return matchKeyword && matchStatus && matchType;
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleAddUser = () => {
|
|
||||||
setEditingUser(null);
|
|
||||||
setFormData({
|
|
||||||
userType: 'enterprise_admin',
|
|
||||||
status: 'active',
|
|
||||||
roleIds: [],
|
|
||||||
});
|
|
||||||
setShowForm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (user: User) => {
|
|
||||||
setEditingUser(user);
|
|
||||||
setFormData(user);
|
|
||||||
setShowForm(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
if (!formData.username || !formData.name || !formData.phone) {
|
|
||||||
toast.error('请填写必填项');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是企业管理员,必须选择企业
|
|
||||||
if (formData.userType === 'enterprise_admin' && !formData.enterpriseId) {
|
|
||||||
toast.error('企业管理员必须选择所属企业');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据选择的企业ID设置企业名称
|
|
||||||
let enterpriseName = formData.enterpriseName;
|
|
||||||
if (formData.enterpriseId && !enterpriseName) {
|
|
||||||
const selectedEnterprise = enterprises.find(e => e.id === formData.enterpriseId);
|
|
||||||
if (selectedEnterprise) {
|
|
||||||
enterpriseName = selectedEnterprise.name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editingUser) {
|
|
||||||
const updated = users.map(user =>
|
|
||||||
user.id === editingUser.id
|
|
||||||
? {
|
|
||||||
...user,
|
|
||||||
...formData,
|
|
||||||
enterpriseName,
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
: user
|
|
||||||
);
|
|
||||||
setUsers(updated);
|
|
||||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
|
||||||
toast.success('用户信息更新成功');
|
|
||||||
} else {
|
|
||||||
const newUser: User = {
|
|
||||||
id: `user-${Date.now()}`,
|
|
||||||
...formData as User,
|
|
||||||
enterpriseName,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
const updated = [...users, newUser];
|
|
||||||
setUsers(updated);
|
|
||||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
|
||||||
toast.success('用户添加成功');
|
|
||||||
}
|
|
||||||
|
|
||||||
setShowForm(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
if (!confirm('确定要删除该用户吗?')) return;
|
|
||||||
|
|
||||||
const updated = users.filter(user => user.id !== id);
|
|
||||||
setUsers(updated);
|
|
||||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
|
||||||
toast.success('用户删除成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleStatus = (user: User) => {
|
|
||||||
const newStatus = user.status === 'active' ? 'frozen' : 'active';
|
|
||||||
const updated = users.map(u =>
|
|
||||||
u.id === user.id
|
|
||||||
? { ...u, status: newStatus, updatedAt: new Date().toISOString() }
|
|
||||||
: u
|
|
||||||
);
|
|
||||||
setUsers(updated);
|
|
||||||
localStorage.setItem('smart_agriculture_users', JSON.stringify(updated));
|
|
||||||
toast.success(newStatus === 'active' ? '账户已激活' : '账户已冻结');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetPassword = (user: User) => {
|
|
||||||
if (!confirm(`确定要重置 ${user.name} 的密码吗?`)) return;
|
|
||||||
toast.success('密码已重置为:123456');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleViewDetail = (user: User) => {
|
|
||||||
setSelectedUser(user);
|
|
||||||
setShowDetailDialog(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleExport = () => {
|
|
||||||
const dataStr = JSON.stringify(users, null, 2);
|
|
||||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(dataBlob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = `users_${new Date().getTime()}.json`;
|
|
||||||
link.click();
|
|
||||||
toast.success('用户数据导出成功');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<UserManagementHeader
|
{/* 页面标题和统计 */}
|
||||||
onAddUser={handleAddUser}
|
<UserManagementHeader stats={stats} onRefresh={handleRefresh} loading={state.loading} />
|
||||||
onExport={handleExport}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 统计卡片 */}
|
{/* 统计卡片 */}
|
||||||
<UserManagementStatsCards users={users} />
|
<UserManagementStatsCards stats={stats} />
|
||||||
|
|
||||||
{/* 搜索和筛选 */}
|
{/* 搜索和筛选 */}
|
||||||
<UserManagementFilters
|
<UserManagementFilters
|
||||||
filters={filters}
|
filters={state.filters}
|
||||||
onFiltersChange={setFilters}
|
onSearchChange={handleSearch}
|
||||||
|
onStatusFilterChange={handleStatusFilter}
|
||||||
|
onTypeFilterChange={handleTypeFilter}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* 错误显示 */}
|
||||||
|
{state.error && (
|
||||||
|
<div className="p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
|
||||||
|
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
|
||||||
|
<span>{state.error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 用户列表 */}
|
{/* 用户列表 */}
|
||||||
<UserList
|
<UserList
|
||||||
users={filteredUsers}
|
users={state.users}
|
||||||
|
loading={state.loading}
|
||||||
|
pagination={state.pagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
onViewDetail={handleViewDetail}
|
onViewDetail={handleViewDetail}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onResetPassword={handleResetPassword}
|
|
||||||
onToggleStatus={handleToggleStatus}
|
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
onToggleStatus={handleToggleStatus}
|
||||||
|
onResetPassword={handleResetPassword}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 添加/编辑表单 */}
|
{/* 用户详情对话框 */}
|
||||||
<UserFormDialog
|
|
||||||
open={showForm}
|
|
||||||
onOpenChange={setShowForm}
|
|
||||||
editingUser={editingUser}
|
|
||||||
formData={formData}
|
|
||||||
onFormDataChange={setFormData}
|
|
||||||
onSave={handleSave}
|
|
||||||
enterprises={enterprises}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 详情对话框 */}
|
|
||||||
<UserDetailDialog
|
<UserDetailDialog
|
||||||
open={showDetailDialog}
|
open={state.showDetailDialog}
|
||||||
onOpenChange={setShowDetailDialog}
|
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||||
selectedUser={selectedUser}
|
user={state.selectedUser}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,18 +3,37 @@
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
name: string;
|
email: string;
|
||||||
phone: string;
|
fullName: string | null;
|
||||||
email?: string;
|
phone: string | null;
|
||||||
enterpriseId?: string;
|
isActive: boolean;
|
||||||
enterpriseName?: string;
|
isSuperuser: boolean;
|
||||||
roleIds: string[];
|
isVerified: boolean;
|
||||||
roles?: string[];
|
|
||||||
userType: UserType;
|
|
||||||
status: UserStatus;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
lastLoginAt: string | null;
|
||||||
|
avatarUrl: string | null;
|
||||||
|
bio: string | null;
|
||||||
|
displayName: string | null;
|
||||||
|
departmentId: string | null;
|
||||||
|
departmentName: string | null;
|
||||||
|
scope: string;
|
||||||
|
companyName: string | null;
|
||||||
|
tenantId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为了兼容现有代码,保留一些映射属性
|
||||||
|
export interface UserWithLegacyFields extends User {
|
||||||
|
// 向后兼容的属性
|
||||||
|
name: string;
|
||||||
|
phone: string;
|
||||||
|
enterpriseId?: string;
|
||||||
|
enterpriseName?: string;
|
||||||
|
userType: UserType;
|
||||||
|
status: UserStatus;
|
||||||
lastLoginTime?: string;
|
lastLoginTime?: string;
|
||||||
|
roleIds: string[];
|
||||||
|
roles?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserType = 'super_admin' | 'enterprise_admin' | 'employee';
|
export type UserType = 'super_admin' | 'enterprise_admin' | 'employee';
|
||||||
@@ -40,6 +59,37 @@ export interface UserFilters {
|
|||||||
typeFilter: string;
|
typeFilter: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API响应数据类型
|
||||||
|
export interface UsersApiResponse {
|
||||||
|
data: User[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total_pages: number;
|
||||||
|
has_next: boolean;
|
||||||
|
has_prev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页状态
|
||||||
|
export interface PaginationState {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
total: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
hasPrev: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API查询参数
|
||||||
|
export interface UsersQueryParams {
|
||||||
|
search?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
order_by?: string;
|
||||||
|
sort_order?: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
export interface UserFormData {
|
export interface UserFormData {
|
||||||
username?: string;
|
username?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user