فهرست منبع

✨ feat(components): add wechat coupon and pay config selectors

- 创建WechatCouponStockSelector组件,支持代金券批次选择,包含状态筛选和详情展示
- 创建WechatPayConfigSelector组件,支持微信支付配置选择,显示商户号和应用ID信息
- 在RedemptionCodes页面中使用WechatCouponStockSelector替换原有的批次选择逻辑
- 在WechatCouponStocks页面中使用WechatPayConfigSelector替换原有的支付配置选择逻辑
- 优化选择器UI,显示更多相关信息如状态、数量、金额等,提升用户体验
yourname 5 ماه پیش
والد
کامیت
a6af17a13d

+ 148 - 0
src/client/admin-shadcn/components/WechatCouponStockSelector.tsx

@@ -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>
+  );
+}

+ 134 - 0
src/client/admin-shadcn/components/WechatPayConfigSelector.tsx

@@ -0,0 +1,134 @@
+import { useState } 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 { wechatPayConfigClient } from '@/client/api';
+import type { WechatPayConfig } from '@/server/modules/wechat-pay/wechat-pay-config.entity';
+
+interface WechatPayConfigSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  disabled?: boolean;
+  placeholder?: string;
+  label?: string;
+  required?: boolean;
+  activeOnly?: boolean;
+}
+
+export function WechatPayConfigSelector({
+  value,
+  onChange,
+  disabled,
+  placeholder = "请选择微信支付配置",
+  label = "微信支付配置",
+  required = false,
+  activeOnly = true,
+}: WechatPayConfigSelectorProps) {
+  const [searchText, setSearchText] = useState('');
+
+  const { data, isLoading, error } = useQuery({
+    queryKey: ['wechat-pay-configs', { active: activeOnly }],
+    queryFn: async () => {
+      const res = await wechatPayConfigClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          ...(activeOnly && { isActive: 1 }),
+        }
+      });
+      
+      if (res.status !== 200) {
+        throw new Error('获取支付配置列表失败');
+      }
+      
+      const result = await res.json();
+      return result.data;
+    },
+    staleTime: 5 * 60 * 1000, // 5分钟缓存
+  });
+
+  const filteredConfigs = data?.filter((config: WechatPayConfig) => {
+    if (!searchText) return true;
+    return config.merchantId.toLowerCase().includes(searchText.toLowerCase()) ||
+           config.appId.toLowerCase().includes(searchText.toLowerCase());
+  });
+
+  const getStatusDisplay = (isActive: number) => {
+    return isActive === 1 ? '启用' : '禁用';
+  };
+
+  const getStatusColor = (isActive: number) => {
+    return isActive === 1 ? 'text-green-500' : 'text-red-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>
+          {filteredConfigs?.map((config: WechatPayConfig) => (
+            <SelectItem key={config.id} value={config.id.toString()}>
+              <div className="flex flex-col">
+                <span className="font-medium">
+                  商户号: {config.merchantId}
+                </span>
+                <span className="text-sm text-muted-foreground">
+                  应用ID: {config.appId}
+                </span>
+                <span className={`text-xs ${getStatusColor(config.isActive)}`}>
+                  状态: {getStatusDisplay(config.isActive)}
+                </span>
+              </div>
+            </SelectItem>
+          ))}
+        </SelectContent>
+      </Select>
+      <FormMessage />
+    </FormItem>
+  );
+}

+ 15 - 48
src/client/admin-shadcn/pages/RedemptionCodes.tsx

@@ -21,6 +21,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
 import { Textarea } from '@/client/components/ui/textarea'
 import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination'
 import { wechatCouponStockClient } from '@/client/api'
+import { WechatCouponStockSelector } from '@/client/admin-shadcn/components/WechatCouponStockSelector'
 
 type CreateRequest = InferRequestType<typeof redemptionCodeClient.$post>['json']
 type UpdateRequest = InferRequestType<typeof redemptionCodeClient[':id']['$put']>['json']
