Просмотр исходного кода

✨ feat(admin): 新增用户卡管理和余额记录功能

- 创建用户卡选择器组件支持根据用户筛选卡号
- 创建用户选择器组件支持用户搜索和选择
- 新增用户卡管理页面包含创建、编辑、删除和列表功能
- 新增用户卡余额记录管理页面支持余额变动记录的完整CRUD操作
- 在菜单中添加卡券管理模块包含用户卡和余额记录子菜单
- 配置路由支持新页面的访问
yourname 4 месяцев назад
Родитель
Сommit
09a3f6c9b8

+ 65 - 0
src/client/admin-shadcn/components/UserCardSelector.tsx

@@ -0,0 +1,65 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { userCardClient } from '@/client/api';
+
+interface UserCardSelectorProps {
+  userId?: number;
+  value?: string;
+  onChange?: (value: string) => void;
+  placeholder?: string;
+  disabled?: boolean;
+}
+
+export const UserCardSelector: React.FC<UserCardSelectorProps> = ({
+  userId,
+  value,
+  onChange,
+  placeholder = "选择用户卡",
+  disabled
+}) => {
+  const { data: cards, isLoading } = useQuery({
+    queryKey: ['userCards', userId],
+    queryFn: async () => {
+      if (!userId) return [];
+      
+      const res = await userCardClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ userId })
+        }
+      });
+      
+      if (res.status !== 200) throw new Error('获取用户卡列表失败');
+      const result = await res.json();
+      return result.data;
+    },
+    enabled: !!userId
+  });
+
+  return (
+    <Select 
+      value={value || ''} 
+      onValueChange={onChange}
+      disabled={disabled || isLoading}
+    >
+      <SelectTrigger>
+        <SelectValue placeholder={placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {isLoading ? (
+          <SelectItem value="loading" disabled>加载中...</SelectItem>
+        ) : cards && cards.length > 0 ? (
+          cards.map((card) => (
+            <SelectItem key={card.id} value={card.cardNo}>
+              {card.cardNo} (余额: ¥{card.balance.toFixed(2)})
+            </SelectItem>
+          ))
+        ) : (
+          <SelectItem value="no-cards" disabled>暂无用户卡</SelectItem>
+        )}
+      </SelectContent>
+    </Select>
+  );
+};

