|
@@ -0,0 +1,754 @@
|
|
|
|
|
+import React, { useState, useMemo } from 'react';
|
|
|
|
|
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
|
|
+import { format } from 'date-fns';
|
|
|
|
|
+import {
|
|
|
|
|
+ CreditCard,
|
|
|
|
|
+ DollarSign,
|
|
|
|
|
+ TrendingUp,
|
|
|
|
|
+ TrendingDown,
|
|
|
|
|
+ History,
|
|
|
|
|
+ RefreshCw,
|
|
|
|
|
+ CheckCircle,
|
|
|
|
|
+ Edit,
|
|
|
|
|
+ Settings,
|
|
|
|
|
+ AlertCircle
|
|
|
|
|
+} from 'lucide-react';
|
|
|
|
|
+import { creditBalanceClient } from '../api/creditBalanceClient';
|
|
|
|
|
+import type {
|
|
|
|
|
+ CreditBalanceDialogProps,
|
|
|
|
|
+ CreditBalanceLogsQueryParams
|
|
|
|
|
+} from '../types/creditBalance';
|
|
|
|
|
+import type { InferRequestType } from 'hono/client';
|
|
|
|
|
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
|
|
|
|
|
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
|
|
|
|
|
+import {
|
|
|
|
|
+ Dialog,
|
|
|
|
|
+ DialogContent,
|
|
|
|
|
+ DialogDescription,
|
|
|
|
|
+ DialogFooter,
|
|
|
|
|
+ DialogHeader,
|
|
|
|
|
+ DialogTitle
|
|
|
|
|
+} from '@d8d/shared-ui-components/components/ui/dialog';
|
|
|
|
|
+import {
|
|
|
|
|
+ Card,
|
|
|
|
|
+ CardContent,
|
|
|
|
|
+ CardDescription,
|
|
|
|
|
+ CardHeader,
|
|
|
|
|
+ CardTitle
|
|
|
|
|
+} from '@d8d/shared-ui-components/components/ui/card';
|
|
|
|
|
+import {
|
|
|
|
|
+ Table,
|
|
|
|
|
+ TableBody,
|
|
|
|
|
+ TableCell,
|
|
|
|
|
+ TableHead,
|
|
|
|
|
+ TableHeader,
|
|
|
|
|
+ TableRow
|
|
|
|
|
+} from '@d8d/shared-ui-components/components/ui/table';
|
|
|
|
|
+import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
|
|
|
|
|
+import {
|
|
|
|
|
+ Form,
|
|
|
|
|
+ FormControl,
|
|
|
|
|
+ FormDescription,
|
|
|
|
|
+ FormField,
|
|
|
|
|
+ FormItem,
|
|
|
|
|
+ FormLabel,
|
|
|
|
|
+ FormMessage
|
|
|
|
|
+} from '@d8d/shared-ui-components/components/ui/form';
|
|
|
|
|
+import { useForm } from 'react-hook-form';
|
|
|
|
|
+import { zodResolver } from '@hookform/resolvers/zod';
|
|
|
|
|
+import { toast } from 'sonner';
|
|
|
|
|
+import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
|
|
|
|
|
+import {
|
|
|
|
|
+ Tabs,
|
|
|
|
|
+ TabsContent,
|
|
|
|
|
+ TabsList,
|
|
|
|
|
+ TabsTrigger
|
|
|
|
|
+} from '@d8d/shared-ui-components/components/ui/tabs';
|
|
|
|
|
+import {
|
|
|
|
|
+ Alert,
|
|
|
|
|
+ AlertDescription,
|
|
|
|
|
+ AlertTitle
|
|
|
|
|
+} from '@d8d/shared-ui-components/components/ui/alert';
|
|
|
|
|
+import { SetLimitDto, AdjustLimitDto, CheckoutDto } from '@d8d/credit-balance-module-mt/schemas';
|
|
|
|
|
+
|
|
|
|
|
+// 使用RPC方式提取类型
|
|
|
|
|
+type SetLimitRequest = InferRequestType<typeof creditBalanceClient[':userId']['$put']>['json'];
|
|
|
|
|
+type AdjustLimitRequest = InferRequestType<typeof creditBalanceClient[':userId']['adjust']['$post']>['json'];
|
|
|
|
|
+type CheckoutRequest = InferRequestType<typeof creditBalanceClient['checkout']['$post']>['json'];
|
|
|
|
|
+
|
|
|
|
|
+// 直接使用后端定义的 schema
|
|
|
|
|
+const setLimitFormSchema = SetLimitDto;
|
|
|
|
|
+const adjustLimitFormSchema = AdjustLimitDto;
|
|
|
|
|
+const checkoutFormSchema = CheckoutDto;
|
|
|
|
|
+
|
|
|
|
|
+type SetLimitFormData = SetLimitRequest;
|
|
|
|
|
+type AdjustLimitFormData = AdjustLimitRequest;
|
|
|
|
|
+type CheckoutFormData = CheckoutRequest;
|
|
|
|
|
+
|
|
|
|
|
+export const CreditBalanceDialog: React.FC<CreditBalanceDialogProps> = ({
|
|
|
|
|
+ userId,
|
|
|
|
|
+ userName = '用户',
|
|
|
|
|
+ open,
|
|
|
|
|
+ onOpenChange,
|
|
|
|
|
+ tenantId,
|
|
|
|
|
+ title = '用户信用额度管理',
|
|
|
|
|
+ description = `管理用户 ${userName} (ID: ${userId}) 的信用额度`,
|
|
|
|
|
+ size = 'lg'
|
|
|
|
|
+}) => {
|
|
|
|
|
+ const queryClient = useQueryClient();
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState('overview');
|
|
|
|
|
+ const [logsQueryParams, setLogsQueryParams] = useState<CreditBalanceLogsQueryParams>({
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ limit: 10
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 额度查询
|
|
|
|
|
+ const { data: balanceData, isLoading: isLoadingBalance, refetch: refetchBalance } = useQuery({
|
|
|
|
|
+ queryKey: ['credit-balance', userId, tenantId],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const res = await creditBalanceClient[':userId'].$get({
|
|
|
|
|
+ param: { userId: userId.toString() }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.status !== 200) throw new Error('获取信用额度失败');
|
|
|
|
|
+ return await res.json();
|
|
|
|
|
+ },
|
|
|
|
|
+ enabled: open && !!userId,
|
|
|
|
|
+ staleTime: 5 * 60 * 1000,
|
|
|
|
|
+ gcTime: 10 * 60 * 1000,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 额度变更记录查询
|
|
|
|
|
+ const { data: logsData, isLoading: isLoadingLogs } = useQuery({
|
|
|
|
|
+ queryKey: ['credit-balance-logs', userId, logsQueryParams, tenantId],
|
|
|
|
|
+ queryFn: async () => {
|
|
|
|
|
+ const res = await creditBalanceClient[':userId'].logs.$get({
|
|
|
|
|
+ param: { userId: userId.toString() },
|
|
|
|
|
+ query: {
|
|
|
|
|
+ page: logsQueryParams.page || 1,
|
|
|
|
|
+ pageSize: logsQueryParams.limit || 10
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.status !== 200) throw new Error('获取额度变更记录失败');
|
|
|
|
|
+ return await res.json();
|
|
|
|
|
+ },
|
|
|
|
|
+ enabled: open && !!userId && activeTab === 'logs',
|
|
|
|
|
+ staleTime: 5 * 60 * 1000,
|
|
|
|
|
+ gcTime: 10 * 60 * 1000,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置额度表单
|
|
|
|
|
+ const setLimitForm = useForm<SetLimitFormData>({
|
|
|
|
|
+ resolver: zodResolver(setLimitFormSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ totalLimit: 0,
|
|
|
|
|
+ remark: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 调整额度表单
|
|
|
|
|
+ const adjustLimitForm = useForm<AdjustLimitFormData>({
|
|
|
|
|
+ resolver: zodResolver(adjustLimitFormSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ adjustAmount: 0,
|
|
|
|
|
+ remark: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 结账恢复额度表单
|
|
|
|
|
+ const checkoutForm = useForm<CheckoutFormData>({
|
|
|
|
|
+ resolver: zodResolver(checkoutFormSchema),
|
|
|
|
|
+ defaultValues: {
|
|
|
|
|
+ userId,
|
|
|
|
|
+ amount: 0,
|
|
|
|
|
+ remark: ''
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置额度mutation
|
|
|
|
|
+ const setLimitMutation = useMutation({
|
|
|
|
|
+ mutationFn: async (data: SetLimitFormData) => {
|
|
|
|
|
+ const res = await creditBalanceClient[':userId'].$put({
|
|
|
|
|
+ param: { userId: userId.toString() },
|
|
|
|
|
+ json: data
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.status !== 200) throw new Error('设置额度失败');
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance', userId] });
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance-logs', userId] });
|
|
|
|
|
+ setLimitForm.reset();
|
|
|
|
|
+ toast.success('额度设置成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error) => {
|
|
|
|
|
+ toast.error(`设置失败: ${error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 调整额度mutation
|
|
|
|
|
+ const adjustLimitMutation = useMutation({
|
|
|
|
|
+ mutationFn: async (data: AdjustLimitFormData) => {
|
|
|
|
|
+ const res = await creditBalanceClient[':userId'].adjust.$post({
|
|
|
|
|
+ param: { userId: userId.toString() },
|
|
|
|
|
+ json: data
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.status !== 200) throw new Error('调整额度失败');
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance', userId] });
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance-logs', userId] });
|
|
|
|
|
+ adjustLimitForm.reset();
|
|
|
|
|
+ toast.success('额度调整成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error) => {
|
|
|
|
|
+ toast.error(`调整失败: ${error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 结账恢复额度mutation
|
|
|
|
|
+ const checkoutMutation = useMutation({
|
|
|
|
|
+ mutationFn: async (data: CheckoutFormData) => {
|
|
|
|
|
+ const res = await creditBalanceClient.checkout.$post({
|
|
|
|
|
+ json: data
|
|
|
|
|
+ });
|
|
|
|
|
+ if (res.status !== 200) throw new Error('结账恢复额度失败');
|
|
|
|
|
+ },
|
|
|
|
|
+ onSuccess: () => {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance', userId] });
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance-logs', userId] });
|
|
|
|
|
+ checkoutForm.reset({ userId, amount: 0, remark: '' });
|
|
|
|
|
+ toast.success('结账恢复额度成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ onError: (error) => {
|
|
|
|
|
+ toast.error(`恢复失败: ${error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 处理设置额度表单提交
|
|
|
|
|
+ const onSetLimitSubmit = (data: SetLimitFormData) => {
|
|
|
|
|
+ setLimitMutation.mutate(data);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理调整额度表单提交
|
|
|
|
|
+ const onAdjustLimitSubmit = (data: AdjustLimitFormData) => {
|
|
|
|
|
+ adjustLimitMutation.mutate(data);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理结账恢复额度表单提交
|
|
|
|
|
+ const onCheckoutSubmit = (data: CheckoutFormData) => {
|
|
|
|
|
+ checkoutMutation.mutate(data);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 计算欠款信息
|
|
|
|
|
+ const overdueInfo = useMemo(() => {
|
|
|
|
|
+ if (!balanceData) return null;
|
|
|
|
|
+ const balance = balanceData;
|
|
|
|
|
+ const isOverdue = balance.usedAmount > balance.totalLimit;
|
|
|
|
|
+ const overdueAmount = isOverdue ? balance.usedAmount - balance.totalLimit : 0;
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ isOverdue,
|
|
|
|
|
+ overdueAmount,
|
|
|
|
|
+ severity: isOverdue ? 'high' : 'none' as 'high' | 'medium' | 'low' | 'none'
|
|
|
|
|
+ };
|
|
|
|
|
+ }, [balanceData]);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取对话框尺寸类名
|
|
|
|
|
+ const getDialogSizeClass = () => {
|
|
|
|
|
+ switch (size) {
|
|
|
|
|
+ case 'sm': return 'sm:max-w-[500px]';
|
|
|
|
|
+ case 'md': return 'sm:max-w-[600px]';
|
|
|
|
|
+ case 'lg': return 'sm:max-w-[800px]';
|
|
|
|
|
+ case 'xl': return 'sm:max-w-[1000px]';
|
|
|
|
|
+ default: return 'sm:max-w-[800px]';
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 刷新数据
|
|
|
|
|
+ const handleRefresh = () => {
|
|
|
|
|
+ refetchBalance();
|
|
|
|
|
+ if (activeTab === 'logs') {
|
|
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['credit-balance-logs', userId] });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 处理日志分页
|
|
|
|
|
+ const handleLogsPageChange = (page: number) => {
|
|
|
|
|
+ setLogsQueryParams(prev => ({ ...prev, page }));
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (!open) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const balance = balanceData;
|
|
|
|
|
+ const logs = logsData?.data || [];
|
|
|
|
|
+ const logsPagination = logsData?.pagination;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Dialog open={open} onOpenChange={onOpenChange}>
|
|
|
|
|
+ <DialogContent className={`${getDialogSizeClass()} max-h-[90vh] overflow-y-auto`}>
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <DialogTitle className="flex items-center gap-2">
|
|
|
|
|
+ <CreditCard className="h-5 w-5" />
|
|
|
|
|
+ {title}
|
|
|
|
|
+ </DialogTitle>
|
|
|
|
|
+ <DialogDescription>{description}</DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ {/* 用户信息卡片 */}
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader className="pb-3">
|
|
|
|
|
+ <CardTitle className="text-lg flex items-center justify-between">
|
|
|
|
|
+ <span>用户信息</span>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={handleRefresh}
|
|
|
|
|
+ disabled={isLoadingBalance}
|
|
|
|
|
+ >
|
|
|
|
|
+ <RefreshCw className={`h-4 w-4 mr-2 ${isLoadingBalance ? 'animate-spin' : ''}`} />
|
|
|
|
|
+ 刷新
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ {isLoadingBalance ? (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Skeleton className="h-4 w-32" />
|
|
|
|
|
+ <Skeleton className="h-4 w-48" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : balance ? (
|
|
|
|
|
+ <div className="grid grid-cols-2 gap-4">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">用户ID</p>
|
|
|
|
|
+ <p className="font-medium">{userId}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">用户名</p>
|
|
|
|
|
+ <p className="font-medium">{userName}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">额度状态</p>
|
|
|
|
|
+ <Badge variant={balance.isEnabled ? 'default' : 'secondary'}>
|
|
|
|
|
+ {balance.isEnabled ? '启用' : '禁用'}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p className="text-sm text-muted-foreground">最后更新</p>
|
|
|
|
|
+ <p className="font-medium text-sm">
|
|
|
|
|
+ {format(new Date(balance.updatedAt), 'yyyy-MM-dd HH:mm')}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Alert variant="destructive">
|
|
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
|
|
+ <AlertTitle>数据加载失败</AlertTitle>
|
|
|
|
|
+ <AlertDescription>无法加载用户信用额度信息</AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 标签页 */}
|
|
|
|
|
+ <Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
|
|
|
+ <TabsList className="grid grid-cols-3">
|
|
|
|
|
+ <TabsTrigger value="overview">额度概览</TabsTrigger>
|
|
|
|
|
+ <TabsTrigger value="operations">额度操作</TabsTrigger>
|
|
|
|
|
+ <TabsTrigger value="logs">变更记录</TabsTrigger>
|
|
|
|
|
+ </TabsList>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 额度概览标签页 */}
|
|
|
|
|
+ <TabsContent value="overview" className="space-y-4">
|
|
|
|
|
+ {isLoadingBalance ? (
|
|
|
|
|
+ <div className="space-y-4">
|
|
|
|
|
+ <Skeleton className="h-32 w-full" />
|
|
|
|
|
+ <Skeleton className="h-24 w-full" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : balance ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {/* 额度统计卡片 */}
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader className="pb-2">
|
|
|
|
|
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
|
|
|
+ <DollarSign className="h-4 w-4" />
|
|
|
|
|
+ 总额度
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="text-2xl font-bold">
|
|
|
|
|
+ ¥{balance.totalLimit.toFixed(2)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
+ 用户可用的最大信用额度
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader className="pb-2">
|
|
|
|
|
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
|
|
|
+ <TrendingUp className="h-4 w-4" />
|
|
|
|
|
+ 已用额度
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="text-2xl font-bold">
|
|
|
|
|
+ ¥{balance.usedAmount.toFixed(2)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
+ 用户当前已使用的额度
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader className="pb-2">
|
|
|
|
|
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
|
|
|
+ <TrendingDown className="h-4 w-4" />
|
|
|
|
|
+ 可用额度
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="text-2xl font-bold">
|
|
|
|
|
+ ¥{balance.availableAmount.toFixed(2)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
+ 剩余可用的信用额度
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 欠款警告 */}
|
|
|
|
|
+ {overdueInfo?.isOverdue && (
|
|
|
|
|
+ <Alert variant="destructive">
|
|
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
|
|
+ <AlertTitle>用户存在欠款</AlertTitle>
|
|
|
|
|
+ <AlertDescription>
|
|
|
|
|
+ 用户已超出信用额度 ¥{overdueInfo.overdueAmount.toFixed(2)},请及时处理。
|
|
|
|
|
+ </AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 额度使用进度 */}
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-sm font-medium">额度使用情况</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <div className="flex justify-between text-sm">
|
|
|
|
|
+ <span>使用进度</span>
|
|
|
|
|
+ <span>{((balance.usedAmount / balance.totalLimit) * 100).toFixed(1)}%</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="h-2 bg-secondary rounded-full overflow-hidden">
|
|
|
|
|
+ <div
|
|
|
|
|
+ className="h-full bg-primary"
|
|
|
|
|
+ style={{ width: `${Math.min((balance.usedAmount / balance.totalLimit) * 100, 100)}%` }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex justify-between text-xs text-muted-foreground">
|
|
|
|
|
+ <span>0</span>
|
|
|
|
|
+ <span>¥{balance.totalLimit.toFixed(2)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Alert variant="destructive">
|
|
|
|
|
+ <AlertCircle className="h-4 w-4" />
|
|
|
|
|
+ <AlertTitle>数据加载失败</AlertTitle>
|
|
|
|
|
+ <AlertDescription>无法加载信用额度信息</AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TabsContent>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 额度操作标签页 */}
|
|
|
|
|
+ <TabsContent value="operations" className="space-y-4">
|
|
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
+ {/* 设置额度表单 */}
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
|
|
|
+ <Settings className="h-4 w-4" />
|
|
|
|
|
+ 设置额度
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ 设置用户的总信用额度
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <Form {...setLimitForm}>
|
|
|
|
|
+ <form onSubmit={setLimitForm.handleSubmit(onSetLimitSubmit)} className="space-y-4">
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={setLimitForm.control}
|
|
|
|
|
+ name="totalLimit"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>总额度</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ step="0.01"
|
|
|
|
|
+ placeholder="请输入总额度"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormDescription>
|
|
|
|
|
+ 设置用户可用的最大信用额度
|
|
|
|
|
+ </FormDescription>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={setLimitForm.control}
|
|
|
|
|
+ name="remark"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>备注</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input placeholder="请输入备注(可选)" {...field} />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ disabled={setLimitMutation.isPending}
|
|
|
|
|
+ >
|
|
|
|
|
+ {setLimitMutation.isPending ? '设置中...' : '设置额度'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 调整额度表单 */}
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
|
|
|
+ <Edit className="h-4 w-4" />
|
|
|
|
|
+ 调整额度
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ 增加或减少用户的信用额度
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <Form {...adjustLimitForm}>
|
|
|
|
|
+ <form onSubmit={adjustLimitForm.handleSubmit(onAdjustLimitSubmit)} className="space-y-4">
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={adjustLimitForm.control}
|
|
|
|
|
+ name="adjustAmount"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>调整金额</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ step="0.01"
|
|
|
|
|
+ placeholder="正数增加,负数减少"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormDescription>
|
|
|
|
|
+ 正数表示增加额度,负数表示减少额度
|
|
|
|
|
+ </FormDescription>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={adjustLimitForm.control}
|
|
|
|
|
+ name="remark"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>备注</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input placeholder="请输入调整原因(可选)" {...field} />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ disabled={adjustLimitMutation.isPending}
|
|
|
|
|
+ >
|
|
|
|
|
+ {adjustLimitMutation.isPending ? '调整中...' : '调整额度'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 结账恢复额度表单 */}
|
|
|
|
|
+ <Card>
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-sm font-medium flex items-center gap-2">
|
|
|
|
|
+ <CheckCircle className="h-4 w-4" />
|
|
|
|
|
+ 结账恢复额度
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ <CardDescription>
|
|
|
|
|
+ 手动恢复用户的信用额度(通常用于结账后)
|
|
|
|
|
+ </CardDescription>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <Form {...checkoutForm}>
|
|
|
|
|
+ <form onSubmit={checkoutForm.handleSubmit(onCheckoutSubmit)} className="space-y-4">
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={checkoutForm.control}
|
|
|
|
|
+ name="amount"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>恢复金额</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ step="0.01"
|
|
|
|
|
+ placeholder="请输入恢复金额"
|
|
|
|
|
+ {...field}
|
|
|
|
|
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormDescription>
|
|
|
|
|
+ 输入要恢复的额度金额
|
|
|
|
|
+ </FormDescription>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <FormField
|
|
|
|
|
+ control={checkoutForm.control}
|
|
|
|
|
+ name="remark"
|
|
|
|
|
+ render={({ field }) => (
|
|
|
|
|
+ <FormItem>
|
|
|
|
|
+ <FormLabel>备注</FormLabel>
|
|
|
|
|
+ <FormControl>
|
|
|
|
|
+ <Input placeholder="请输入备注(可选)" {...field} />
|
|
|
|
|
+ </FormControl>
|
|
|
|
|
+ <FormMessage />
|
|
|
|
|
+ </FormItem>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ className="w-full"
|
|
|
|
|
+ disabled={checkoutMutation.isPending}
|
|
|
|
|
+ >
|
|
|
|
|
+ {checkoutMutation.isPending ? '恢复中...' : '结账恢复额度'}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </form>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </TabsContent>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 变更记录标签页 */}
|
|
|
|
|
+ <TabsContent value="logs" className="space-y-4">
|
|
|
|
|
+ {isLoadingLogs ? (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ <Skeleton className="h-8 w-full" />
|
|
|
|
|
+ <Skeleton className="h-64 w-full" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : logs.length > 0 ? (
|
|
|
|
|
+ <>
|
|
|
|
|
+ <div className="rounded-md border">
|
|
|
|
|
+ <Table>
|
|
|
|
|
+ <TableHeader>
|
|
|
|
|
+ <TableRow>
|
|
|
|
|
+ <TableHead>时间</TableHead>
|
|
|
|
|
+ <TableHead>变更类型</TableHead>
|
|
|
|
|
+ <TableHead>变更金额</TableHead>
|
|
|
|
|
+ <TableHead>变更前总额</TableHead>
|
|
|
|
|
+ <TableHead>变更后总额</TableHead>
|
|
|
|
|
+ <TableHead>备注</TableHead>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ </TableHeader>
|
|
|
|
|
+ <TableBody>
|
|
|
|
|
+ {logs.map((log) => (
|
|
|
|
|
+ <TableRow key={log.id}>
|
|
|
|
|
+ <TableCell className="text-sm">
|
|
|
|
|
+ {format(new Date(log.createdAt), 'MM-dd HH:mm')}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ <Badge variant="outline">
|
|
|
|
|
+ {log.changeType}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className={log.changeAmount >= 0 ? 'text-green-600' : 'text-red-600'}>
|
|
|
|
|
+ {log.changeAmount >= 0 ? '+' : ''}{log.changeAmount.toFixed(2)}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ {log.beforeTotal?.toFixed(2) || '-'}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ {log.afterTotal?.toFixed(2) || '-'}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="max-w-[200px] truncate">
|
|
|
|
|
+ {log.remark || '-'}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </TableBody>
|
|
|
|
|
+ </Table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* 分页控件 */}
|
|
|
|
|
+ {logsPagination && logsPagination.total > logsQueryParams.limit! && (
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="text-sm text-muted-foreground">
|
|
|
|
|
+ 共 {logsPagination.total} 条记录
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex gap-2">
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => handleLogsPageChange(logsQueryParams.page! - 1)}
|
|
|
|
|
+ disabled={logsQueryParams.page === 1}
|
|
|
|
|
+ >
|
|
|
|
|
+ 上一页
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ onClick={() => handleLogsPageChange(logsQueryParams.page! + 1)}
|
|
|
|
|
+ disabled={logsQueryParams.page! * logsQueryParams.limit! >= logsPagination.total}
|
|
|
|
|
+ >
|
|
|
|
|
+ 下一页
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Alert>
|
|
|
|
|
+ <History className="h-4 w-4" />
|
|
|
|
|
+ <AlertTitle>暂无变更记录</AlertTitle>
|
|
|
|
|
+ <AlertDescription>
|
|
|
|
|
+ 该用户暂无信用额度变更记录
|
|
|
|
|
+ </AlertDescription>
|
|
|
|
|
+ </Alert>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TabsContent>
|
|
|
|
|
+ </Tabs>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <DialogFooter>
|
|
|
|
|
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
|
|
|
|
|
+ 关闭
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+ );
|
|
|
|
|
+};
|