@@ -432,30 +433,13 @@ export const RedemptionCodesPage = () => {
                 control={(isCreateForm ? createForm : updateForm).control}
                 name="batchId"
                 render={({ field }) => (
-                  <FormItem>
-                    <FormLabel className="flex items-center">
-                      代金券批次
-                      <span className="text-red-500 ml-1">*</span>
-                    </FormLabel>
-                    <Select
-                      onValueChange={(value) => field.onChange(Number(value))}
-                      value={String(field.value)}
-                    >
-                      <FormControl>
-                        <SelectTrigger>
-                          <SelectValue placeholder="请选择批次" />
-                        </SelectTrigger>
-                      </FormControl>
-                      <SelectContent>
-                        {stockData?.data.map((stock) => (
-                          <SelectItem key={stock.id} value={String(stock.id)}>
-                            {stock.stockName}
-                          </SelectItem>
-                        ))}
-                      </SelectContent>
-                    </Select>
-                    <FormMessage />
-                  </FormItem>
+                  <WechatCouponStockSelector
+                    value={field.value}
+                    onChange={field.onChange}
+                    required
+                    label="代金券批次"
+                    placeholder="请选择代金券批次"
+                  />
                 )}
               />
 
@@ -615,30 +599,13 @@ export const RedemptionCodesPage = () => {
                 control={bulkForm.control}
                 name="batchId"
                 render={({ field }) => (
-                  <FormItem>
-                    <FormLabel className="flex items-center">
-                      代金券批次
-                      <span className="text-red-500 ml-1">*</span>
-                    </FormLabel>
-                    <Select
-                      onValueChange={(value) => field.onChange(Number(value))}
-                      value={String(field.value)}
-                    >
-                      <FormControl>
-                        <SelectTrigger>
-                          <SelectValue placeholder="请选择批次" />
-                        </SelectTrigger>
-                      </FormControl>
-                      <SelectContent>
-                        {stockData?.data.map((stock) => (
-                          <SelectItem key={stock.id} value={String(stock.id)}>
-                            {stock.stockName}
-                          </SelectItem>
-                        ))}
-                      </SelectContent>
-                    </Select>
-                    <FormMessage />
-                  </FormItem>
+                  <WechatCouponStockSelector
+                    value={field.value}
+                    onChange={field.onChange}
+                    required
+                    label="代金券批次"
+                    placeholder="请选择代金券批次"
+                  />
                 )}
               />
 

+ 8 - 24
src/client/admin-shadcn/pages/WechatCouponStocks.tsx

@@ -21,6 +21,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
 import { Textarea } from '@/client/components/ui/textarea'
 import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination'
 import { wechatPayConfigClient } from '@/client/api'
+import { WechatPayConfigSelector } from '@/client/admin-shadcn/components/WechatPayConfigSelector'
 import { z } from 'zod'
 
 type CreateRequest = InferRequestType<typeof wechatCouponStockClient.$post>['json']
@@ -351,30 +352,13 @@ export const WechatCouponStocksPage = () => {
                 control={(isCreateForm ? createForm : updateForm).control}
                 name="configId"
                 render={({ field }) => (
-                  <FormItem>
-                    <FormLabel className="flex items-center">
-                      支付配置
-                      <span className="text-red-500 ml-1">*</span>
-                    </FormLabel>
-                    <Select
-                      onValueChange={(value) => field.onChange(Number(value))}
-                      value={String(field.value)}
-                    >
-                      <FormControl>
-                        <SelectTrigger>
-                          <SelectValue placeholder="请选择支付配置" />
-                        </SelectTrigger>
-                      </FormControl>
-                      <SelectContent>
-                        {configData?.data.map((config) => (
-                          <SelectItem key={config.id} value={String(config.id)}>
-                            {config.merchantId} - {config.appId}
-                          </SelectItem>
-                        ))}
-                      </SelectContent>
-                    </Select>
-                    <FormMessage />
-                  </FormItem>
+                  <WechatPayConfigSelector
+                    value={field.value}
+                    onChange={field.onChange}
+                    required
+                    label="支付配置"
+                    placeholder="请选择微信支付配置"
+                  />
                 )}
               />