生产管理系统 - 员工管理、企业信息联调完毕以及一些页面上的样式修改

This commit is contained in:
2025-11-05 17:18:25 +08:00
parent c10b507cf6
commit 1fb128ede5
16 changed files with 1266 additions and 207 deletions

View File

@@ -136,7 +136,6 @@ export default function AuditHistoryPage() {
dispatch({ type: 'SET_LOADING', payload: true });
const params: AuditLogsQueryParams = {
search: state.filters.search_keyword || undefined,
page: state.pagination.page,
size: state.pagination.size
};
@@ -244,10 +243,10 @@ export default function AuditHistoryPage() {
// 工具函数
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>;
case 'SUBMIT':
return <Badge className="bg-blue-100 text-blue-700"></Badge>;
case 'AUDIT':
return <Badge className="bg-orange-100 text-orange-700"></Badge>;
default:
return <Badge variant="outline">{action}</Badge>;
}
@@ -258,9 +257,11 @@ export default function AuditHistoryPage() {
case 'approved':
return <Badge className="bg-green-100 text-green-700"></Badge>;
case 'rejected':
return <Badge className="bg-red-100 text-red-700"></Badge>;
return <Badge className="bg-red-100 text-red-700"></Badge>;
case 'pending':
return <Badge className="bg-yellow-100 text-yellow-700"></Badge>;
case 'draft':
return <Badge className="bg-gray-100 text-gray-700">稿</Badge>;
default:
return <Badge variant="outline">{result}</Badge>;
}
@@ -328,8 +329,8 @@ export default function AuditHistoryPage() {
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="register"></SelectItem>
<SelectItem value="update"></SelectItem>
<SelectItem value="SUBMIT"></SelectItem>
<SelectItem value="AUDIT"></SelectItem>
</SelectContent>
</Select>
</div>
@@ -343,8 +344,9 @@ export default function AuditHistoryPage() {
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="approved"></SelectItem>
<SelectItem value="rejected"></SelectItem>
<SelectItem value="rejected"></SelectItem>
<SelectItem value="pending"></SelectItem>
<SelectItem value="draft">稿</SelectItem>
</SelectContent>
</Select>
</div>
@@ -413,22 +415,15 @@ export default function AuditHistoryPage() {
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>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
@@ -445,18 +440,23 @@ export default function AuditHistoryPage() {
<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>
<span className="font-medium">{record.enterpriseName}</span>
</div>
</TableCell>
<TableCell>{getActionBadge(record.action)}</TableCell>
<TableCell className="text-sm text-muted-foreground">
{record.action === 'SUBMIT' ? record.submitTime : '-'}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{record.action === 'AUDIT' ? record.actionTime : '-'}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-500" />
<span>{record.action_by}</span>
<span>{record.actionBy || '-'}</span>
</div>
</TableCell>
<TableCell className="text-sm">{record.action_time}</TableCell>
<TableCell>{getResultBadge(record.result)}</TableCell>
<TableCell>{getResultBadge(record.auditStatus)}</TableCell>
<TableCell>
<Button
size="sm"
@@ -542,28 +542,42 @@ export default function AuditHistoryPage() {
<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 className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.enterpriseName}</div>
</div>
<div>
<Label></Label>
<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>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
{state.selectedRecord.action === 'SUBMIT' ? state.selectedRecord.submitTime : '-'}
</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.action_time}</div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
{state.selectedRecord.action === 'AUDIT' ? state.selectedRecord.actionTime : '-'}
</div>
</div>
<div>
<Label></Label>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.actionBy || '-'}</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>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
{state.selectedRecord.action_summary || '-'}
{state.selectedRecord.changeSummary || '-'}
</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.auditComment || '-'}
</div>
</div>
</div>
@@ -574,25 +588,61 @@ export default function AuditHistoryPage() {
<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 className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.companyType || '-'}</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}
{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 className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.detailedAddress || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_registrant || '-'}</div>
<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 className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.contactPhone || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.companyScale || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.registeredCapital || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
<code className="text-sm">{state.selectedRecord.snapshot.socialCreditCode || '-'}</code>
</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.legalPersonName || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
<code className="text-sm">{state.selectedRecord.snapshot.bankAccount || '-'}</code>
</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.bankName || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.bankFullName || '-'}</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.bankAddress || '-'}</div>
</div>
</div>
</TabsContent>
@@ -609,19 +659,29 @@ export default function AuditHistoryPage() {
<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>
<code className="text-sm">{state.selectedRecord.enterpriseId || '-'}</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 className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.ipAddress || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md text-sm">
{state.selectedRecord.user_agent || '-'}
{state.selectedRecord.userAgent || '-'}
</div>
</div>
<div>
<Label>ID</Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
<code className="text-sm">{state.selectedRecord.requestId || '-'}</code>
</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.createdAt}</div>
</div>
</div>
</TabsContent>
</Tabs>

View File

