|
|
@@ -0,0 +1,148 @@
|
|
|
+import { useState, useEffect } from 'react';
|
|
|
+import { FormControl, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
|
|
|
+import {
|
|
|
+ Select,
|
|
|
+ SelectContent,
|
|
|
+ SelectItem,
|
|
|
+ SelectTrigger,
|
|
|
+ SelectValue,
|
|
|
+} from '@/client/components/ui/select';
|
|
|
+import { Skeleton } from '@/client/components/ui/skeleton';
|
|
|
+import { useQuery } from '@tanstack/react-query';
|
|
|
+import { wechatCouponStockClient } from '@/client/api';
|
|
|
+import type { WechatCouponStock } from '@/server/modules/wechat-pay/wechat-coupon-stock.entity';
|
|
|
+
|
|
|
+interface WechatCouponStockSelectorProps {
|
|
|
+ value?: number;
|
|
|
+ onChange?: (value: number) => void;
|
|
|
+ disabled?: boolean;
|
|
|
+ placeholder?: string;
|
|
|
+ label?: string;
|
|
|
+ required?: boolean;
|
|
|
+ statusFilter?: string[];
|
|
|
+}
|
|
|
+
|
|
|
+export function WechatCouponStockSelector({
|
|
|
+ value,
|
|
|
+ onChange,
|
|
|
+ disabled,
|
|
|
+ placeholder = "请选择代金券批次",
|
|
|
+ label = "代金券批次",
|
|
|
+ required = false,
|
|
|
+ statusFilter = ['RUNNING', 'PROCESSING'],
|
|
|
+}: WechatCouponStockSelectorProps) {
|
|
|
+ const [searchText, setSearchText] = useState('');
|
|
|
+
|
|
|
+ const { data, isLoading, error } = useQuery({
|
|
|
+ queryKey: ['wechat-coupon-stocks', { status: statusFilter.join(',') }],
|
|
|
+ queryFn: async () => {
|
|
|
+ const res = await wechatCouponStockClient.$get({
|
|
|
+ query: {
|
|
|
+ page: 1,
|
|
|
+ pageSize: 100,
|
|
|
+ status: statusFilter.join(','),
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (res.status !== 200) {
|
|
|
+ throw new Error('获取批次列表失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ const result = await res.json();
|
|
|
+ return result.data;
|
|
|
+ },
|
|
|
+ staleTime: 5 * 60 * 1000, // 5分钟缓存
|
|
|
+ });
|
|
|
+
|
|
|
+ const filteredStocks = data?.filter((stock: WechatCouponStock) => {
|
|
|
+ if (!searchText) return true;
|
|
|
+ return stock.stockName.toLowerCase().includes(searchText.toLowerCase()) ||
|
|
|
+ stock.stockId.toLowerCase().includes(searchText.toLowerCase());
|
|
|
+ });
|
|
|
+
|
|
|
+ const getStatusDisplay = (status: string) => {
|
|
|
+ const statusMap: Record<string, string> = {
|
|
|
+ 'CREATED': '已创建',
|
|
|
+ 'PROCESSING': '发放中',
|
|
|
+ 'RUNNING': '运行中',
|
|
|
+ 'STOPED': '已停止',
|
|
|
+ 'PAUSED': '暂停',
|
|
|
+ 'FINISHED': '已结束',
|
|
|
+ };
|
|
|
+ return statusMap[status] || status;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getStatusColor = (status: string) => {
|
|
|
+ const colorMap: Record<string, string> = {
|
|
|
+ 'CREATED': 'text-gray-500',
|
|
|
+ 'PROCESSING': 'text-blue-500',
|
|
|
+ 'RUNNING': 'text-green-500',
|
|
|
+ 'STOPED': 'text-red-500',
|
|
|
+ 'PAUSED': 'text-yellow-500',
|
|
|
+ 'FINISHED': 'text-gray-400',
|
|
|
+ };
|
|
|
+ return colorMap[status] || 'text-gray-500';
|
|
|
+ };
|
|
|
+
|
|
|
+ if (isLoading) {
|
|
|
+ return (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>
|
|
|
+ {label}
|
|
|
+ {required && <span className="text-red-500 ml-1">*</span>}
|
|
|
+ </FormLabel>
|
|
|
+ <Skeleton className="h-10 w-full" />
|
|
|
+ </FormItem>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (error) {
|
|
|
+ return (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>
|
|
|
+ {label}
|
|
|
+ {required && <span className="text-red-500 ml-1">*</span>}
|
|
|
+ </FormLabel>
|
|
|
+ <div className="text-sm text-red-500">
|
|
|
+ 加载失败,请稍后重试
|
|
|
+ </div>
|
|
|
+ </FormItem>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <FormItem>
|
|
|
+ <FormLabel>
|
|
|
+ {label}
|
|
|
+ {required && <span className="text-red-500 ml-1">*</span>}
|
|
|
+ </FormLabel>
|
|
|
+ <Select
|
|
|
+ value={value?.toString()}
|
|
|
+ onValueChange={(val) => onChange?.(Number(val))}
|
|
|
+ disabled={disabled || !data?.length}
|
|
|
+ >
|
|
|
+ <FormControl>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder={placeholder} />
|
|
|
+ </SelectTrigger>
|
|
|
+ </FormControl>
|
|
|
+ <SelectContent>
|
|
|
+ {filteredStocks?.map((stock: WechatCouponStock) => (
|
|
|
+ <SelectItem key={stock.id} value={stock.id.toString()}>
|
|
|
+ <div className="flex flex-col">
|
|
|
+ <span className="font-medium">{stock.stockName}</span>
|
|
|
+ <span className="text-sm text-muted-foreground">
|
|
|
+ 批次号: {stock.stockId} | 面额: ¥{(stock.couponAmount / 100).toFixed(2)}
|
|
|
+ </span>
|
|
|
+ <span className={`text-xs ${getStatusColor(stock.status)}`}>
|
|
|
+ 状态: {getStatusDisplay(stock.status)} | 可用: {stock.availableQuantity}/{stock.couponQuantity}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </SelectItem>
|
|
|
+ ))}
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ <FormMessage />
|
|
|
+ </FormItem>
|
|
|
+ );
|
|
|
+}
|