+ 59 - 0
src/client/admin-shadcn/components/UserSelector.tsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
+import { userClient } from '@/client/api';
+
+interface UserSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+}
+
+export const UserSelector: React.FC<UserSelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "选择用户",
+  disabled
+}) => {
+  const { data: users, isLoading } = useQuery({
+    queryKey: ['users'],
+    queryFn: async () => {
+      const res = await userClient.$get({
+        query: {
+          page: 1,
+          pageSize: 100
+        }
+      });
+      
+      if (res.status !== 200) throw new Error('获取用户列表失败');
+      const result = await res.json();
+      return result.data;
+    }
+  });
+
+  return (
+    <Select 
+      value={value?.toString() || ''} 
+      onValueChange={(val) => onChange?.(parseInt(val))}
+      disabled={disabled || isLoading}
+    >
+      <SelectTrigger>
+        <SelectValue placeholder={placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {isLoading ? (
+          <SelectItem value="loading" disabled>加载中...</SelectItem>
+        ) : users && users.length > 0 ? (
+          users.map((user) => (
+            <SelectItem key={user.id} value={user.id.toString()}>
+              {user.name || user.username} ({user.phone || user.email})
+            </SelectItem>
+          ))
+        ) : (
+          <SelectItem value="no-users" disabled>暂无用户</SelectItem>
+        )}
+      </SelectContent>
+    </Select>
+  );
+};

+ 23 - 1
src/client/admin-shadcn/menu.tsx

@@ -14,7 +14,9 @@ import {
   Package,
   Package,
   Truck,
   Truck,
   Building,
   Building,
-  UserCheck
+  UserCheck,
+  CreditCard,
+  TrendingUp
 } from 'lucide-react';
 } from 'lucide-react';
 
 
 export interface MenuItem {
 export interface MenuItem {
@@ -174,6 +176,26 @@ export const useMenu = () => {
       path: '/admin/agents',
       path: '/admin/agents',
       permission: 'agent:manage'
       permission: 'agent:manage'
     },
     },
+    {
+      key: 'cards',
+      label: '卡券管理',
+      icon: <CreditCard className="h-4 w-4" />,
+      permission: 'card:manage',
+      children: [
+        {
+          key: 'user-cards',
+          label: '用户卡管理',
+          path: '/admin/user-cards',
+          permission: 'card:manage'
+        },
+        {
+          key: 'user-card-balance-records',
+          label: '余额记录',
+          path: '/admin/user-card-balance-records',
+          permission: 'card:manage'
+        }
+      ]
+    },
     {
     {
       key: 'settings',
       key: 'settings',
       label: '系统设置',
       label: '系统设置',

+ 761 - 0
src/client/admin-shadcn/pages/UserCardBalanceRecords.tsx

@@ -0,0 +1,761 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Search, Edit, Trash2, CreditCard, TrendingUp, TrendingDown } from 'lucide-react';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Badge } from '@/client/components/ui/badge';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination';
+
+import { userCardBalanceRecordClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { CreateUserCardBalanceRecordDto, UpdateUserCardBalanceRecordDto } from '@/server/modules/user-card-balance-records/user-card-balance-record.schema';
+import { UserSelector } from '@/client/admin-shadcn/components/UserSelector';
+import { UserCardSelector } from '@/client/admin-shadcn/components/UserCardSelector';
+
+type CreateRequest = InferRequestType<typeof userCardBalanceRecordClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof userCardBalanceRecordClient[':id']['$put']>['json'];
+type UserCardBalanceRecordResponse = InferResponseType<typeof userCardBalanceRecordClient.$get, 200>['data'][0];
+
+const createFormSchema = CreateUserCardBalanceRecordDto;
+const updateFormSchema = UpdateUserCardBalanceRecordDto;
+
+export const UserCardBalanceRecordsPage = () => {
+  const queryClient = useQueryClient();
+  const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, search: '', type: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [editingRecord, setEditingRecord] = useState<UserCardBalanceRecordResponse | null>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [recordToDelete, setRecordToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      userId: 0,
+      cardNo: '',
+      amount: 0,
+      amountBefore: 0,
+      amountAfter: 0,
+      orderNo: '',
+      type: 1,
+      remark: ''
+    }
+  });
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {}
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['userCardBalanceRecords', searchParams],
+    queryFn: async () => {
+      const res = await userCardBalanceRecordClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search,
+          ...(searchParams.type && { filters: JSON.stringify({ type: parseInt(searchParams.type) }) })
+        }
+      });
+      if (res.status !== 200) throw new Error('获取余额记录列表失败');
+      return await res.json();
+    }
+  });
+
+  // 创建余额记录
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await userCardBalanceRecordClient.$post({ json: data });
+      if (res.status !== 201) throw new Error('创建余额记录失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('余额记录创建成功');
+      setIsModalOpen(false);
+      createForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error.message || '创建余额记录失败');
+    }
+  });
+
+  // 更新余额记录
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await userCardBalanceRecordClient[':id']['$put']({
+        param: { id: id.toString() },
+        json: data
+      });
+      if (res.status !== 200) throw new Error('更新余额记录失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('余额记录更新成功');
+      setIsModalOpen(false);
+      updateForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error.message || '更新余额记录失败');
+    }
+  });
+
+  // 删除余额记录
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await userCardBalanceRecordClient[':id']['$delete']({
+        param: { id: id.toString() }
+      });
+      if (res.status !== 204) throw new Error('删除余额记录失败');
+      return res;
+    },
+    onSuccess: () => {
+      toast.success('余额记录删除成功');
+      setDeleteDialogOpen(false);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error.message || '删除余额记录失败');
+    }
+  });
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 处理创建余额记录
+  const handleCreateRecord = () => {
+    setIsCreateForm(true);
+    setEditingRecord(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 处理编辑余额记录
+  const handleEditRecord = (record: UserCardBalanceRecordResponse) => {
+    setIsCreateForm(false);
+    setEditingRecord(record);
+    updateForm.reset({
+      userId: record.userId,
+      cardNo: record.cardNo,
+      amount: record.amount,
+      amountBefore: record.amountBefore,
+      amountAfter: record.amountAfter,
+      orderNo: record.orderNo,
+      type: record.type,
+      remark: record.remark || undefined
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理删除余额记录
+  const handleDeleteRecord = (id: number) => {
+    setRecordToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = () => {
+    if (recordToDelete) {
+      deleteMutation.mutate(recordToDelete);
+    }
+  };
+
+  // 计算总计
+  const totalAmount = data?.data.reduce((sum, record) => sum + record.amount, 0) || 0;
+  const totalConsumption = data?.data.filter(r => r.type === 1).reduce((sum, record) => sum + record.amount, 0) || 0;
+  const totalRefund = data?.data.filter(r => r.type === 2).reduce((sum, record) => sum + record.amount, 0) || 0;
+
+  // 加载状态
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <Skeleton className="h-8 w-48" />
+          <Skeleton className="h-10 w-32" />
+        </div>
+        
+        <Card>
+          <CardContent className="pt-6">
+            <div className="space-y-3">
+              {[...Array(5)].map((_, i) => (
+                <div key={i} className="flex gap-4">
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 w-20" />
+                </div>
+              ))}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      {/* 页面标题 */}
+      <div className="flex justify-between items-center">
+        <div>
+          <h1 className="text-2xl font-bold">用户卡余额记录</h1>
+          <p className="text-muted-foreground">管理用户卡余额变动记录</p>
+        </div>
+        <Button onClick={handleCreateRecord}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建记录
+        </Button>
+      </div>
+
+      {/* 统计卡片 */}
+      <div className="grid gap-4 md:grid-cols-3">
+        <Card>
+          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+            <CardTitle className="text-sm font-medium">总变动金额</CardTitle>
+            <CreditCard className="h-4 w-4 text-muted-foreground" />
+          </CardHeader>
+          <CardContent>
+            <div className="text-2xl font-bold">¥{totalAmount.toFixed(2)}</div>
+          </CardContent>
+        </Card>
+        <Card>
+          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+            <CardTitle className="text-sm font-medium">总消费</CardTitle>
+            <TrendingDown className="h-4 w-4 text-muted-foreground" />
+          </CardHeader>
+          <CardContent>
+            <div className="text-2xl font-bold text-red-600">¥{totalConsumption.toFixed(2)}</div>
+          </CardContent>
+        </Card>
+        <Card>
+          <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+            <CardTitle className="text-sm font-medium">总退款</CardTitle>
+            <TrendingUp className="h-4 w-4 text-muted-foreground" />
+          </CardHeader>
+          <CardContent>
+            <div className="text-2xl font-bold text-green-600">¥{totalRefund.toFixed(2)}</div>
+          </CardContent>
+        </Card>
+      </div>
+
+      {/* 搜索区域 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>余额记录列表</CardTitle>
+          <CardDescription>查看和管理所有余额变动记录</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <form onSubmit={handleSearch} className="flex gap-2 flex-wrap">
+            <div className="relative flex-1 max-w-sm">
+              <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="搜索卡号、订单号、备注..."
+                value={searchParams.search}
+                onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                className="pl-8"
+              />
+            </div>
+            <select
+              className="px-3 py-2 border rounded-md"
+              value={searchParams.type}
+              onChange={(e) => setSearchParams(prev => ({ ...prev, type: e.target.value, page: 1 }))}
+            >
+              <option value="">全部类型</option>
+              <option value="1">消费</option>
+              <option value="2">退款</option>
+            </select>
+            <Button type="submit" variant="outline">
+              搜索
+            </Button>
+          </form>
+        </CardContent>
+      </Card>
+
+      {/* 数据表格 */}
+      <Card>
+        <CardContent className="p-0">
+          <Table>
+            <TableHeader>
+              <TableRow>
+                <TableHead>ID</TableHead>
+                <TableHead>用户</TableHead>
+                <TableHead>卡号</TableHead>
+                <TableHead>变动金额</TableHead>
+                <TableHead>变动前</TableHead>
+                <TableHead>变动后</TableHead>
+                <TableHead>订单号</TableHead>
+                <TableHead>类型</TableHead>
+                <TableHead>备注</TableHead>
+                <TableHead>创建时间</TableHead>
+                <TableHead className="text-right">操作</TableHead>
+              </TableRow>
+            </TableHeader>
+            <TableBody>
+              {data?.data.map((record) => (
+                <TableRow key={record.id}>
+                  <TableCell>{record.id}</TableCell>
+                  <TableCell>{record.user?.name || record.user?.username || '-'}</TableCell>
+                  <TableCell>{record.cardNo}</TableCell>
+                  <TableCell>
+                    <span className={record.type === 1 ? 'text-red-600' : 'text-green-600'}>
+                      {record.type === 1 ? '-' : '+'}¥{record.amount.toFixed(2)}
+                    </span>
+                  </TableCell>
+                  <TableCell>¥{record.amountBefore.toFixed(2)}</TableCell>
+                  <TableCell>¥{record.amountAfter.toFixed(2)}</TableCell>
+                  <TableCell className="font-mono text-xs">{record.orderNo}</TableCell>
+                  <TableCell>
+                    <Badge variant={record.type === 1 ? 'destructive' : 'default'}>
+                      {record.type === 1 ? (
+                        <>
+                          <TrendingDown className="h-3 w-3 mr-1" />
+                          消费
+                        </>
+                      ) : (
+                        <>
+                          <TrendingUp className="h-3 w-3 mr-1" />
+                          退款
+                        </>
+                      )}
+                    </Badge>
+                  </TableCell>
+                  <TableCell>{record.remark || '-'}</TableCell>
+                  <TableCell>
+                    {format(new Date(record.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
+                  </TableCell>
+                  <TableCell className="text-right">
+                    <div className="flex justify-end gap-2">
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={() => handleEditRecord(record)}
+                      >
+                        <Edit className="h-4 w-4" />
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={() => handleDeleteRecord(record.id)}
+                      >
+                        <Trash2 className="h-4 w-4" />
+                      </Button>
+                    </div>
+                  </TableCell>
+                </TableRow>
+              ))}
+            </TableBody>
+          </Table>
+          
+          {data?.data.length === 0 && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无余额记录数据</p>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 分页 */}
+      {data && data.data.length > 0 && (
+        <DataTablePagination
+          currentPage={searchParams.page}
+          pageSize={searchParams.limit}
+          totalCount={data.pagination.total || 0}
+          onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+        />
+      )}
+
+      {/* 创建/编辑模态框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建余额记录' : '编辑余额记录'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的余额变动记录' : '编辑现有余额记录信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit((data) => createMutation.mutate(data))} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="userId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <UserSelector
+                          value={field.value}
+                          onChange={field.onChange}
+                          placeholder="选择用户"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="cardNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        卡号 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <UserCardSelector
+                          userId={createForm.watch('userId')}
+                          value={field.value}
+                          onChange={field.onChange}
+                          placeholder="选择用户卡"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="amount"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        变动金额 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="amountBefore"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        变动前金额 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="amountAfter"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        变动后金额 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="orderNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        订单号 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入订单号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="type"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>类型</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="w-full px-3 py-2 border rounded-md"
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={1}>消费</option>
+                          <option value={2}>退款</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="remark"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>备注</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入备注" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending}>
+                    {createMutation.isPending ? '创建中...' : '创建'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit((data) => updateMutation.mutate({ id: editingRecord!.id, data }))} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="userId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <UserSelector
+                          value={field.value || editingRecord?.userId || 0}
+                          onChange={field.onChange}
+                          placeholder="选择用户"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="cardNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        卡号 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <UserCardSelector
+                          userId={updateForm.watch('userId') || editingRecord?.userId || 0}
+                          value={field.value || editingRecord?.cardNo || ''}
+                          onChange={field.onChange}
+                          placeholder="选择用户卡"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="amount"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        变动金额 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="amountBefore"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        变动前金额 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="amountAfter"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        变动后金额 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="orderNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        订单号 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入订单号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="type"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>类型</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="w-full px-3 py-2 border rounded-md"
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={1}>消费</option>
+                          <option value={2}>退款</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="remark"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>备注</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入备注" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={updateMutation.isPending}>
+                    {updateMutation.isPending ? '更新中...' : '更新'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这条余额记录吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button variant="destructive" onClick={confirmDelete} disabled={deleteMutation.isPending}>
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};

+ 678 - 0
src/client/admin-shadcn/pages/UserCards.tsx

@@ -0,0 +1,678 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Plus, Search, Edit, Trash2, CreditCard } from 'lucide-react';
+import { format } from 'date-fns';
+import { zhCN } from 'date-fns/locale';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+
+import { Button } from '@/client/components/ui/button';
+import { Input } from '@/client/components/ui/input';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/client/components/ui/table';
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/client/components/ui/dialog';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/client/components/ui/form';
+import { Badge } from '@/client/components/ui/badge';
+import { Skeleton } from '@/client/components/ui/skeleton';
+import { DataTablePagination } from '@/client/admin-shadcn/components/DataTablePagination';
+
+import { userCardClient } from '@/client/api';
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { CreateUserCardDto, UpdateUserCardDto } from '@/server/modules/user-cards/user-card.schema';
+import { UserSelector } from '@/client/admin-shadcn/components/UserSelector';
+import { AgentSelector } from '@/client/admin-shadcn/components/AgentSelector';
+
+type CreateRequest = InferRequestType<typeof userCardClient.$post>['json'];
+type UpdateRequest = InferRequestType<typeof userCardClient[':id']['$put']>['json'];
+type UserCardResponse = InferResponseType<typeof userCardClient.$get, 200>['data'][0];
+
+const createFormSchema = CreateUserCardDto;
+const updateFormSchema = UpdateUserCardDto;
+
+export const UserCardsPage = () => {
+  const queryClient = useQueryClient();
+  const [searchParams, setSearchParams] = useState({ page: 1, limit: 10, search: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [editingCard, setEditingCard] = useState<UserCardResponse | null>(null);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [cardToDelete, setCardToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      userId: 0,
+      cardNo: '',
+      password: '',
+      balance: 0,
+      state: 1,
+      isDefault: 2
+    }
+  });
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {}
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['userCards', searchParams],
+    queryFn: async () => {
+      const res = await userCardClient.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      });
+      if (res.status !== 200) throw new Error('获取用户卡列表失败');
+      return await res.json();
+    }
+  });
+
+  // 创建用户卡
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await userCardClient.$post({ json: data });
+      if (res.status !== 201) throw new Error('创建用户卡失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('用户卡创建成功');
+      setIsModalOpen(false);
+      createForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error.message || '创建用户卡失败');
+    }
+  });
+
+  // 更新用户卡
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await userCardClient[':id']['$put']({
+        param: { id: id.toString() },
+        json: data
+      });
+      if (res.status !== 200) throw new Error('更新用户卡失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('用户卡更新成功');
+      setIsModalOpen(false);
+      updateForm.reset();
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error.message || '更新用户卡失败');
+    }
+  });
+
+  // 删除用户卡
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await userCardClient[':id']['$delete']({
+        param: { id: id.toString() }
+      });
+      if (res.status !== 204) throw new Error('删除用户卡失败');
+      return res;
+    },
+    onSuccess: () => {
+      toast.success('用户卡删除成功');
+      setDeleteDialogOpen(false);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error.message || '删除用户卡失败');
+    }
+  });
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+  };
+
+  // 处理创建用户卡
+  const handleCreateCard = () => {
+    setIsCreateForm(true);
+    setEditingCard(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 处理编辑用户卡
+  const handleEditCard = (card: UserCardResponse) => {
+    setIsCreateForm(false);
+    setEditingCard(card);
+    updateForm.reset({
+      userId: card.userId,
+      agentId: card.agentId || undefined,
+      cardNo: card.cardNo,
+      sjtCardNo: card.sjtCardNo || undefined,
+      password: card.password,
+      authCode: card.authCode || undefined,
+      state: card.state,
+      balance: card.balance,
+      isDefault: card.isDefault
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理删除用户卡
+  const handleDeleteCard = (id: number) => {
+    setCardToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = () => {
+    if (cardToDelete) {
+      deleteMutation.mutate(cardToDelete);
+    }
+  };
+
+  // 加载状态
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <Skeleton className="h-8 w-48" />
+          <Skeleton className="h-10 w-32" />
+        </div>
+        
+        <Card>
+          <CardContent className="pt-6">
+            <div className="space-y-3">
+              {[...Array(5)].map((_, i) => (
+                <div key={i} className="flex gap-4">
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 flex-1" />
+                  <Skeleton className="h-10 w-20" />
+                </div>
+              ))}
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-4">
+      {/* 页面标题 */}
+      <div className="flex justify-between items-center">
+        <div>
+          <h1 className="text-2xl font-bold">用户卡管理</h1>
+          <p className="text-muted-foreground">管理用户卡和余额信息</p>
+        </div>
+        <Button onClick={handleCreateCard}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建用户卡
+        </Button>
+      </div>
+
+      {/* 搜索区域 */}
+      <Card>
+        <CardHeader>
+          <CardTitle>用户卡列表</CardTitle>
+          <CardDescription>查看和管理所有用户卡</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <form onSubmit={handleSearch} className="flex gap-2">
+            <div className="relative flex-1 max-w-sm">
+              <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+              <Input
+                placeholder="搜索卡号、盛京通卡号..."
+                value={searchParams.search}
+                onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                className="pl-8"
+              />
+            </div>
+            <Button type="submit" variant="outline">
+              搜索
+            </Button>
+          </form>
+        </CardContent>
+      </Card>
+
+      {/* 数据表格 */}
+      <Card>
+        <CardContent className="p-0">
+          <Table>
+            <TableHeader>
+              <TableRow>
+                <TableHead>ID</TableHead>
+                <TableHead>用户</TableHead>
+                <TableHead>卡号</TableHead>
+                <TableHead>盛京通卡号</TableHead>
+                <TableHead>代理商</TableHead>
+                <TableHead>余额</TableHead>
+                <TableHead>状态</TableHead>
+                <TableHead>默认</TableHead>
+                <TableHead>创建时间</TableHead>
+                <TableHead className="text-right">操作</TableHead>
+              </TableRow>
+            </TableHeader>
+            <TableBody>
+              {data?.data.map((card) => (
+                <TableRow key={card.id}>
+                  <TableCell>{card.id}</TableCell>
+                  <TableCell>{card.user?.name || card.user?.username || '-'}</TableCell>
+                  <TableCell>{card.cardNo}</TableCell>
+                  <TableCell>{card.sjtCardNo || '-'}</TableCell>
+                  <TableCell>{card.agent?.name || '-'}</TableCell>
+                  <TableCell>¥{card.balance.toFixed(2)}</TableCell>
+                  <TableCell>
+                    <Badge variant={card.state === 1 ? 'default' : 'secondary'}>
+                      {card.state === 1 ? '绑定' : '解绑'}
+                    </Badge>
+                  </TableCell>
+                  <TableCell>
+                    <Badge variant={card.isDefault === 1 ? 'default' : 'secondary'}>
+                      {card.isDefault === 1 ? '是' : '否'}
+                    </Badge>
+                  </TableCell>
+                  <TableCell>
+                    {format(new Date(card.createdAt), 'yyyy-MM-dd HH:mm', { locale: zhCN })}
+                  </TableCell>
+                  <TableCell className="text-right">
+                    <div className="flex justify-end gap-2">
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={() => handleEditCard(card)}
+                      >
+                        <Edit className="h-4 w-4" />
+                      </Button>
+                      <Button
+                        variant="ghost"
+                        size="icon"
+                        onClick={() => handleDeleteCard(card.id)}
+                      >
+                        <Trash2 className="h-4 w-4" />
+                      </Button>
+                    </div>
+                  </TableCell>
+                </TableRow>
+              ))}
+            </TableBody>
+          </Table>
+          
+          {data?.data.length === 0 && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无用户卡数据</p>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 分页 */}
+      {data && data.data.length > 0 && (
+        <DataTablePagination
+          currentPage={searchParams.page}
+          pageSize={searchParams.limit}
+          totalCount={data.pagination.total || 0}
+          onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+        />
+      )}
+
+      {/* 创建/编辑模态框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建用户卡' : '编辑用户卡'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的用户卡' : '编辑现有用户卡信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit((data) => createMutation.mutate(data))} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="userId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <UserSelector
+                          value={field.value}
+                          onChange={field.onChange}
+                          placeholder="选择用户"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="cardNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        卡号 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入卡号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        密码 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="请输入密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="agentId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>代理商</FormLabel>
+                      <FormControl>
+                        <AgentSelector
+                          value={field.value || undefined}
+                          onChange={field.onChange}
+                          placeholder="选择代理商"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="sjtCardNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>盛京通卡号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入盛京通卡号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="balance"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>初始余额</FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="state"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="w-full px-3 py-2 border rounded-md"
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={1}>绑定</option>
+                          <option value={2}>解绑</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="isDefault"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>是否默认</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="w-full px-3 py-2 border rounded-md"
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={1}>是</option>
+                          <option value={2}>否</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending}>
+                    {createMutation.isPending ? '创建中...' : '创建'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit((data) => updateMutation.mutate({ id: editingCard!.id, data }))} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="userId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        用户 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <UserSelector
+                          value={field.value || editingCard?.userId || 0}
+                          onChange={field.onChange}
+                          placeholder="选择用户"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="cardNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        卡号 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入卡号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="password"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        密码 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input type="password" placeholder="请输入密码" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="agentId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>代理商</FormLabel>
+                      <FormControl>
+                        <AgentSelector
+                          value={field.value || editingCard?.agentId || undefined}
+                          onChange={field.onChange}
+                          placeholder="选择代理商"
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="sjtCardNo"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>盛京通卡号</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入盛京通卡号" {...field} />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="balance"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>余额</FormLabel>
+                      <FormControl>
+                        <Input
+                          type="number"
+                          step="0.01"
+                          placeholder="0.00"
+                          {...field}
+                          onChange={(e) => field.onChange(parseFloat(e.target.value))}
+                        />
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="state"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="w-full px-3 py-2 border rounded-md"
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={1}>绑定</option>
+                          <option value={2}>解绑</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="isDefault"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>是否默认</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="w-full px-3 py-2 border rounded-md"
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                        >
+                          <option value={1}>是</option>
+                          <option value={2}>否</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={updateMutation.isPending}>
+                    {updateMutation.isPending ? '更新中...' : '更新'}
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这张用户卡吗?此操作将同时删除相关的余额记录。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button variant="destructive" onClick={confirmDelete} disabled={deleteMutation.isPending}>
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};

+ 12 - 0
src/client/admin-shadcn/routes.tsx

@@ -16,6 +16,8 @@ import { ExpressCompaniesPage } from './pages/ExpressCompanies';
 import { SuppliersPage } from './pages/Suppliers';
 import { SuppliersPage } from './pages/Suppliers';
 import { MerchantsPage } from './pages/Merchants'
 import { MerchantsPage } from './pages/Merchants'
 import { AgentsPage } from './pages/Agents';
 import { AgentsPage } from './pages/Agents';
+import { UserCardsPage } from './pages/UserCards';
+import { UserCardBalanceRecordsPage } from './pages/UserCardBalanceRecords';
 
 
 export const router = createBrowserRouter([
 export const router = createBrowserRouter([
   {
   {
@@ -93,6 +95,16 @@ export const router = createBrowserRouter([
         element: <AgentsPage />,
         element: <AgentsPage />,
         errorElement: <ErrorPage />
         errorElement: <ErrorPage />
       },
       },
+      {
+        path: 'user-cards',
+        element: <UserCardsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'user-card-balance-records',
+        element: <UserCardBalanceRecordsPage />,
+        errorElement: <ErrorPage />
+      },
       {
       {
         path: '*',
         path: '*',
         element: <NotFoundPage />,
         element: <NotFoundPage />,