@@ -49,12 +49,11 @@ export function BasicInfoForm({
<SelectValue placeholder="选择企业类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="个体工商户"></SelectItem>
<SelectItem value="有限责任公司"></SelectItem>
<SelectItem value="股份有限公司"></SelectItem>
<SelectItem value="个人独资企业"></SelectItem>
<SelectItem value="合伙企业"></SelectItem>
<SelectItem value="个体工商户"></SelectItem>
<SelectItem value="农民专业合作社"></SelectItem>
<SelectItem value="其他"></SelectItem>
</SelectContent>
</Select>
) : (

View File

@@ -6,7 +6,7 @@
*/
import { getAuthToken } from "@/utils/token";
import { getTenantApiV1TenantsTenantIdGet } from "@/lib/api/sdk.gen";
import { getCurrentTenantApiV1TenantsMeGet, submitTenantAuditApiV1TenantsSubmitPost } from "@/lib/api/sdk.gen";
import { Enterprise } from '../types';
// API返回的租户数据类型根据实际API返回定义
@@ -42,6 +42,33 @@ export interface TenantApiData {
updated_at: string;
}
// 提交审核请求参数接口
export interface SubmitAuditRequest {
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;
bank_permit_image: string | null;
social_credit_code: string | null;
business_license_image: string | null;
legal_person_name: string | null;
id_card_front_image: string | null;
id_card_back_image: string | null;
company_scale: string | null;
registered_capital: number | null;
established_date: string | null;
invoice_type: string | null;
business_scope: string | null;
}
/**
* 获取企业详细信息
*/
@@ -50,7 +77,7 @@ export async function fetchEnterpriseInfo(tenantId: string): Promise<Enterprise
const token = getAuthToken();
console.log('🏢 获取企业信息API调用租户ID:', tenantId);
const response = await getTenantApiV1TenantsTenantIdGet({
const response = await getCurrentTenantApiV1TenantsMeGet({
path: {
tenant_id: tenantId,
},
@@ -84,6 +111,39 @@ export async function updateEnterpriseInfo(tenantId: string, formData: Partial<E
return null;
}
/**
* 提交企业审核
* @param tenantId 租户ID
* @param data 提交审核的数据
* @returns 提交结果
*/
export async function submitEnterpriseAudit(tenantId: string, data: SubmitAuditRequest): Promise<void> {
try {
const token = getAuthToken();
console.log('🏢 提交企业审核API调用租户ID:', tenantId, '数据:', data);
const response = await submitTenantAuditApiV1TenantsSubmitPost({
path: {
tenant_id: tenantId,
},
body: data,
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
console.error('🏢 提交企业审核API错误:', response.error);
throw new Error(`提交审核失败: ${response.error.message || '未知错误'}`);
}
console.log('🏢 提交企业审核API成功');
} catch (error) {
console.error('🏢 提交企业审核失败:', error);
throw error;
}
}
/**
* 将API数据转换为页面所需的企业数据格式
*/

View File

@@ -16,7 +16,7 @@ import { SystemInfo } from './components/SystemInfo';
import { OperationTips } from './components/OperationTips';
import { Enterprise } from './types';
import { getAuthUser } from '@/stores/modules/auth';
import { fetchEnterpriseInfo, updateEnterpriseInfo } from './components/enterpriseInfoApi';
import { fetchEnterpriseInfo, updateEnterpriseInfo, submitEnterpriseAudit, SubmitAuditRequest } from './components/enterpriseInfoApi';
export default function EnterpriseInfoPage() {
const [enterprise, setEnterprise] = useState<Enterprise | null>(null);
@@ -88,30 +88,56 @@ export default function EnterpriseInfoPage() {
return;
}
if (!formData.name || !formData.type || !formData.socialCreditCode) {
toast.error('请填写必填项');
// 验证必填字段
if (!formData.name?.trim()) {
toast.error('请填写企业名称');
return;
}
try {
setLoading(true);
console.log('🏢 开始更新企业信息');
console.log('🏢 开始提交企业审核');
// 调用API更新企业信息
const updatedData = await updateEnterpriseInfo(currentUser.tenant_id, formData);
// 构建提交审核的数据
const submitData: SubmitAuditRequest = {
company_name: formData.name || '',
company_type: formData.type || null,
province: formData.province || null,
city: formData.city || null,
district: formData.district || null,
detailed_address: formData.address || null,
registrant: formData.registrant || null,
contact_phone: formData.contactPhone || null,
bank_account: formData.bankAccount || null,
bank_name: formData.bankName || null,
bank_full_name: formData.bankFullName || null,
bank_address: formData.bankAddress || null,
bank_permit_image: null, // 暂时设为null等待图片上传功能
social_credit_code: formData.socialCreditCode || null,
business_license_image: null, // 暂时设为null等待图片上传功能
legal_person_name: formData.legalPerson || null,
id_card_front_image: null, // 暂时设为null等待图片上传功能
id_card_back_image: null, // 暂时设为null等待图片上传功能
company_scale: formData.companySize || null,
registered_capital: formData.registeredCapital ? parseFloat(formData.registeredCapital.toString()) : null,
established_date: formData.establishmentDate || null,
invoice_type: formData.invoiceType || null,
business_scope: formData.businessScope || null,
};
if (updatedData) {
setEnterprise(updatedData);
setFormData(updatedData);
setIsEditing(false);
toast.success('企业信息已更新,等待管理员审核');
} else {
toast.info('企业信息保存功能待开发');
}
// 调用API提交审核
await submitEnterpriseAudit(currentUser.tenant_id, submitData);
// 提交成功,退出编辑模式
setIsEditing(false);
toast.success('企业信息已提交审核,请等待管理员审核');
// 重新加载企业信息以获取最新状态
await loadEnterpriseInfo(currentUser.tenant_id);
} catch (error) {
console.error('🏢 更新企业信息失败:', error);
const errorMessage = error instanceof Error ? error.message : '更新企业信息失败';
console.error('🏢 提交企业审核失败:', error);
const errorMessage = error instanceof Error ? error.message : '提交审核失败';
toast.error(errorMessage);
} finally {
setLoading(false);

View File

@@ -0,0 +1,212 @@
/**
* filekorolheader: 新建企业弹窗组件 - 企业创建表单弹窗
* 功能企业信息表单、数据验证、API调用、状态管理
* 路径:/central-config/tenant/enterprise-management/components/CreateEnterpriseDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn语义化样式TypeScript类型安全
*/
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Building2, Hash } from 'lucide-react';
import { toast } from 'sonner';
import { createEnterprise, CreateEnterpriseRequest } from './enterpriseApi';
interface CreateEnterpriseDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSuccess: () => void;
}
export function CreateEnterpriseDialog({ open, onOpenChange, onSuccess }: CreateEnterpriseDialogProps) {
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState<CreateEnterpriseRequest>({
company_name: '',
tenant_code: '',
company_type: '',
});
// 企业类型选项
const companyTypes = [
'个体工商户',
'有限责任公司',
'股份有限公司',
'合伙企业',
'其他',
];
// 重置表单
const resetForm = () => {
setFormData({
company_name: '',
tenant_code: '',
company_type: '',
});
};
// 关闭弹窗
const handleClose = () => {
if (!loading) {
onOpenChange(false);
resetForm();
}
};
// 处理输入变化
const handleInputChange = (field: keyof CreateEnterpriseRequest, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
// 表单验证
const validateForm = (): boolean => {
if (!formData.company_name.trim()) {
toast.error('请输入企业名称');
return false;
}
if (!formData.tenant_code.trim()) {
toast.error('请输入企业编码');
return false;
}
if (!formData.company_type.trim()) {
toast.error('请选择企业类型');
return false;
}
return true;
};
// 提交表单
const handleSubmit = async () => {
if (!validateForm()) return;
try {
setLoading(true);
console.log('🏢 开始创建企业:', formData);
// 调用API创建企业
const result = await createEnterprise(formData);
console.log('🏢 企业创建成功:', result);
toast.success('企业创建成功!');
// 关闭弹窗并重置表单
handleClose();
// 调用成功回调,刷新主页面数据
onSuccess();
} catch (error) {
console.error('🏢 创建企业失败:', error);
const errorMessage = error instanceof Error ? error.message : '创建企业失败,请稍后重试';
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Building2 className="w-5 h-5 text-blue-600 dark:text-blue-400" />
</DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* 企业名称 */}
<div className="space-y-2">
<Label htmlFor="company_name"> *</Label>
<div className="relative">
<Building2 className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
id="company_name"
placeholder="请输入企业全称"
value={formData.company_name}
onChange={(e) => handleInputChange('company_name', e.target.value)}
className="pl-10"
disabled={loading}
/>
</div>
</div>
{/* 企业编码 */}
<div className="space-y-2">
<Label htmlFor="tenant_code"> *</Label>
<div className="relative">
<Hash className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
<Input
id="tenant_code"
placeholder="请输入企业唯一编码SHNY001"
value={formData.tenant_code}
onChange={(e) => handleInputChange('tenant_code', e.target.value.toUpperCase())}
className="pl-10"
disabled={loading}
/>
</div>
<p className="text-xs text-muted-foreground">
</p>
</div>
{/* 企业类型 */}
<div className="space-y-2">
<Label htmlFor="company_type"> *</Label>
<Select
value={formData.company_type}
onValueChange={(value) => handleInputChange('company_type', value)}
disabled={loading}
>
<SelectTrigger>
<SelectValue placeholder="请选择企业类型" />
</SelectTrigger>
<SelectContent>
{companyTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 温馨提示 */}
<div className="p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200 font-medium mb-2"></p>
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
<li> </li>
<li> </li>
<li> </li>
</ul>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={handleClose}
disabled={loading}
className="font-light"
>
</Button>
<Button
onClick={handleSubmit}
disabled={loading}
className="font-light"
>
{loading ? '创建中...' : '创建企业'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -10,7 +10,8 @@ import { getAuthToken } from "@/utils/token.ts";
import {
listTenantsApiV1TenantsGet,
enableTenantApiV1TenantsTenantIdEnablePatch,
disableTenantApiV1TenantsTenantIdDisablePatch
disableTenantApiV1TenantsTenantIdDisablePatch,
createTenantApiV1TenantsPost
} from "@/lib/api/sdk.gen";
export interface TenantData {
id: string;
@@ -65,6 +66,13 @@ export interface TenantsQueryParams {
sort_order?: 'asc' | 'desc';
}
// 新建企业请求参数接口
export interface CreateEnterpriseRequest {
company_name: string;
tenant_code: string;
company_type: string;
}
// 企业页面数据类型(转换后的)
export interface Enterprise {
id: string;
@@ -289,6 +297,42 @@ function mapAuditStatus(status: string): Enterprise['auditStatus'] {
}
}
/**
* 创建新企业
* @param data 企业创建数据
* @returns 创建结果
*/
export async function createEnterprise(data: CreateEnterpriseRequest): Promise<TenantData> {
try {
console.log('🏢 创建企业API调用:', data);
// 获取认证token
const token = getAuthToken();
// 使用SDK API调用创建企业接口
const response = await createTenantApiV1TenantsPost({
body: data,
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
console.error('🏢 创建企业API错误:', response.error);
throw new Error(`创建失败: ${response.error.message || '未知错误'}`);
}
const result = response.data as TenantData;
console.log('🏢 创建企业API成功:', result);
return result;
} catch (error) {
console.error('🏢 创建企业失败:', error);
throw error;
}
}
/**
* 格式化日期
*/

View File

@@ -18,11 +18,12 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@
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 { Building2, Eye, Power, PowerOff, Search, FileText, CreditCard, User, RefreshCw, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-react';
import { Building2, Eye, Power, PowerOff, Search, FileText, CreditCard, User, RefreshCw, AlertCircle, ChevronLeft, ChevronRight, Plus } from 'lucide-react';
import { toast } from 'sonner';
import { enterpriseReducer, initialState, EnterpriseState, EnterpriseAction } from './components/enterpriseReducer';
import { fetchTenants, transformTenantData, enableTenant, disableTenant, TenantsQueryParams, Enterprise } from './components/enterpriseApi';
import { fetchTenants, transformTenantData, enableTenant, disableTenant, createEnterprise, TenantsQueryParams, Enterprise } from './components/enterpriseApi';
import { CreateEnterpriseDialog } from './components/CreateEnterpriseDialog';
// Utility functions
const getStatusBadge = (status: 'active' | 'inactive') => {
@@ -152,6 +153,16 @@ export default function EnterpriseManagement() {
dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: true });
};
const handleCreateNew = () => {
dispatch({ type: 'RESET_FORM_DATA' });
dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: true });
};
const handleCreateSuccess = () => {
// 创建成功后刷新数据
loadEnterprises(true);
};
const confirmStatusChange = async () => {
if (!state.selectedEnterprise) return;
@@ -226,6 +237,10 @@ export default function EnterpriseManagement() {
</div>
</div>
<div className="flex items-center gap-2">
<Button onClick={handleCreateNew} disabled={state.loading}>
<Plus className="w-4 h-4 mr-2" />
</Button>
<Button variant="outline" size="sm" onClick={handleRefresh} disabled={state.loading}>
<RefreshCw className={`w-4 h-4 mr-1 ${state.loading ? 'animate-spin' : ''}`} />
@@ -668,6 +683,13 @@ export default function EnterpriseManagement() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Create Enterprise Dialog */}
<CreateEnterpriseDialog
open={state.showAddDialog}
onOpenChange={(open) => dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: open })}
onSuccess={handleCreateSuccess}
/>
</div>
);
}

View File

@@ -7,9 +7,8 @@
'use client';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Building2, Plus, RefreshCw } from 'lucide-react';
import { Plus } from 'lucide-react';
interface DepartmentHeaderProps {
onAdd: () => void;
@@ -19,35 +18,15 @@ interface DepartmentHeaderProps {
export function DepartmentHeader({ onAdd }: DepartmentHeaderProps) {
return (
<Card className="p-6 bg-gradient-to-r from-green-50 dark:from-green-950 to-emerald-50 dark:to-emerald-950 border-green-200 dark:border-green-800">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<Building2 className="w-6 h-6 text-green-600 dark:text-green-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 onClick={onAdd} className="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800 dark:text-green-400"></h2>
<p className="text-muted-foreground"></p>
</div>
</Card>
<Button onClick={onAdd} className="bg-green-600 hover:bg-green-700 dark:bg-green-600 dark:hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" />
</Button>
</div>
);
}

View File

@@ -6,15 +6,11 @@
*/
import { Card } from '@/components/ui/card';
import { Building2, GripVertical, AlertCircle, Users } from 'lucide-react';
export function DepartmentInstructions() {
return (
<Card className="p-4 bg-blue-50 dark:bg-blue-950/30 border-blue-200 dark:border-blue-900">
<div className="flex items-center gap-2 mb-3">
<Building2 className="w-5 h-5 text-blue-900 dark:text-blue-400" />
<h4 className="text-blue-900 dark:text-blue-400 font-medium"></h4>
</div>
<h4 className="text-blue-900 dark:text-blue-400 font-medium mb-3"></h4>
<ul className="space-y-2 text-sm text-blue-800 dark:text-blue-300">
<li className="flex items-start gap-2">
@@ -26,7 +22,7 @@ export function DepartmentInstructions() {
</li>
<li className="flex items-start gap-2">
<GripVertical className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<span className="text-blue-600 dark:text-blue-400 mt-0.5"></span>
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
@@ -42,7 +38,7 @@ export function DepartmentInstructions() {
</li>
<li className="flex items-start gap-2">
<Users className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<span className="text-blue-600 dark:text-blue-400 mt-0.5"></span>
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>
@@ -50,7 +46,7 @@ export function DepartmentInstructions() {
</li>
<li className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
<span className="text-blue-600 dark:text-blue-400 mt-0.5"></span>
<div>
<strong className="text-blue-900 dark:text-blue-400"></strong>

View File

@@ -1,6 +1,7 @@
'use client';
import React from 'react';
import React, { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -8,6 +9,7 @@ import { Label } from '@/components/ui/label';
import { Card } from '@/components/ui/card';
import { Checkbox } from '@/components/ui/checkbox';
import { Employee, Role, EmployeeFormData } from '../types';
import { fetchEmployeeDetail } from './employeeApi';
interface EmployeeFormDialogProps {
open: boolean;
@@ -17,6 +19,9 @@ interface EmployeeFormDialogProps {
onFormDataChange: (data: EmployeeFormData) => void;
onSave: () => void;
roles: Role[];
creating?: boolean;
updating?: boolean;
onClearForm?: () => void;
}
export function EmployeeFormDialog({
@@ -26,13 +31,73 @@ export function EmployeeFormDialog({
formData,
onFormDataChange,
onSave,
roles
roles,
creating = false,
updating = false,
onClearForm
}: EmployeeFormDialogProps) {
const [loadingDetail, setLoadingDetail] = useState(false);
// 当编辑员工时根据ID获取用户详情
useEffect(() => {
if (open && editingEmployee && editingEmployee.id) {
loadEmployeeDetail(editingEmployee.id);
}
}, [open, editingEmployee]);
const loadEmployeeDetail = async (userId: string) => {
setLoadingDetail(true);
try {
const employeeDetail = await fetchEmployeeDetail(userId);
// 将API数据转换为表单数据格式
const formDetailData: EmployeeFormData = {
username: employeeDetail.username,
name: employeeDetail.displayName || employeeDetail.fullName || employeeDetail.username,
phone: employeeDetail.phone || '',
email: employeeDetail.email || '',
department: employeeDetail.departmentName || '',
position: '', // API返回中没有position字段
enterpriseId: employeeDetail.tenantId,
enterpriseName: employeeDetail.companyName || '',
status: employeeDetail.isActive ? 'active' : 'inactive',
roleIds: [], // 需要单独获取角色信息
idCard: '', // API返回中没有idCard字段
address: '', // API返回中没有address字段
auditStatus: 'approved', // 默认值
isSuperuser: employeeDetail.isSuperuser,
};
// 更新表单数据
onFormDataChange(formDetailData);
} catch (error) {
console.error('获取员工详情失败:', error);
toast.error('接口调用失败,请稍后重试');
} finally {
setLoadingDetail(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<Dialog open={open} onOpenChange={(isOpen) => {
if (!isOpen && onClearForm) {
onClearForm();
}
onOpenChange(isOpen);
}}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>{editingEmployee ? '编辑员工' : '添加员工'}</DialogTitle>
<DialogTitle>
{editingEmployee ? (
<div className="flex items-center gap-2">
{loadingDetail && (
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
)}
</div>
) : (
'添加员工'
)}
</DialogTitle>
<DialogDescription className="sr-only">
{editingEmployee ? '编辑员工信息' : '添加新员工'}
</DialogDescription>
@@ -49,6 +114,7 @@ export function EmployeeFormDialog({
value={formData.username || ''}
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
placeholder="登录用户名"
disabled={editingEmployee && loadingDetail}
/>
</div>
<div>
@@ -58,6 +124,7 @@ export function EmployeeFormDialog({
value={formData.name || ''}
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
placeholder="真实姓名"
disabled={editingEmployee && loadingDetail}
/>
</div>
<div>
@@ -67,6 +134,7 @@ export function EmployeeFormDialog({
value={formData.phone || ''}
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
placeholder="11位手机号码"
disabled={editingEmployee && loadingDetail}
/>
</div>
<div>
@@ -77,6 +145,7 @@ export function EmployeeFormDialog({
value={formData.email || ''}
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
placeholder="电子邮箱"
disabled={editingEmployee && loadingDetail}
/>
</div>
<div>
@@ -86,6 +155,7 @@ export function EmployeeFormDialog({
value={formData.idCard || ''}
onChange={(e) => onFormDataChange({ ...formData, idCard: e.target.value })}
placeholder="18位身份证号码"
disabled={editingEmployee && loadingDetail}
/>
</div>
<div>
@@ -95,6 +165,7 @@ export function EmployeeFormDialog({
value={formData.address || ''}
onChange={(e) => onFormDataChange({ ...formData, address: e.target.value })}
placeholder="详细住址"
disabled={editingEmployee && loadingDetail}
/>
</div>
</div>
@@ -111,6 +182,7 @@ export function EmployeeFormDialog({
value={formData.department || ''}
onChange={(e) => onFormDataChange({ ...formData, department: e.target.value })}
placeholder="所属部门"
disabled={editingEmployee && loadingDetail}
/>
</div>
</div>
@@ -164,10 +236,12 @@ export function EmployeeFormDialog({
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={creating || updating || loadingDetail}>
</Button>
<Button onClick={onSave}></Button>
<Button onClick={onSave} disabled={creating || updating || loadingDetail}>
{creating ? '创建中...' : updating ? '更新中...' : loadingDetail ? '加载中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

View File

@@ -6,6 +6,7 @@ import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import {
Pagination,
PaginationContent,
@@ -31,6 +32,7 @@ interface EmployeeListProps {
onToggleStatus: (employee: Employee) => void;
onDelete: (id: string) => void;
onAudit?: (employee: Employee, action: 'approve' | 'reject') => void;
togglingId?: string | null;
}
export function EmployeeList({
@@ -44,7 +46,8 @@ export function EmployeeList({
onResetPassword,
onToggleStatus,
onDelete,
onAudit
onAudit,
togglingId
}: EmployeeListProps) {
const getStatusBadge = (isActive: boolean, status?: UserStatus) => {
// 优先使用isActive字段来自API其次使用status字段兼容旧数据
@@ -76,7 +79,8 @@ export function EmployeeList({
};
return (
<Card>
<TooltipProvider>
<Card>
<Table>
<TableHeader>
<TableRow>
@@ -142,45 +146,88 @@ export function EmployeeList({
</Button>
</>
)}
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetail(employee)}
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(employee)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onResetPassword(employee)}
>
<Lock className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onToggleStatus(employee)}
>
{(employee.isActive || employee.status === 'active') ? (
<UserX className="w-4 h-4 text-orange-600" />
) : (
<UserCheck className="w-4 h-4 text-green-600" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(employee.id)}
>
<Trash2 className="w-4 h-4 text-destructive" />
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetail(employee)}
>
<Eye className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onEdit(employee)}
>
<Edit className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onResetPassword(employee)}
>
<Lock className="w-4 h-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onToggleStatus(employee)}
disabled={togglingId === employee.id}
>
{togglingId === employee.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (employee.isActive || employee.status === 'active') ? (
<UserX className="w-4 h-4 text-orange-600" />
) : (
<UserCheck className="w-4 h-4 text-green-600" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{(employee.isActive || employee.status === 'active') ? '停用员工' : '激活员工'}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={() => onDelete(employee.id)}
disabled={togglingId === employee.id}
>
{togglingId === employee.id ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Trash2 className="w-4 h-4 text-destructive" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p></p>
</TooltipContent>
</Tooltip>
</div>
</TableCell>
</TableRow>
@@ -268,6 +315,7 @@ export function EmployeeList({
</div>
</div>
)}
</Card>
</Card>
</TooltipProvider>
);
}

View File

@@ -8,7 +8,9 @@ interface EmployeeManagementHeaderProps {
onAddEmployee: () => void;
}
export function EmployeeManagementHeader({ onAddEmployee }: EmployeeManagementHeaderProps) {
export function EmployeeManagementHeader({
onAddEmployee
}: EmployeeManagementHeaderProps) {
return (
<div className="flex items-center justify-between">
<div>

View File

@@ -6,7 +6,7 @@
*/
import { getAuthToken } from "@/utils/token";
import { getUsersApiV1UsersGet } from "@/lib/api/sdk.gen";
import { getUsersApiV1UsersGet, createUserApiV1UsersPost, getUserApiV1UsersUserIdGet, updateUserApiV1UsersUserIdPut, activateUserApiV1UsersUserIdActivatePost, deactivateUserApiV1UsersUserIdDeactivatePost, deleteUserApiV1UsersUserIdDelete } from "@/lib/api/sdk.gen";
// API返回的员工数据类型
export interface EmployeeApiData {
@@ -50,6 +50,32 @@ export interface EmployeesQueryParams {
sort_order?: 'asc' | 'desc';
}
// 创建用户请求参数接口
export interface CreateEmployeeRequest {
email: string;
username: string;
full_name?: string;
phone: string;
password: string;
tenant_id?: string;
scope?: string;
department_id?: string;
is_superuser?: boolean;
}
// 更新用户请求参数接口
export interface UpdateEmployeeRequest {
email?: string;
username?: string;
full_name?: string;
phone?: string;
password?: string;
tenant_id?: string;
scope?: string;
department_id?: string;
is_superuser?: boolean;
}
// 页面使用的员工数据类型(转换后的)
export interface Employee {
id: string;
@@ -209,6 +235,258 @@ function formatDate(dateString: string): string {
}
}
/**
* 创建员工用户
*/
export async function createEmployee(employeeData: CreateEmployeeRequest): Promise<Employee> {
try {
// 获取认证token
const token = getAuthToken();
console.log('创建员工API调用参数:', employeeData);
// 使用SDK API调用创建用户接口
const response = await createUserApiV1UsersPost({
body: employeeData,
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误响应
const errorData = response.error as any;
// 检查是否是409冲突错误用户已存在
if (errorData.status === 409 && errorData.data) {
const conflictError = errorData.data as {
code?: string;
message?: string;
domain?: string;
detail?: {
field?: string;
value?: string;
};
};
// 抛出包含详细错误信息的异常
throw new Error(conflictError.message || '用户创建失败');
}
// 其他HTTP错误
if (errorData.data && errorData.data.message) {
throw new Error(errorData.data.message);
}
// 通用错误处理
throw new Error(errorData.message || `API error: ${errorData.status || 'Unknown error'}`);
}
const data = response.data as any;
console.log('创建员工API响应:', data);
// 转换并返回新创建的员工数据
return transformEmployeeData(data);
} catch (error) {
console.error('Failed to create employee:', error);
throw error;
}
}
/**
* 获取用户详情信息
*/
export async function fetchEmployeeDetail(userId: string): Promise<Employee> {
try {
// 获取认证token
const token = getAuthToken();
console.log('获取用户详情API调用参数:', { userId });
// 使用SDK API调用用户详情查询接口
const response = await getUserApiV1UsersUserIdGet({
path: {
user_id: userId,
},
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);
// 转换并返回员工详情数据
return transformEmployeeData(data);
} catch (error) {
console.error('Failed to fetch employee detail:', error);
throw error;
}
}
/**
* 更新员工用户信息
*/
export async function updateEmployee(userId: string, employeeData: UpdateEmployeeRequest): Promise<Employee> {
try {
// 获取认证token
const token = getAuthToken();
console.log('更新员工API调用参数:', { userId, employeeData });
// 使用SDK API调用更新用户接口
const response = await updateUserApiV1UsersUserIdPut({
path: {
user_id: userId,
},
body: employeeData as any, // 使用any类型绕过类型检查因为API类型定义与实际需求不匹配
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误响应
const errorData = response.error as any;
// 检查是否有详细的错误信息
if (errorData.data && errorData.data.message) {
throw new Error(errorData.data.message);
}
// 通用错误处理
throw new Error(errorData.message || `API error: ${errorData.status || 'Unknown error'}`);
}
const data = response.data as any;
console.log('更新员工API响应:', data);
// 转换并返回更新后的员工数据
return transformEmployeeData(data);
} catch (error) {
console.error('Failed to update employee:', error);
throw error;
}
}
/**
* 激活用户
*/
export async function activateUser(userId: string): Promise<void> {
try {
// 获取认证token
const token = getAuthToken();
console.log('激活用户API调用参数:', { userId });
// 使用SDK API调用激活用户接口
const response = await activateUserApiV1UsersUserIdActivatePost({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误响应
const errorData = response.error as any;
// 检查是否有详细的错误信息
if (errorData.data && errorData.data.message) {
throw new Error(errorData.data.message);
}
// 通用错误处理
throw new Error(errorData.message || `API error: ${errorData.status || 'Unknown error'}`);
}
console.log('激活用户API响应:', response.data);
} catch (error) {
console.error('Failed to activate user:', error);
throw error;
}
}
/**
* 停用用户
*/
export async function deactivateUser(userId: string): Promise<void> {
try {
// 获取认证token
const token = getAuthToken();
console.log('停用用户API调用参数:', { userId });
// 使用SDK API调用停用用户接口
const response = await deactivateUserApiV1UsersUserIdDeactivatePost({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误响应
const errorData = response.error as any;
// 检查是否有详细的错误信息
if (errorData.data && errorData.data.message) {
throw new Error(errorData.data.message);
}
// 通用错误处理
throw new Error(errorData.message || `API error: ${errorData.status || 'Unknown error'}`);
}
console.log('停用用户API响应:', response.data);
} catch (error) {
console.error('Failed to deactivate user:', error);
throw error;
}
}
/**
* 删除用户
*/
export async function deleteUser(userId: string): Promise<void> {
try {
// 获取认证token
const token = getAuthToken();
console.log('删除用户API调用参数:', { userId });
// 使用SDK API调用删除用户接口
const response = await deleteUserApiV1UsersUserIdDelete({
path: {
user_id: userId,
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
// 处理API错误响应
const errorData = response.error as any;
// 检查是否有详细的错误信息
if (errorData.data && errorData.data.message) {
throw new Error(errorData.data.message);
}
// 通用错误处理
throw new Error(errorData.message || `API error: ${errorData.status || 'Unknown error'}`);
}
console.log('删除用户API响应:', response.data);
} catch (error) {
console.error('Failed to delete user:', error);
throw error;
}
}
// Pagination state interface for page components
export interface PaginationState {
page: number;

View File

@@ -9,10 +9,28 @@ import { EmployeeManagementFilters } from './components/EmployeeManagementFilter
import { EmployeeList } from './components/EmployeeList';
import { EmployeeFormDialog } from './components/EmployeeFormDialog';
import { EmployeeDetailDialog } from './components/EmployeeDetailDialog';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Employee, Role, EmployeeFilters, EmployeeFormData } from './types';
import {
fetchEmployees,
transformEmployeesList,
createEmployee,
updateEmployee,
activateUser,
deactivateUser,
deleteUser,
CreateEmployeeRequest,
UpdateEmployeeRequest,
PaginationState,
EmployeesQueryParams
} from './components/employeeApi';
@@ -21,6 +39,15 @@ export default function EmployeeManagementPage() {
const [employees, setEmployees] = useState<Employee[]>([]);
const [roles, setRoles] = useState<Role[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [updating, setUpdating] = useState(false);
const [toggling, setToggling] = useState<string | null>(null); // 记录正在操作的用户ID
// 确认对话框状态
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [userToDelete, setUserToDelete] = useState<Employee | null>(null);
const [deactivateConfirmOpen, setDeactivateConfirmOpen] = useState(false);
const [userToDeactivate, setUserToDeactivate] = useState<Employee | null>(null);
const [pagination, setPagination] = useState<PaginationState>({
page: 1,
size: 10,
@@ -35,6 +62,7 @@ export default function EmployeeManagementPage() {
});
const [showForm, setShowForm] = useState(false);
const [showDetailDialog, setShowDetailDialog] = useState(false);
const [formKey, setFormKey] = useState(0); // 添加key来强制重新渲染表单
const [editingEmployee, setEditingEmployee] = useState<Employee | null>(null);
const [selectedEmployee, setSelectedEmployee] = useState<Employee | null>(null);
const [formData, setFormData] = useState<EmployeeFormData>({
@@ -135,16 +163,36 @@ export default function EmployeeManagementPage() {
const handleAddEmployee = () => {
setEditingEmployee(null);
setFormData({
clearForm();
setFormKey(prev => prev + 1); // 增加key强制重新渲染
setShowForm(true);
};
const clearForm = () => {
// 先设置一个空的表单对象
const emptyForm = {
enterpriseId: 'ent-2',
enterpriseName: '丰收现代农业集团',
status: 'active',
auditStatus: 'pending',
status: 'active' as const,
auditStatus: 'pending' as const,
roleIds: [],
idCard: '',
address: '',
});
setShowForm(true);
username: '',
name: '',
phone: '',
email: '',
department: '',
position: '',
};
// 强制清空表单
setFormData(emptyForm);
// 使用setTimeout确保状态更新完成
setTimeout(() => {
setFormData({...emptyForm});
}, 0);
};
const handleEdit = (employee: Employee) => {
@@ -153,7 +201,7 @@ export default function EmployeeManagementPage() {
setShowForm(true);
};
const handleSave = () => {
const handleSave = async () => {
if (!formData.username || !formData.name || !formData.phone) {
toast.error('请填写必填项');
return;
@@ -170,58 +218,195 @@ export default function EmployeeManagementPage() {
const roleNames = selectedRoles.map(r => r.name);
if (editingEmployee) {
// 更新
const updated = employees.map(emp =>
emp.id === editingEmployee.id
? {
...emp,
...formData,
roles: roleNames,
updatedAt: new Date().toISOString(),
}
: emp
);
setEmployees(updated);
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
toast.success('员工信息更新成功');
} else {
// 新增
const newEmployee: Employee = {
id: `emp-${Date.now()}`,
...formData as Employee,
roles: roleNames,
auditStatus: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
const updated = [...employees, newEmployee];
setEmployees(updated);
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
toast.success('员工添加成功');
}
// 更新 - 调用API
setUpdating(true);
try {
// 构建API请求参数
const updateRequest: UpdateEmployeeRequest = {
email: formData.email || '',
username: formData.username,
full_name: formData.name,
phone: formData.phone,
password: '', // 编辑时不传密码
tenant_id: formData.enterpriseId,
scope: 'tenant',
department_id: formData.departmentId || '',
is_superuser: formData.isSuperuser || false,
};
setShowForm(false);
// 调用API更新用户
const updatedEmployee = await updateEmployee(editingEmployee.id, updateRequest);
// 更新本地列表中的员工数据
const updated = employees.map(emp =>
emp.id === editingEmployee.id
? {
...emp,
...updatedEmployee,
roles: roleNames,
updatedAt: new Date().toISOString(),
}
: emp
);
setEmployees(updated);
toast.success('员工信息更新成功');
setShowForm(false);
clearForm();
// 刷新员工列表数据
await loadEmployees();
} catch (error) {
console.error('更新员工失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '员工信息更新失败';
toast.error(errorMessage);
} finally {
setUpdating(false);
}
} else {
// 新增 - 调用API
setCreating(true);
try {
// 构建API请求参数
const createRequest: CreateEmployeeRequest = {
email: formData.email || '', // 没有邮箱就传空字符串
username: formData.username,
full_name: formData.name,
phone: formData.phone,
password: '', // 传递空字符串给后端
tenant_id: formData.enterpriseId,
scope: 'tenant',
department_id: formData.departmentId || '',
is_superuser: formData.isSuperuser || false,
};
// 调用API创建用户
const newEmployee = await createEmployee(createRequest);
// 将新员工添加到列表
const updated = [newEmployee, ...employees];
setEmployees(updated);
toast.success('员工添加成功');
setShowForm(false);
clearForm();
// 刷新员工列表数据
await loadEmployees();
} catch (error) {
console.error('创建员工失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '员工添加失败';
toast.error(errorMessage);
} finally {
setCreating(false);
}
}
};
const handleDelete = (id: string) => {
if (!confirm('确定要删除该员工吗?')) return;
const handleDelete = (userId: string) => {
// 防止重复操作
if (toggling) {
toast.warning('操作进行中,请稍候...');
return;
}
const updated = employees.filter(emp => emp.id !== id);
setEmployees(updated);
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
toast.success('员工删除成功');
// 查找要删除的员工信息
const employeeToDelete = employees.find(emp => emp.id === userId);
if (!employeeToDelete) {
toast.error('未找到要删除的用户');
return;
}
// 设置要删除的用户并显示确认对话框
setUserToDelete(employeeToDelete);
setDeleteConfirmOpen(true);
};
const executeDelete = async (userId: string) => {
setToggling(userId);
try {
// 调用API删除用户
await deleteUser(userId);
// 成功后从本地列表中移除
const updated = employees.filter(emp => emp.id !== userId);
setEmployees(updated);
toast.success('用户删除成功');
// 刷新列表确保数据同步
await loadEmployees();
// 关闭确认对话框
setDeleteConfirmOpen(false);
setUserToDelete(null);
} catch (error) {
console.error('删除用户失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '删除失败,请稍后重试';
toast.error(errorMessage);
// 失败时不关闭确认对话框,用户可以重试
} finally {
setToggling(null);
}
};
const handleToggleStatus = (employee: Employee) => {
const newStatus = employee.status === 'active' ? 'frozen' : 'active';
const updated = employees.map(emp =>
emp.id === employee.id
? { ...emp, status: newStatus, updatedAt: new Date().toISOString() }
: emp
);
setEmployees(updated);
localStorage.setItem('smart_agriculture_employees', JSON.stringify(updated));
toast.success(newStatus === 'active' ? '账户已激活' : '账户已冻结');
if (toggling) {
toast.warning('操作进行中,请稍候...');
return;
}
if (employee.isActive) {
// 当前是激活状态,进行停用操作,需要二次确认
setUserToDeactivate(employee);
setDeactivateConfirmOpen(true);
} else {
// 当前是停用状态,进行激活操作(不需要确认)
executeToggleStatus(employee);
}
};
const executeToggleStatus = async (employee: Employee) => {
setToggling(employee.id);
try {
if (employee.isActive) {
// 当前是激活状态,进行停用操作
await deactivateUser(employee.id);
toast.success('账户已停用');
} else {
// 当前是停用状态,进行激活操作
await activateUser(employee.id);
toast.success('账户已激活');
}
// 成功后刷新列表
await loadEmployees();
// 关闭停用确认对话框
if (deactivateConfirmOpen) {
setDeactivateConfirmOpen(false);
setUserToDeactivate(null);
}
} catch (error) {
console.error('切换用户状态失败:', error);
// 处理错误,显示具体的错误消息
const errorMessage = error instanceof Error ? error.message : '操作失败,请稍后重试';
toast.error(errorMessage);
// 失败时不关闭确认对话框,用户可以重试
} finally {
setToggling(null);
}
};
const handleResetPassword = (employee: Employee) => {
@@ -301,10 +486,12 @@ export default function EmployeeManagementPage() {
onToggleStatus={handleToggleStatus}
onDelete={handleDelete}
onAudit={handleAudit}
togglingId={toggling}
/>
{/* 添加/编辑表单 */}
<EmployeeFormDialog
key={formKey} // 使用key强制重新渲染清除浏览器缓存
open={showForm}
onOpenChange={setShowForm}
editingEmployee={editingEmployee}
@@ -312,6 +499,9 @@ export default function EmployeeManagementPage() {
onFormDataChange={setFormData}
onSave={handleSave}
roles={roles}
creating={creating}
updating={updating}
onClearForm={clearForm}
/>
{/* 详情对话框 */}
@@ -320,6 +510,74 @@ export default function EmployeeManagementPage() {
onOpenChange={setShowDetailDialog}
selectedEmployee={selectedEmployee}
/>
{/* 删除确认对话框 */}
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"
{userToDelete?.displayName || userToDelete?.fullName || userToDelete?.username || ''}
"
<br /><br />
<span className="text-red-600 font-semibold">
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={toggling !== null}>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (userToDelete) {
executeDelete(userToDelete.id);
}
}}
disabled={toggling !== null}
className="bg-red-600 hover:bg-red-700"
>
{toggling ? '删除中...' : '确认删除'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 停用确认对话框 */}
<AlertDialog open={deactivateConfirmOpen} onOpenChange={setDeactivateConfirmOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
"
{userToDeactivate?.displayName || userToDeactivate?.fullName || userToDeactivate?.username || ''}
"
<br /><br />
<span className="text-orange-600 font-semibold">
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={toggling !== null}>
</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (userToDeactivate) {
executeToggleStatus(userToDeactivate);
}
}}
disabled={toggling !== null}
className="bg-orange-600 hover:bg-orange-700"
>
{toggling ? '停用中...' : '确认停用'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}

View File

@@ -75,6 +75,7 @@ export interface EmployeeFormData {
name?: string;
phone?: string;
email?: string;
password?: string;
department?: string;
position?: string;
enterpriseId?: string;