Răsfoiți Sursa

✨ feat(disability-module): 新增银行名称模块依赖并重构银行卡实体

- 添加@d8d/bank-names-module依赖到package.json
- 重构disabled-bank-card.entity.ts,将bankName字段改为bankNameId外键关联
- 新增cardType字段支持一类卡/二类卡类型
- 建立与BankName实体的多对一关系

📝 docs(disability-module): 更新残疾人Schema以支持银行名称关联

- 更新disabled-person.schema.ts中的DisabledBankCardSchema
- 将bankName字段改为bankNameId并关联BankNameSchema
- 新增cardType字段Schema定义
- 新增CreateDisabledBankCardSchema和UpdateDisabledBankCardSchema用于创建和更新操作

✨ feat(disability-person-management-ui): 集成银行名称选择器并优化银行卡管理

- 添加@d8d/bank-name-management-ui依赖
- 重构BankCardManagement组件,使用BankSelect组件替代原有的银行名称输入框
- 新增银行卡类型选择器(一类卡/二类卡)
- 新增BankSelect组件,支持从银行名称模块获取银行列表
- 更新组件导出文件包含BankSelect组件

✅ test(disability-person-management-ui): 新增银行卡管理集成测试

- 新增bank-card-management.integration.test.tsx集成测试文件
- 测试银行卡管理组件的各项功能:添加、删除、编辑、验证等
- 测试银行选择器和银行卡类型选择器的集成
- 更新测试配置文件以支持ResizeObserver和Element.scrollIntoView的mock

📝 docs: 更新银行卡管理优化故事文档状态

- 将009.004.bank-card-management-optimization.story.md状态从Draft更新为Ready for Development

✨ feat(bank-name-management-ui): 新增银行名称管理UI包

- 创建完整的银行名称管理UI包,包含BankNameManagement和BankNameSelector组件
- 实现银行名称的CRUD操作界面
- 提供API客户端管理器和类型定义
- 包含完整的集成测试套件
- 配置ESLint、TypeScript和Vitest开发环境

✨ feat(bank-names-module): 新增银行名称管理模块

- 创建银行名称实体、Schema、服务和路由
- 集成共享CRUD模块提供完整的CRUD功能
- 实现银行名称的创建、读取、更新、删除操作
- 包含完整的集成测试套件
- 配置TypeScript和Vitest开发环境

📦 build: 更新pnpm-lock.yaml依赖关系

- 添加新包@d8d/bank-names-module和@d8d/bank-name-management-ui的依赖关系
- 更新相关包的版本依赖
yourname 2 luni în urmă
părinte
comite
84d0bafdf5
35 a modificat fișierele cu 3282 adăugiri și 53 ștergeri
  1. 1 0
      allin-packages/disability-module/package.json
  2. 19 5
      allin-packages/disability-module/src/entities/disabled-bank-card.entity.ts
  3. 90 3
      allin-packages/disability-module/src/schemas/disabled-person.schema.ts
  4. 1 0
      allin-packages/disability-person-management-ui/package.json
  5. 53 8
      allin-packages/disability-person-management-ui/src/components/BankCardManagement.tsx
  6. 101 0
      allin-packages/disability-person-management-ui/src/components/BankSelect.tsx
  7. 1 0
      allin-packages/disability-person-management-ui/src/components/index.ts
  8. 499 0
      allin-packages/disability-person-management-ui/tests/integration/bank-card-management.integration.test.tsx
  9. 33 2
      allin-packages/disability-person-management-ui/tests/setup.ts
  10. 1 1
      docs/stories/009.004.bank-card-management-optimization.story.md
  11. 36 0
      packages/bank-name-management-ui/eslint.config.js
  12. 93 0
      packages/bank-name-management-ui/package.json
  13. 44 0
      packages/bank-name-management-ui/src/api/bankNameClient.ts
  14. 3 0
      packages/bank-name-management-ui/src/api/index.ts
  15. 586 0
      packages/bank-name-management-ui/src/components/BankNameManagement.tsx
  16. 78 0
      packages/bank-name-management-ui/src/components/BankNameSelector.tsx
  17. 4 0
      packages/bank-name-management-ui/src/components/index.ts
  18. 19 0
      packages/bank-name-management-ui/src/index.ts
  19. 58 0
      packages/bank-name-management-ui/src/types/bankName.ts
  20. 343 0
      packages/bank-name-management-ui/tests/integration/bank-name-management.integration.test.tsx
  21. 214 0
      packages/bank-name-management-ui/tests/integration/bank-name-selector.integration.test.tsx
  22. 46 0
      packages/bank-name-management-ui/tests/setup.ts
  23. 36 0
      packages/bank-name-management-ui/tsconfig.json
  24. 24 0
      packages/bank-name-management-ui/vitest.config.ts
  25. 78 0
      packages/bank-names-module/package.json
  26. 73 0
      packages/bank-names-module/src/entities/bank-name.entity.ts
  27. 6 0
      packages/bank-names-module/src/index.ts
  28. 18 0
      packages/bank-names-module/src/routes/bank-names.ts
  29. 81 0
      packages/bank-names-module/src/schemas/bank-name.schema.ts
  30. 1 0
      packages/bank-names-module/src/schemas/index.ts
  31. 9 0
      packages/bank-names-module/src/services/bank-name.service.ts
  32. 268 0
      packages/bank-names-module/tests/integration/bank-names.integration.test.ts
  33. 16 0
      packages/bank-names-module/tsconfig.json
  34. 21 0
      packages/bank-names-module/vitest.config.ts
  35. 328 34
      pnpm-lock.yaml

+ 1 - 0
allin-packages/disability-module/package.json

@@ -52,6 +52,7 @@
     "@d8d/auth-module": "workspace:*",
     "@d8d/user-module": "workspace:*",
     "@d8d/file-module": "workspace:*",
+    "@d8d/bank-names-module": "workspace:*",
     "@hono/zod-openapi": "^1.0.2",
     "typeorm": "^0.3.20",
     "zod": "^4.1.12"

+ 19 - 5
allin-packages/disability-module/src/entities/disabled-bank-card.entity.ts

@@ -1,6 +1,7 @@
 import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, JoinColumn } from 'typeorm';
 import { DisabledPerson } from './disabled-person.entity';
 import { File } from '@d8d/file-module';
+import { BankName } from '@d8d/bank-names-module';
 
 @Entity('disabled_bank_card')
 export class DisabledBankCard {
@@ -29,13 +30,12 @@ export class DisabledBankCard {
   subBankName!: string;
 
   @Column({
-    name: 'bank_name',
-    type: 'varchar',
-    length: 50,
+    name: 'bank_name_id',
+    type: 'int',
     nullable: false,
-    comment: '银行名称'
+    comment: '银行名称ID,引用bank_name表'
   })
-  bankName!: string;
+  bankNameId!: number;
 
   @Column({
     name: 'card_number',
@@ -55,6 +55,15 @@ export class DisabledBankCard {
   })
   cardholderName!: string;
 
+  @Column({
+    name: 'card_type',
+    type: 'varchar',
+    length: 20,
+    nullable: true,
+    comment: '银行卡类型:一类卡/二类卡'
+  })
+  cardType!: string | null;
+
   @Column({
     name: 'file_id',
     type: 'int',
@@ -81,4 +90,9 @@ export class DisabledBankCard {
   @ManyToOne(() => File, { onDelete: 'CASCADE' })
   @JoinColumn({ name: 'file_id' })
   file!: File;
+
+  // 关系定义 - 银行名称
+  @ManyToOne(() => BankName, { onDelete: 'RESTRICT' })
+  @JoinColumn({ name: 'bank_name_id' })
+  bankName!: BankName;
 }

+ 90 - 3
allin-packages/disability-module/src/schemas/disabled-person.schema.ts

@@ -1,5 +1,6 @@
 import { z } from '@hono/zod-openapi';
 import { FileSchema } from '@d8d/file-module/schemas';
+import { BankNameSchema } from '@d8d/bank-names-module/schemas';
 
 // 基础字段定义
 const BaseDisabledPersonSchema = z.object({
@@ -279,9 +280,23 @@ export const DisabledBankCardSchema = z.object({
     description: '发卡支行',
     example: '中国工商银行北京分行'
   }),
-  bankName: z.string().max(50).openapi({
-    description: '银行名称',
-    example: '中国工商银行'
+  bankNameId: z.number().int().positive().openapi({
+    description: '银行名称ID',
+    example: 1
+  }),
+  bankName: BankNameSchema.openapi({
+    description: '银行名称实体信息',
+    example: {
+      id: 1,
+      name: '中国工商银行',
+      code: 'ICBC',
+      remark: '大型国有商业银行',
+      status: 1,
+      createdAt: '2024-01-01T00:00:00Z',
+      updatedAt: '2024-01-01T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1
+    }
   }),
   cardNumber: z.string().max(50).openapi({
     description: '卡号',
@@ -291,6 +306,10 @@ export const DisabledBankCardSchema = z.object({
     description: '持卡人姓名',
     example: '张三'
   }),
+  cardType: z.string().max(20).nullable().openapi({
+    description: '银行卡类型:一类卡/二类卡',
+    example: '一类卡'
+  }),
   fileId: z.number().int().positive().openapi({
     description: '银行卡照片文件ID',
     example: 1
@@ -318,6 +337,74 @@ export const DisabledBankCardSchema = z.object({
   })
 });
 
+// 创建银行卡DTO
+export const CreateDisabledBankCardSchema = z.object({
+  personId: z.number().int().positive().openapi({
+    description: '残疾人ID',
+    example: 1
+  }),
+  subBankName: z.string().max(100).openapi({
+    description: '发卡支行',
+    example: '中国工商银行北京分行'
+  }),
+  bankNameId: z.number().int().positive().openapi({
+    description: '银行名称ID',
+    example: 1
+  }),
+  cardNumber: z.string().max(50).openapi({
+    description: '卡号',
+    example: '6222021234567890123'
+  }),
+  cardholderName: z.string().max(50).openapi({
+    description: '持卡人姓名',
+    example: '张三'
+  }),
+  cardType: z.string().max(20).nullable().optional().openapi({
+    description: '银行卡类型:一类卡/二类卡',
+    example: '一类卡'
+  }),
+  fileId: z.number().int().positive().openapi({
+    description: '银行卡照片文件ID',
+    example: 1
+  }),
+  isDefault: z.number().int().min(0).max(1).default(0).optional().openapi({
+    description: '是否默认:1-是,0-否',
+    example: 1
+  })
+});
+
+// 更新银行卡DTO
+export const UpdateDisabledBankCardSchema = z.object({
+  subBankName: z.string().max(100).optional().openapi({
+    description: '发卡支行',
+    example: '中国工商银行北京分行'
+  }),
+  bankNameId: z.number().int().positive().optional().openapi({
+    description: '银行名称ID',
+    example: 1
+  }),
+  cardNumber: z.string().max(50).optional().openapi({
+    description: '卡号',
+    example: '6222021234567890123'
+  }),
+  cardholderName: z.string().max(50).optional().openapi({
+    description: '持卡人姓名',
+    example: '张三'
+  }),
+  cardType: z.string().max(20).nullable().optional().openapi({
+    description: '银行卡类型:一类卡/二类卡',
+    example: '一类卡'
+  }),
+  fileId: z.number().int().positive().optional().openapi({
+    description: '银行卡照片文件ID',
+    example: 1
+  }),
+  isDefault: z.number().int().min(0).max(1).optional().openapi({
+    description: '是否默认:1-是,0-否',
+    example: 1
+  })
+});
+
 // 照片实体Schema
 export const DisabledPhotoSchema = z.object({
   id: z.number().int().positive().openapi({

+ 1 - 0
allin-packages/disability-person-management-ui/package.json

@@ -39,6 +39,7 @@
     "@d8d/allin-enums": "workspace:*",
     "@d8d/area-management-ui": "workspace:*",
     "@d8d/file-management-ui": "workspace:*",
+    "@d8d/bank-name-management-ui": "workspace:*",
     "@d8d/shared-types": "workspace:*",
     "@d8d/shared-ui-components": "workspace:*",
     "@hookform/resolvers": "^5.2.1",

+ 53 - 8
allin-packages/disability-person-management-ui/src/components/BankCardManagement.tsx

@@ -4,15 +4,31 @@ import { Card, CardContent } from '@d8d/shared-ui-components/components/ui/card'
 import { Label } from '@d8d/shared-ui-components/components/ui/label';
 import { Input } from '@d8d/shared-ui-components/components/ui/input';
 import { Switch } from '@d8d/shared-ui-components/components/ui/switch';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@d8d/shared-ui-components/components/ui/select';
 import { Plus, Trash2, CreditCard } from 'lucide-react';
 import { FileSelector } from '@d8d/file-management-ui/components';
+import { BankSelect } from './BankSelect';
 import { toast } from 'sonner';
 
+// 银行卡类型枚举
+export enum BankCardType {
+  FIRST_CLASS = '一类卡',
+  SECOND_CLASS = '二类卡'
+}
+
 export interface BankCardItem {
   subBankName: string;
-  bankName: string;
+  bankNameId: number | null;
+  bankName?: string; // 保留用于向后兼容
   cardNumber: string;
   cardholderName: string;
+  cardType?: BankCardType | string | null;
   fileId: number | null;
   isDefault: number;
   tempId?: string; // 临时ID用于React key
@@ -44,9 +60,11 @@ export const BankCardManagement: React.FC<BankCardManagementProps> = ({
 
     const newBankCard: BankCardItem = {
       subBankName: '',
+      bankNameId: null,
       bankName: '',
       cardNumber: '',
       cardholderName: '',
+      cardType: null,
       fileId: null,
       isDefault: 0,
       tempId: `temp-${Date.now()}-${Math.random()}`,
@@ -81,6 +99,7 @@ export const BankCardManagement: React.FC<BankCardManagementProps> = ({
   };
 
   const handleFileIdChange = (index: number, fileId: number | null) => {
+    console.debug(`handleFileIdChange called: index=${index}, fileId=${fileId}`);
     const newBankCards = [...bankCards];
     newBankCards[index] = { ...newBankCards[index], fileId };
     setBankCards(newBankCards);
@@ -144,13 +163,18 @@ export const BankCardManagement: React.FC<BankCardManagementProps> = ({
 
                 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                   <div className="space-y-2">
-                    <Label htmlFor={`bankName-${index}`}>银行名称 *</Label>
-                    <Input
-                      id={`bankName-${index}`}
-                      value={card.bankName}
-                      onChange={(e) => handleFieldChange(index, 'bankName', e.target.value)}
-                      placeholder="例如:中国工商银行"
-                      data-testid={`bank-name-input-${index}`}
+                    <Label htmlFor={`bankNameId-${index}`}>银行名称 *</Label>
+                    <BankSelect
+                      value={card.bankNameId || undefined}
+                      onChange={(value) => handleFieldChange(index, 'bankNameId', value)}
+                      placeholder="请选择银行"
+                      testId={`bank-select-${index}`}
+                      allowCustomInput={true}
+                      onCustomInput={(customValue) => {
+                        // 处理自定义银行名称
+                        // 这里可以调用API创建新的银行名称,然后设置bankNameId
+                        toast.info(`自定义银行名称: ${customValue},需要先创建银行名称`);
+                      }}
                     />
                   </div>
 
@@ -193,11 +217,29 @@ export const BankCardManagement: React.FC<BankCardManagementProps> = ({
                     />
                   </div>
 
+                  <div className="space-y-2">
+                    <Label htmlFor={`cardType-${index}`}>银行卡类型</Label>
+                    <Select
+                      value={card.cardType || ''}
+                      onValueChange={(value) => handleFieldChange(index, 'cardType', value || null)}
+                    >
+                      <SelectTrigger id={`cardType-${index}`} data-testid={`card-type-select-${index}`}>
+                        <SelectValue placeholder="请选择银行卡类型" />
+                      </SelectTrigger>
+                      <SelectContent>
+                        <SelectItem value={BankCardType.FIRST_CLASS}>{BankCardType.FIRST_CLASS}</SelectItem>
+                        <SelectItem value={BankCardType.SECOND_CLASS}>{BankCardType.SECOND_CLASS}</SelectItem>
+                      </SelectContent>
+                    </Select>
+                  </div>
+
                   <div className="space-y-2">
                     <Label>银行卡照片</Label>
                     <FileSelector
                       value={card.fileId ? [card.fileId] : []}
                       onChange={(fileIds) => {
+                        console.debug('FileSelector onChange called with:', fileIds);
+                        // 修复文件ID绑定逻辑
                         if (Array.isArray(fileIds) && fileIds.length > 0) {
                           handleFileIdChange(index, fileIds[0]);
                         } else if (typeof fileIds === 'number') {
@@ -210,6 +252,9 @@ export const BankCardManagement: React.FC<BankCardManagementProps> = ({
                       data-testid={`bank-card-photo-upload-${index}`}
                     />
                     <p className="text-xs text-muted-foreground">请上传银行卡正面照片</p>
+                    {card.fileId && (
+                      <p className="text-xs text-green-600">已选择文件ID: {card.fileId}</p>
+                    )}
                   </div>
 
                   <div className="space-y-2 flex items-center">

+ 101 - 0
allin-packages/disability-person-management-ui/src/components/BankSelect.tsx

@@ -0,0 +1,101 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@d8d/shared-ui-components/components/ui/select';
+import { bankNameClientManager } from '@d8d/bank-name-management-ui/api';
+
+interface BankSelectProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+  className?: string;
+  testId?: string;
+  allowCustomInput?: boolean;
+  onCustomInput?: (value: string) => void;
+}
+
+export const BankSelect: React.FC<BankSelectProps> = ({
+  value,
+  onChange,
+  placeholder = "请选择银行",
+  disabled = false,
+  className,
+  testId = 'bank-select',
+  allowCustomInput = false,
+  onCustomInput,
+}) => {
+  const {
+    data: bankNames,
+    isLoading,
+    isError,
+  } = useQuery({
+    queryKey: ['bank-names'],
+    queryFn: async () => {
+      const client = bankNameClientManager.get()
+      const res = await client.index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          status: 1 // 只获取启用的银行名称
+        }
+      });
+      if (res.status !== 200) throw new Error('获取银行名称失败');
+      return await res.json();
+    },
+  });
+
+  if (isError) {
+    return (
+      <div className="text-sm text-destructive">
+        加载银行列表失败
+      </div>
+    );
+  }
+
+  const banks = bankNames?.data || [];
+
+  // 如果允许自定义输入,可以在这里添加自定义输入逻辑
+  // 参考省份选择组件的实现
+
+  return (
+    <Select
+      value={value?.toString()}
+      onValueChange={(val) => {
+        if (val === 'custom' && onCustomInput) {
+          // 处理自定义输入
+          const customValue = prompt('请输入银行名称:');
+          if (customValue && onCustomInput) {
+            onCustomInput(customValue);
+          }
+        } else {
+          onChange?.(parseInt(val));
+        }
+      }}
+      disabled={disabled || isLoading || banks.length === 0}
+    >
+      <SelectTrigger className={className} data-testid={testId}>
+        <SelectValue placeholder={isLoading ? '加载中...' : placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {banks.map((bank) => (
+          <SelectItem key={bank.id} value={bank.id.toString()}>
+            {bank.name}
+          </SelectItem>
+        ))}
+        {allowCustomInput && (
+          <SelectItem value="custom" className="text-muted-foreground">
+            + 自定义银行名称
+          </SelectItem>
+        )}
+      </SelectContent>
+    </Select>
+  );
+};
+
+export default BankSelect;

+ 1 - 0
allin-packages/disability-person-management-ui/src/components/index.ts

@@ -3,5 +3,6 @@ export { default as DisabledPersonSelector } from './DisabledPersonSelector';
 export { default as PhotoUploadField, type PhotoItem } from './PhotoUploadField';
 export { default as PhotoPreview } from './PhotoPreview';
 export { default as BankCardManagement, type BankCardItem } from './BankCardManagement';
+export { default as BankSelect } from './BankSelect';
 export { default as RemarkManagement, type RemarkItem } from './RemarkManagement';
 export { default as VisitManagement, type VisitItem } from './VisitManagement';

+ 499 - 0
allin-packages/disability-person-management-ui/tests/integration/bank-card-management.integration.test.tsx

@@ -0,0 +1,499 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { BankCardManagement, BankCardItem, BankCardType } from '../../src/components/BankCardManagement';
+import { toast } from 'sonner';
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+    warning: vi.fn(),
+  },
+}));
+
+// Mock 文件选择器组件
+vi.mock('@d8d/file-management-ui/components', () => ({
+  FileSelector: vi.fn(({ onChange, placeholder }) => (
+    <div data-testid="file-selector">
+      <button
+        data-testid="file-selector-button"
+        onClick={() => onChange && onChange(1)}
+      >
+        {placeholder || '选择文件'}
+      </button>
+    </div>
+  )),
+}));
+
+// Mock 银行名称管理UI API
+vi.mock('@d8d/bank-name-management-ui/api', () => ({
+  bankNameClientManager: {
+    get: vi.fn(() => ({
+      index: {
+        $get: vi.fn(() => Promise.resolve({
+          status: 200,
+          json: async () => ({
+            data: [
+              { id: 1, name: '中国工商银行', code: 'ICBC', remark: '工商银行', status: 1 },
+              { id: 2, name: '中国建设银行', code: 'CCB', remark: '建设银行', status: 1 },
+              { id: 3, name: '中国农业银行', code: 'ABC', remark: '农业银行', status: 1 },
+              { id: 4, name: '中国银行', code: 'BOC', remark: '中国银行', status: 1 },
+              { id: 5, name: '招商银行', code: 'CMB', remark: '招商银行', status: 1 },
+            ],
+            total: 5
+          })
+        }))
+      }
+    }))
+  }
+}));
+
+describe('银行卡管理组件集成测试', () => {
+  const mockOnChange = vi.fn();
+  let queryClient: QueryClient;
+
+  beforeEach(() => {
+    queryClient = new QueryClient({
+      defaultOptions: {
+        queries: {
+          retry: false,
+        },
+      },
+    });
+    vi.clearAllMocks();
+  });
+
+  const renderComponent = (props = {}) => {
+    return render(
+      <QueryClientProvider client={queryClient}>
+        <BankCardManagement
+          value={[]}
+          onChange={mockOnChange}
+          {...props}
+        />
+      </QueryClientProvider>
+    );
+  };
+
+  it('应该正确渲染银行卡管理组件', () => {
+    renderComponent();
+
+    // 验证标题
+    expect(screen.getByText('银行卡管理')).toBeInTheDocument();
+
+    // 验证添加按钮
+    expect(screen.getByText('添加银行卡')).toBeInTheDocument();
+    expect(screen.getByRole('button', { name: '添加银行卡' })).toBeInTheDocument();
+  });
+
+  it('应该添加新的银行卡', () => {
+    renderComponent();
+
+    // 点击添加银行卡按钮
+    const addButton = screen.getByTestId('add-bank-card-button');
+    fireEvent.click(addButton);
+
+    // 验证新的银行卡表单出现
+    expect(screen.getByText('发卡支行 *')).toBeInTheDocument();
+    expect(screen.getByText('银行名称 *')).toBeInTheDocument();
+    expect(screen.getByText('银行卡号 *')).toBeInTheDocument();
+    expect(screen.getByText('持卡人姓名 *')).toBeInTheDocument();
+    expect(screen.getByText('银行卡类型')).toBeInTheDocument();
+    expect(screen.getByText('银行卡照片')).toBeInTheDocument();
+    expect(screen.getByText('设为默认银行卡')).toBeInTheDocument();
+
+    // 验证onChange被调用
+    expect(mockOnChange).toHaveBeenCalled();
+    const callArgs = mockOnChange.mock.calls[0][0];
+    expect(callArgs).toHaveLength(1);
+    expect(callArgs[0]).toMatchObject({
+      subBankName: '',
+      bankNameId: null,
+      cardNumber: '',
+      cardholderName: '',
+      cardType: null,
+      fileId: null,
+      isDefault: 0,
+    });
+  });
+
+  it('应该填写银行卡信息', async () => {
+    renderComponent();
+
+    // 添加银行卡
+    const addButton = screen.getByTestId('add-bank-card-button');
+    fireEvent.click(addButton);
+
+    // 等待银行数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('bank-select-0')).toBeInTheDocument();
+    });
+
+    // 填写支行名称
+    const subBankNameInput = screen.getByTestId('sub-bank-name-input-0');
+    fireEvent.change(subBankNameInput, { target: { value: '北京分行朝阳支行' } });
+
+    // 选择银行 - 点击银行选择器
+    const bankSelectTrigger = screen.getByTestId('bank-select-0');
+    fireEvent.click(bankSelectTrigger);
+
+    // 等待银行选项出现
+    await waitFor(() => {
+      expect(screen.getByText('中国工商银行')).toBeInTheDocument();
+    });
+
+    // 选择中国工商银行
+    const bankOption = screen.getByText('中国工商银行');
+    fireEvent.click(bankOption);
+
+    // 填写银行卡号
+    const cardNumberInput = screen.getByTestId('card-number-input-0');
+    fireEvent.change(cardNumberInput, { target: { value: '6222021234567890123' } });
+
+    // 填写持卡人姓名
+    const cardholderNameInput = screen.getByTestId('cardholder-name-input-0');
+    fireEvent.change(cardholderNameInput, { target: { value: '张三' } });
+
+    // 选择银行卡类型 - 点击类型选择器
+    const cardTypeSelectTrigger = screen.getByTestId('card-type-select-0');
+    fireEvent.click(cardTypeSelectTrigger);
+
+    // 等待银行卡类型选项出现
+    await waitFor(() => {
+      expect(screen.getByText(BankCardType.FIRST_CLASS)).toBeInTheDocument();
+    });
+
+    // 选择一类卡
+    const cardTypeOption = screen.getByText(BankCardType.FIRST_CLASS);
+    fireEvent.click(cardTypeOption);
+
+    // 选择文件
+    const fileSelectorButton = screen.getByTestId('file-selector-button');
+    fireEvent.click(fileSelectorButton);
+
+    // 设置为默认银行卡
+    const defaultSwitch = screen.getByTestId('default-card-switch-0');
+    fireEvent.click(defaultSwitch);
+
+    // 验证所有字段都被正确更新
+    const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
+    expect(lastCall[0]).toMatchObject({
+      subBankName: '北京分行朝阳支行',
+      bankNameId: 1, // 中国工商银行的ID
+      cardNumber: '6222021234567890123',
+      cardholderName: '张三',
+      cardType: BankCardType.FIRST_CLASS,
+      fileId: 1,
+      isDefault: 1,
+    });
+  });
+
+  it('应该删除银行卡', () => {
+    const initialCards: BankCardItem[] = [
+      {
+        subBankName: '北京分行',
+        bankNameId: 1,
+        cardNumber: '6222021234567890123',
+        cardholderName: '张三',
+        cardType: BankCardType.FIRST_CLASS,
+        fileId: 1,
+        isDefault: 1,
+        tempId: 'temp-1',
+      },
+      {
+        subBankName: '上海分行',
+        bankNameId: 2,
+        cardNumber: '6222029876543210987',
+        cardholderName: '李四',
+        cardType: BankCardType.SECOND_CLASS,
+        fileId: 2,
+        isDefault: 0,
+        tempId: 'temp-2',
+      },
+    ];
+
+    renderComponent({ value: initialCards });
+
+    // 验证有两张银行卡
+    expect(screen.getAllByText('发卡支行 *')).toHaveLength(2);
+    expect(screen.getByTestId('remove-bank-card-0')).toBeInTheDocument();
+    expect(screen.getByTestId('remove-bank-card-1')).toBeInTheDocument();
+
+    // 删除第一张银行卡
+    const deleteButton = screen.getByTestId('remove-bank-card-0');
+    fireEvent.click(deleteButton);
+
+    // 验证onChange被调用,且只剩一张银行卡
+    expect(mockOnChange).toHaveBeenCalled();
+    const callArgs = mockOnChange.mock.calls[0][0];
+    expect(callArgs).toHaveLength(1);
+    expect(callArgs[0].subBankName).toBe('上海分行');
+  });
+
+  it('应该限制最大银行卡数量', () => {
+    const maxCards = 3;
+    renderComponent({ maxCards });
+
+    // 添加最大数量的银行卡
+    const addButton = screen.getByTestId('add-bank-card-button');
+    for (let i = 0; i < maxCards; i++) {
+      fireEvent.click(addButton);
+    }
+
+    // 验证可以添加maxCards张银行卡
+    expect(screen.getAllByText('发卡支行 *')).toHaveLength(maxCards);
+
+    // 验证按钮被禁用
+    expect(addButton).toBeDisabled();
+
+    // 尝试添加超过限制的银行卡
+    fireEvent.click(addButton);
+
+    // 验证toast警告被调用(按钮被禁用时可能不会触发)
+    // expect(toast.warning).toHaveBeenCalledWith(`最多只能添加 ${maxCards} 张银行卡`);
+
+    // 验证银行卡数量没有增加
+    expect(screen.getAllByText('发卡支行 *')).toHaveLength(maxCards);
+  });
+
+  it('应该只能设置一张默认银行卡', () => {
+    const initialCards: BankCardItem[] = [
+      {
+        subBankName: '北京分行',
+        bankNameId: 1,
+        cardNumber: '6222021234567890123',
+        cardholderName: '张三',
+        cardType: BankCardType.FIRST_CLASS,
+        fileId: 1,
+        isDefault: 1, // 第一张是默认卡
+        tempId: 'temp-1',
+      },
+      {
+        subBankName: '上海分行',
+        bankNameId: 2,
+        cardNumber: '6222029876543210987',
+        cardholderName: '李四',
+        cardType: BankCardType.SECOND_CLASS,
+        fileId: 2,
+        isDefault: 0, // 第二张不是默认卡
+        tempId: 'temp-2',
+      },
+    ];
+
+    renderComponent({ value: initialCards });
+
+    // 获取所有默认开关
+    const defaultSwitches = screen.getAllByTestId(/default-card-switch-/);
+    expect(defaultSwitches).toHaveLength(2);
+
+    // 第一张卡应该是默认卡(开关应该被选中)
+    const firstSwitch = defaultSwitches[0];
+    expect(firstSwitch).toBeChecked();
+
+    // 第二张卡不是默认卡(开关不应该被选中)
+    const secondSwitch = defaultSwitches[1];
+    expect(secondSwitch).not.toBeChecked();
+
+    // 将第二张卡设置为默认卡
+    fireEvent.click(secondSwitch);
+
+    // 验证onChange被调用,且只有第二张卡是默认卡
+    const callArgs = mockOnChange.mock.calls[0][0];
+    expect(callArgs[0].isDefault).toBe(0); // 第一张卡不再是默认卡
+    expect(callArgs[1].isDefault).toBe(1); // 第二张卡现在是默认卡
+  });
+
+  it('应该测试银行选择组件集成', async () => {
+    renderComponent();
+
+    // 添加银行卡
+    const addButton = screen.getByTestId('add-bank-card-button');
+    fireEvent.click(addButton);
+
+    // 等待银行数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('bank-select-0')).toBeInTheDocument();
+    });
+
+    // 验证银行选择器存在
+    const bankSelectTrigger = screen.getByTestId('bank-select-0');
+    expect(bankSelectTrigger).toBeInTheDocument();
+
+    // 等待Select组件的禁用状态被解开(数据加载完成)
+    await waitFor(() => {
+      expect(bankSelectTrigger).not.toHaveAttribute('disabled');
+    });
+
+    // 检查Select组件的aria-expanded属性
+    expect(bankSelectTrigger).toHaveAttribute('aria-expanded', 'false');
+
+    // 使用fireEvent点击银行选择器
+    fireEvent.click(bankSelectTrigger);
+
+    // 等待Select组件打开(aria-expanded变为true)
+    await waitFor(() => {
+      expect(bankSelectTrigger).toHaveAttribute('aria-expanded', 'true');
+    });
+
+    // 等待银行选项出现
+    await waitFor(() => {
+      expect(screen.getByText('中国农业银行')).toBeInTheDocument();
+    });
+
+    // 查找隐藏的select元素并直接设置值(参考area-select-form测试)
+    const hiddenSelect = bankSelectTrigger.parentElement?.querySelector('select[aria-hidden="true"]');
+    if (hiddenSelect) {
+      await userEvent.selectOptions(hiddenSelect, '3'); // 选择中国农业银行(ID为3)
+    } else {
+      // 如果没有隐藏的select,尝试直接点击选项
+      const bankOption = screen.getByText('中国农业银行');
+      fireEvent.click(bankOption);
+    }
+
+    // 验证银行ID被正确设置
+    const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
+    expect(lastCall[0].bankNameId).toBe(3);
+  });
+
+  it('应该测试银行卡类型选择', async () => {
+    renderComponent();
+
+    // 添加银行卡
+    const addButton = screen.getByTestId('add-bank-card-button');
+    fireEvent.click(addButton);
+
+    // 验证银行卡类型选择器存在
+    const cardTypeSelect = screen.getByTestId('card-type-select-0');
+    expect(cardTypeSelect).toBeInTheDocument();
+
+    // 点击银行卡类型选择器
+    fireEvent.click(cardTypeSelect);
+
+    // 等待银行卡类型选项出现
+    await waitFor(() => {
+      expect(screen.getByText(BankCardType.FIRST_CLASS)).toBeInTheDocument();
+      expect(screen.getByText(BankCardType.SECOND_CLASS)).toBeInTheDocument();
+    });
+
+    // 选择一类卡
+    const firstClassOption = screen.getByText(BankCardType.FIRST_CLASS);
+    fireEvent.click(firstClassOption);
+
+    // 验证一类卡被选中
+    const lastCall1 = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
+    expect(lastCall1[0].cardType).toBe(BankCardType.FIRST_CLASS);
+
+    // 再次点击选择器选择二类卡
+    fireEvent.click(cardTypeSelect);
+    await waitFor(() => {
+      expect(screen.getByText(BankCardType.SECOND_CLASS)).toBeInTheDocument();
+    });
+
+    // 选择二类卡
+    const secondClassOption = screen.getByText(BankCardType.SECOND_CLASS);
+    fireEvent.click(secondClassOption);
+
+    // 验证二类卡被选中
+    const lastCall2 = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
+    expect(lastCall2[0].cardType).toBe(BankCardType.SECOND_CLASS);
+  });
+
+  it('应该测试文件选择器集成', () => {
+    renderComponent();
+
+    // 添加银行卡
+    const addButton = screen.getByText('添加银行卡');
+    fireEvent.click(addButton);
+
+    // 验证文件选择器存在
+    expect(screen.getByTestId('file-selector')).toBeInTheDocument();
+    expect(screen.getByTestId('file-selector-button')).toBeInTheDocument();
+
+    // 点击文件选择器按钮
+    const fileSelectorButton = screen.getByTestId('file-selector-button');
+    fireEvent.click(fileSelectorButton);
+
+    // 验证文件ID被正确设置
+    const lastCall = mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1][0];
+    expect(lastCall[0].fileId).toBe(1);
+  });
+
+  it('应该测试初始值渲染', () => {
+    const initialCards: BankCardItem[] = [
+      {
+        subBankName: '测试支行',
+        bankNameId: 1,
+        cardNumber: '6222021234567890123',
+        cardholderName: '测试用户',
+        cardType: BankCardType.FIRST_CLASS,
+        fileId: 123,
+        isDefault: 1,
+        tempId: 'test-1',
+      },
+    ];
+
+    renderComponent({ value: initialCards });
+
+    // 验证初始值被正确渲染
+    const subBankNameInput = screen.getByPlaceholderText('例如:北京分行朝阳支行') as HTMLInputElement;
+    const cardNumberInput = screen.getByPlaceholderText('例如:6222 0212 3456 7890') as HTMLInputElement;
+    const cardholderNameInput = screen.getByPlaceholderText('请输入持卡人姓名') as HTMLInputElement;
+
+    expect(subBankNameInput.value).toBe('测试支行');
+    expect(cardNumberInput.value).toBe('6222 0212 3456 7890 123');
+    expect(cardholderNameInput.value).toBe('测试用户');
+
+    // 验证银行选择器存在
+    expect(screen.getByTestId('bank-select-0')).toBeInTheDocument();
+
+    // 验证银行卡类型选择器存在
+    expect(screen.getByTestId('card-type-select-0')).toBeInTheDocument();
+    // 注意:Select组件的值在测试环境中可能无法直接获取
+    // 我们只验证组件存在
+
+    // 验证默认开关被选中
+    const defaultSwitch = screen.getByTestId('default-card-switch-0');
+    expect(defaultSwitch).toBeChecked();
+  });
+
+  it('应该测试外部value变化同步', async () => {
+    const { rerender } = renderComponent();
+
+    // 初始状态没有银行卡
+    expect(screen.queryByText('发卡支行 *')).not.toBeInTheDocument();
+
+    // 更新props添加银行卡
+    const newCards: BankCardItem[] = [
+      {
+        subBankName: '新支行',
+        bankNameId: 2,
+        cardNumber: '6222029999999999999',
+        cardholderName: '新用户',
+        cardType: BankCardType.SECOND_CLASS,
+        fileId: 456,
+        isDefault: 1,
+        tempId: 'new-1',
+      },
+    ];
+
+    rerender(
+      <QueryClientProvider client={queryClient}>
+        <BankCardManagement
+          value={newCards}
+          onChange={mockOnChange}
+        />
+      </QueryClientProvider>
+    );
+
+    // 验证新银行卡被渲染
+    await waitFor(() => {
+      expect(screen.getByText('发卡支行 *')).toBeInTheDocument();
+    });
+
+    const subBankNameInput = screen.getByPlaceholderText('例如:北京分行朝阳支行') as HTMLInputElement;
+    expect(subBankNameInput.value).toBe('新支行');
+  });
+});

+ 33 - 2
allin-packages/disability-person-management-ui/tests/setup.ts

@@ -1,8 +1,39 @@
 import '@testing-library/jest-dom';
-import { afterEach } from 'vitest';
+import { afterEach, vi } from 'vitest';
 import { cleanup } from '@testing-library/react';
 
 // 在每个测试后清理
 afterEach(() => {
   cleanup();
-});
+});
+
+// Mock window.matchMedia
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: vi.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: vi.fn(),
+    removeListener: vi.fn(),
+    addEventListener: vi.fn(),
+    removeEventListener: vi.fn(),
+    dispatchEvent: vi.fn(),
+  })),
+});
+
+// Mock ResizeObserver - 需要调用callback以便Select组件正常工作
+global.ResizeObserver = class ResizeObserver {
+  cb: any;
+  constructor(cb: any) {
+    this.cb = cb;
+  }
+  observe() {
+    this.cb([{ borderBoxSize: { inlineSize: 0, blockSize: 0 } }]);
+  }
+  unobserve() {}
+  disconnect() {}
+};
+
+// Mock Element.scrollIntoView 避免shadcn/ui Select组件错误
+Element.prototype.scrollIntoView = vi.fn();

+ 1 - 1
docs/stories/009.004.bank-card-management-optimization.story.md

@@ -1,7 +1,7 @@
 # Story 009.004: 银行卡管理优化
 
 ## Status
-Draft
+Ready for Development
 
 ## Story
 **As a** 残疾人信息管理员

+ 36 - 0
packages/bank-name-management-ui/eslint.config.js

@@ -0,0 +1,36 @@
+import tseslint from '@typescript-eslint/eslint-plugin';
+import tsparser from '@typescript-eslint/parser';
+
+export default [
+  {
+    files: ['**/*.{ts,tsx}'],
+    ignores: ['dist/**', 'node_modules/**', 'coverage/**'],
+    languageOptions: {
+      parser: tsparser,
+      ecmaVersion: 'latest',
+      sourceType: 'module',
+      parserOptions: {
+        ecmaFeatures: {
+          jsx: true,
+        },
+      },
+    },
+    plugins: {
+      '@typescript-eslint': tseslint,
+    },
+    rules: {
+      ...tseslint.configs.recommended.rules,
+
+      // TypeScript specific rules
+      '@typescript-eslint/no-unused-vars': 'error',
+      '@typescript-eslint/no-explicit-any': 'warn',
+      '@typescript-eslint/explicit-function-return-type': 'off',
+      '@typescript-eslint/explicit-module-boundary-types': 'off',
+
+      // General rules
+      'no-console': 'warn',
+      'prefer-const': 'error',
+      'no-var': 'error',
+    },
+  },
+];

+ 93 - 0
packages/bank-name-management-ui/package.json

@@ -0,0 +1,93 @@
+{
+  "name": "@d8d/bank-name-management-ui",
+  "version": "1.0.0",
+  "description": "银行名称管理界面包 - 提供银行名称管理的完整前端界面,包括银行名称CRUD操作、状态管理、搜索过滤等功能",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./components": {
+      "types": "./src/components/index.ts",
+      "import": "./src/components/index.ts",
+      "require": "./src/components/index.ts"
+    },
+    "./hooks": {
+      "types": "./src/hooks/index.ts",
+      "import": "./src/hooks/index.ts",
+      "require": "./src/hooks/index.ts"
+    },
+    "./api": {
+      "types": "./src/api/index.ts",
+      "import": "./src/api/index.ts",
+      "require": "./src/api/index.ts"
+    }
+  },
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "build": "unbuild",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/bank-names-module": "workspace:*",
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.12",
+    "axios": "^1.7.9",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "date-fns": "^4.1.0",
+    "dayjs": "^1.11.13",
+    "hono": "^4.8.5",
+    "lucide-react": "^0.536.0",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-hook-form": "^7.61.1",
+    "react-router": "^7.1.3",
+    "sonner": "^2.0.7",
+    "tailwind-merge": "^3.3.1",
+    "zod": "^4.0.15"
+  },
+  "devDependencies": {
+    "@testing-library/jest-dom": "^6.8.0",
+    "@testing-library/react": "^16.3.0",
+    "@testing-library/user-event": "^14.6.1",
+    "@types/node": "^22.10.2",
+    "@types/react": "^19.2.2",
+    "@types/react-dom": "^19.2.3",
+    "@typescript-eslint/eslint-plugin": "^8.18.1",
+    "@typescript-eslint/parser": "^8.18.1",
+    "eslint": "^9.17.0",
+    "jsdom": "^26.0.0",
+    "typescript": "^5.8.3",
+    "unbuild": "^3.4.0",
+    "vitest": "^4.0.9"
+  },
+  "peerDependencies": {
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0"
+  },
+  "keywords": [
+    "bank",
+    "bank-name",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 44 - 0
packages/bank-name-management-ui/src/api/bankNameClient.ts

@@ -0,0 +1,44 @@
+import { bankNameRoutes } from '@d8d/bank-names-module';
+import { rpcClient } from '@d8d/shared-ui-components/utils/hc'
+
+class BankNameClientManager {
+  private static instance: BankNameClientManager;
+  private client: ReturnType<typeof rpcClient<typeof bankNameRoutes>> | null = null;
+
+  private constructor() {}
+
+  public static getInstance(): BankNameClientManager {
+    if (!BankNameClientManager.instance) {
+      BankNameClientManager.instance = new BankNameClientManager();
+    }
+    return BankNameClientManager.instance;
+  }
+
+  // 初始化客户端
+  public init(baseUrl: string = '/'): ReturnType<typeof rpcClient<typeof bankNameRoutes>> {
+    return this.client = rpcClient<typeof bankNameRoutes>(baseUrl);
+  }
+
+  // 获取客户端实例
+  public get(): ReturnType<typeof rpcClient<typeof bankNameRoutes>> {
+    if (!this.client) {
+      return this.init()
+    }
+    return this.client;
+  }
+
+  // 重置客户端(用于测试或重新初始化)
+  public reset(): void {
+    this.client = null;
+  }
+}
+
+// 导出单例实例
+const bankNameClientManager = BankNameClientManager.getInstance();
+
+// 导出默认客户端实例(延迟初始化)
+export const bankNameClient = bankNameClientManager.get()
+
+export {
+  bankNameClientManager
+}

+ 3 - 0
packages/bank-name-management-ui/src/api/index.ts

@@ -0,0 +1,3 @@
+// API客户端导出入口
+
+export { bankNameClient, bankNameClientManager } from './bankNameClient';

+ 586 - 0
packages/bank-name-management-ui/src/components/BankNameManagement.tsx

@@ -0,0 +1,586 @@
+import { useState } from 'react'
+import { useQuery, useMutation } from '@tanstack/react-query'
+import { Plus, Search, Edit, Trash2 } from 'lucide-react'
+import { format } from 'date-fns'
+import { useForm } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { toast } from 'sonner'
+
+import { Button } from '@d8d/shared-ui-components/components/ui/button'
+import { Input } from '@d8d/shared-ui-components/components/ui/input'
+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 { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog'
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form'
+import { Switch } from '@d8d/shared-ui-components/components/ui/switch'
+import { Textarea } from '@d8d/shared-ui-components/components/ui/textarea'
+import { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton'
+
+import { bankNameClientManager } from '../api/bankNameClient'
+import type { BankName, BankNameFormData, BankNameQueryParams } from '../types/bankName'
+import { CreateBankNameDto, UpdateBankNameDto } from '@d8d/bank-names-module/schemas'
+
+// 表单Schema直接使用后端定义
+const createFormSchema = CreateBankNameDto
+const updateFormSchema = UpdateBankNameDto
+
+export const BankNameManagement = () => {
+  // 状态管理
+  const [searchParams, setSearchParams] = useState<BankNameQueryParams>({ page: 1, limit: 10, search: '' })
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [editingType, setEditingType] = useState<BankName | null>(null)
+  const [isCreateForm, setIsCreateForm] = useState(true)
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
+  const [typeToDelete, setTypeToDelete] = useState<number | null>(null)
+
+  // 表单实例
+  const createForm = useForm({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      name: '',
+      code: '',
+      remark: '',
+      status: 1
+    }
+  })
+
+  const updateForm = useForm({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {
+      name: '',
+      code: '',
+      remark: '',
+      status: 1
+    }
+  })
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['advertisement-types', searchParams],
+    queryFn: async () => {
+      const client = bankNameClientManager.get()
+      const res = await client.index.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      })
+      if (res.status !== 200) throw new Error('获取银行名称列表失败')
+      return await res.json()
+    }
+  })
+
+  // 创建mutation
+  const createMutation = useMutation({
+    mutationFn: async (data: BankNameFormData) => {
+      const client = bankNameClientManager.get()
+      const res = await client.index.$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 || '创建失败')
+    }
+  })
+
+  // 更新mutation
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: BankNameFormData }) => {
+      const client = bankNameClientManager.get()
+      const res = await client[':id']['$put']({
+        param: { id: id },
+        json: data
+      })
+      if (res.status !== 200) throw new Error('更新失败')
+      return await res.json()
+    },
+    onSuccess: () => {
+      toast.success('银行名称更新成功')
+      setIsModalOpen(false)
+      setEditingType(null)
+      refetch()
+    },
+    onError: (error) => {
+      toast.error(error.message || '更新失败')
+    }
+  })
+
+  // 删除mutation
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const client = bankNameClientManager.get()
+      const res = await client[':id']['$delete']({
+        param: { id: id }
+      })
+      if (res.status !== 204) throw new Error('删除失败')
+      return await res.json()
+    },
+    onSuccess: () => {
+      toast.success('银行名称删除成功')
+      setDeleteDialogOpen(false)
+      setTypeToDelete(null)
+      refetch()
+    },
+    onError: (error) => {
+      toast.error(error.message || '删除失败')
+    }
+  })
+
+  // 业务逻辑函数
+  const handleSearch = () => {
+    setSearchParams(prev => ({ ...prev, page: 1 }))
+  }
+
+  const handleCreateType = () => {
+    setIsCreateForm(true)
+    setEditingType(null)
+    createForm.reset({
+      name: '',
+      code: '',
+      remark: '',
+      status: 1
+    })
+    setIsModalOpen(true)
+  }
+
+  const handleEditType = (type: AdvertisementType) => {
+    setIsCreateForm(false)
+    setEditingType(type)
+    updateForm.reset({
+      name: type.name,
+      code: type.code,
+      remark: type.remark || '',
+      status: type.status
+    })
+    setIsModalOpen(true)
+  }
+
+  const handleDeleteType = (id: number) => {
+    setTypeToDelete(id)
+    setDeleteDialogOpen(true)
+  }
+
+  const confirmDelete = () => {
+    if (typeToDelete) {
+      deleteMutation.mutate(typeToDelete)
+    }
+  }
+
+  const handleCreateSubmit = async (data: any) => {
+    try {
+      createMutation.mutate(data)
+    } catch (error) {
+      toast.error('创建失败,请重试')
+    }
+  }
+
+  const handleUpdateSubmit = async (data: any) => {
+    if (!editingType) return
+
+    try {
+      updateMutation.mutate({ id: editingType.id, data })
+    } catch (error) {
+      toast.error('更新失败,请重试')
+    }
+  }
+
+  // 格式化时间
+  const formatDate = (date: string | null) => {
+    if (!date) return '-'
+    return format(new Date(date), 'yyyy-MM-dd HH:mm')
+  }
+
+  if (isLoading) {
+    return (
+      <div className="space-y-4">
+        <div className="flex justify-between items-center">
+          <div>
+            <h1 className="text-2xl font-bold" data-testid="page-title">银行名称管理</h1>
+            <p className="text-muted-foreground" data-testid="page-description">管理银行名称配置,用于广告位分类</p>
+          </div>
+          <Button disabled data-testid="create-type-button">
+            <Plus className="mr-2 h-4 w-4" />
+            创建类型
+          </Button>
+        </div>
+
+        <Card>
+          <CardHeader>
+            <Skeleton className="h-6 w-1/4" />
+          </CardHeader>
+          <CardContent>
+            <div className="space-y-2">
+              <Skeleton className="h-4 w-full" />
+              <Skeleton className="h-4 w-full" />
+              <Skeleton className="h-4 w-full" />
+            </div>
+          </CardContent>
+        </Card>
+      </div>
+    )
+  }
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <div>
+          <h1 className="text-2xl font-bold" data-testid="page-title">银行名称管理</h1>
+          <p className="text-muted-foreground" data-testid="page-description">管理银行名称配置,用于广告位分类</p>
+        </div>
+        <Button onClick={handleCreateType} data-testid="create-type-button">
+          <Plus className="mr-2 h-4 w-4" />
+          创建类型
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>银行名称列表</CardTitle>
+          <CardDescription>管理所有银行名称配置</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={(e) => { e.preventDefault(); 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"
+                  data-testid="search-input"
+                />
+              </div>
+              <Button type="submit" variant="outline" data-testid="search-button">
+                搜索
+              </Button>
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <Table>
+              <TableHeader>
+                <TableRow>
+                  <TableHead>ID</TableHead>
+                  <TableHead>类型名称</TableHead>
+                  <TableHead>调用别名</TableHead>
+                  <TableHead>状态</TableHead>
+                  <TableHead>创建时间</TableHead>
+                  <TableHead className="text-right">操作</TableHead>
+                </TableRow>
+              </TableHeader>
+              <TableBody>
+                {data?.data.map((type) => (
+                  <TableRow key={type.id} data-testid={`type-row-${type.id}`}>
+                    <TableCell className="font-medium">{type.id}</TableCell>
+                    <TableCell>{type.name}</TableCell>
+                    <TableCell>
+                      <code className="text-xs bg-muted px-2 py-1 rounded">{type.code}</code>
+                    </TableCell>
+                    <TableCell>
+                      <Badge variant={type.status === 1 ? 'default' : 'secondary'}>
+                        {type.status === 1 ? '启用' : '禁用'}
+                      </Badge>
+                    </TableCell>
+                    <TableCell className="text-sm">
+                      {formatDate(type.createdAt)}
+                    </TableCell>
+                    <TableCell className="text-right">
+                      <div className="flex justify-end gap-2">
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleEditType(type)}
+                          data-testid={`edit-button-${type.id}`}
+                        >
+                          <Edit className="h-4 w-4" />
+                        </Button>
+                        <Button
+                          variant="ghost"
+                          size="icon"
+                          onClick={() => handleDeleteType(type.id)}
+                          data-testid={`delete-button-${type.id}`}
+                        >
+                          <Trash2 className="h-4 w-4" />
+                        </Button>
+                      </div>
+                    </TableCell>
+                  </TableRow>
+                ))}
+              </TableBody>
+            </Table>
+          </div>
+
+          {data?.data.length === 0 && !isLoading && (
+            <div className="text-center py-8">
+              <p className="text-muted-foreground">暂无银行名称数据</p>
+            </div>
+          )}
+
+          {/* 分页组件 - 需要从共享UI组件包中导入或自行实现 */}
+          {data?.pagination && (
+            <div className="mt-4 flex items-center justify-between">
+              <div className="text-sm text-muted-foreground">
+                共 {data.pagination.total} 条记录
+              </div>
+              <div className="flex items-center space-x-2">
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={() => setSearchParams(prev => ({ ...prev, page: (prev.page || 1) - 1 }))}
+                  disabled={(searchParams.page || 1) <= 1}
+                  data-testid="prev-page-button"
+                >
+                  上一页
+                </Button>
+                <span className="text-sm" data-testid="current-page-info">
+                  第 {searchParams.page} 页
+                </span>
+                <Button
+                  variant="outline"
+                  size="sm"
+                  onClick={() => setSearchParams(prev => ({ ...prev, page: (prev.page || 1) + 1 }))}
+                  disabled={(searchParams.page || 1) >= Math.ceil(data.pagination.total / (searchParams.limit || 10))}
+                  data-testid="next-page-button"
+                >
+                  下一页
+                </Button>
+              </div>
+            </div>
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 创建/编辑模态框 */}
+      <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[500px] max-h-[90vh] overflow-y-auto" data-testid="type-modal">
+          <DialogHeader>
+            <DialogTitle data-testid="modal-title">{isCreateForm ? '创建银行名称' : '编辑银行名称'}</DialogTitle>
+            <DialogDescription data-testid="modal-description">
+              {isCreateForm ? '创建一个新的银行名称配置' : '编辑现有银行名称信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            // 创建表单(独立渲染)
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        类型名称 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入类型名称" {...field} data-testid="create-name-input" />
+                      </FormControl>
+                      <FormDescription>例如:首页轮播、侧边广告等</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        调用别名 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入调用别名" {...field} data-testid="create-code-input" />
+                      </FormControl>
+                      <FormDescription>用于程序调用的唯一标识,建议使用英文小写和下划线</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="remark"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>备注</FormLabel>
+                      <FormControl>
+                        <Textarea
+                          placeholder="请输入备注信息(可选)"
+                          className="resize-none"
+                          {...field}
+                          data-testid="create-remark-textarea"
+                        />
+                      </FormControl>
+                      <FormDescription>对银行名称的详细描述</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">启用状态</FormLabel>
+                        <FormDescription>
+                          禁用后该类型下的广告将无法正常展示
+                        </FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value === 1}
+                          onCheckedChange={field.onChange}
+                          data-testid="create-status-switch"
+                        />
+                      </FormControl>
+                    </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(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="name"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        类型名称 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入类型名称" {...field} data-testid="edit-name-input" />
+                      </FormControl>
+                      <FormDescription>例如:首页轮播、侧边广告等</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="code"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        调用别名 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入调用别名" {...field} data-testid="edit-code-input" />
+                      </FormControl>
+                      <FormDescription>用于程序调用的唯一标识,建议使用英文小写和下划线</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="remark"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>备注</FormLabel>
+                      <FormControl>
+                        <Textarea
+                          placeholder="请输入备注信息(可选)"
+                          className="resize-none"
+                          {...field}
+                          data-testid="edit-remark-textarea"
+                        />
+                      </FormControl>
+                      <FormDescription>对银行名称的详细描述</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
+                      <div className="space-y-0.5">
+                        <FormLabel className="text-base">启用状态</FormLabel>
+                        <FormDescription>
+                          禁用后该类型下的广告将无法正常展示
+                        </FormDescription>
+                      </div>
+                      <FormControl>
+                        <Switch
+                          checked={field.value === 1}
+                          onCheckedChange={field.onChange}
+                          data-testid="edit-status-switch"
+                        />
+                      </FormControl>
+                    </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 data-testid="delete-dialog">
+          <DialogHeader>
+            <DialogTitle data-testid="delete-dialog-title">确认删除</DialogTitle>
+            <DialogDescription data-testid="delete-dialog-description">
+              确定要删除这个银行名称吗?此操作无法撤销。
+              <br />
+              <span className="text-destructive">
+                注意:删除后,该类型下的所有广告将失去类型关联。
+              </span>
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)} data-testid="delete-cancel-button">
+              取消
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={confirmDelete}
+              disabled={deleteMutation.isPending}
+              data-testid="delete-confirm-button"
+            >
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  )
+}

+ 78 - 0
packages/bank-name-management-ui/src/components/BankNameSelector.tsx

@@ -0,0 +1,78 @@
+import React from 'react';
+import { useQuery } from '@tanstack/react-query';
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@d8d/shared-ui-components/components/ui/select';
+import { bankNameClientManager } from '../api/bankNameClient';
+
+interface BankNameSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+  className?: string;
+  testId?: string;
+}
+
+export const BankNameSelector: React.FC<BankNameSelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "请选择银行名称",
+  disabled = false,
+  className,
+  testId,
+}) => {
+  const {
+    data: bankNames,
+    isLoading,
+    isError,
+  } = useQuery({
+    queryKey: ['bank-names'],
+    queryFn: async () => {
+      const client = bankNameClientManager.get()
+      const res = await client.index.$get({
+        query: {
+          page: 1,
+          pageSize: 100
+        }
+      });
+      if (res.status !== 200) throw new Error('获取银行名称失败');
+      return await res.json();
+    },
+  });
+
+  if (isError) {
+    return (
+      <div className="text-sm text-destructive">
+        加载银行名称失败
+      </div>
+    );
+  }
+
+  const types = bankNames?.data || [];
+
+  return (
+    <Select
+      value={value?.toString()}
+      onValueChange={(val) => onChange?.(parseInt(val))}
+      disabled={disabled || isLoading || types.length === 0}
+    >
+      <SelectTrigger className={className} data-testid={testId}>
+        <SelectValue placeholder={isLoading ? '加载中...' : placeholder} />
+      </SelectTrigger>
+      <SelectContent>
+        {types.map((type) => (
+          <SelectItem key={type.id} value={type.id.toString()}>
+            {type.name}
+          </SelectItem>
+        ))}
+      </SelectContent>
+    </Select>
+  );
+};
+
+export default BankNameSelector;

+ 4 - 0
packages/bank-name-management-ui/src/components/index.ts

@@ -0,0 +1,4 @@
+// 组件导出入口
+
+export { BankNameManagement } from './BankNameManagement';
+export { BankNameSelector } from './BankNameSelector';

+ 19 - 0
packages/bank-name-management-ui/src/index.ts

@@ -0,0 +1,19 @@
+// 主包导出入口
+
+export { BankNameManagement, BankNameSelector } from './components';
+
+export { bankNameClient, bankNameClientManager } from './api/bankNameClient';
+
+export type {
+  BankName,
+  BankNameFormData,
+  BankNameQueryParams,
+  CreateBankNameRequest,
+  CreateBankNameResponse,
+  UpdateBankNameRequest,
+  UpdateBankNameResponse,
+  ListBankNamesResponse,
+  GetBankNameResponse,
+  DeleteBankNameResponse,
+  BankNameStatus
+} from './types/bankName';

+ 58 - 0
packages/bank-name-management-ui/src/types/bankName.ts

@@ -0,0 +1,58 @@
+import { InferRequestType, InferResponseType } from 'hono';
+import { bankNameRoutes } from '@d8d/bank-names-module';
+
+// 银行名称实体类型
+export interface BankName {
+  id: number;
+  name: string;
+  code: string;
+  remark: string | null;
+  status: number;
+  createdAt: string;
+  updatedAt: string;
+  createdBy: number | null;
+  updatedBy: number | null;
+}
+
+// 银行名称创建请求类型
+export type CreateBankNameRequest = InferRequestType<typeof bankNameRoutes>['post'];
+
+// 银行名称创建响应类型
+export type CreateBankNameResponse = InferResponseType<typeof bankNameRoutes>['post'];
+
+// 银行名称更新请求类型
+export type UpdateBankNameRequest = InferRequestType<typeof bankNameRoutes>['put'];
+
+// 银行名称更新响应类型
+export type UpdateBankNameResponse = InferResponseType<typeof bankNameRoutes>['put'];
+
+// 银行名称列表响应类型
+export type ListBankNamesResponse = InferResponseType<typeof bankNameRoutes>['get'];
+
+// 银行名称详情响应类型
+export type GetBankNameResponse = InferResponseType<typeof bankNameRoutes>['/:id']['get'];
+
+// 银行名称删除响应类型
+export type DeleteBankNameResponse = InferResponseType<typeof bankNameRoutes>['/:id']['delete'];
+
+// 银行名称状态枚举
+export enum BankNameStatus {
+  DISABLED = 0,
+  ENABLED = 1
+}
+
+// 银行名称表单数据
+export interface BankNameFormData {
+  name: string;
+  code: string;
+  remark?: string | null;
+  status: number;
+}
+
+// 银行名称查询参数
+export interface BankNameQueryParams {
+  page?: number;
+  limit?: number;
+  search?: string;
+  status?: number;
+}

+ 343 - 0
packages/bank-name-management-ui/tests/integration/bank-name-management.integration.test.tsx

@@ -0,0 +1,343 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, waitFor, fireEvent } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { BrowserRouter } from 'react-router';
+
+import { BankNameManagement } from '../../src/components/BankNameManagement';
+import { bankNameClient } from '../../src/api/bankNameClient';
+
+// Mock the bank name client
+vi.mock('../../src/api/bankNameClient', () => {
+  const bankNameClient = {
+    index: {
+      $get: vi.fn(),
+      $post: vi.fn(),
+    },
+    ':id': {
+      $put: vi.fn(),
+      $delete: vi.fn(),
+    },
+  }
+  return {
+    bankNameClient,
+    bankNameClientManager: {
+      get: vi.fn(() => bankNameClient),
+    },
+  }
+})
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+  },
+}));
+
+// 测试包装器组件
+const TestWrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+  return (
+    <BrowserRouter>
+      <QueryClientProvider client={queryClient}>
+        {children as any}
+      </QueryClientProvider>
+    </BrowserRouter>
+  );
+};
+
+// 测试数据
+const mockBankNames = {
+  data: [
+    {
+      id: 1,
+      name: '中国工商银行',
+      code: 'ICBC',
+      remark: '大型国有商业银行',
+      status: 1,
+      createdAt: '2024-01-01T00:00:00Z',
+      updatedAt: '2024-01-01T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      id: 2,
+      name: '中国建设银行',
+      code: 'CCB',
+      remark: '大型国有商业银行',
+      status: 0,
+      createdAt: '2024-01-01T00:00:00Z',
+      updatedAt: '2024-01-01T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+  ],
+  pagination: {
+    total: 2,
+    page: 1,
+    pageSize: 10,
+    totalPages: 1,
+  },
+};
+
+describe('BankNameManagement 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染银行名称管理界面', async () => {
+    // Mock API响应
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 验证标题和描述
+    expect(screen.getByTestId('page-title')).toHaveTextContent('银行名称管理');
+    expect(screen.getByTestId('page-description')).toHaveTextContent('管理银行名称配置,用于广告位分类');
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('type-row-1')).toBeInTheDocument();
+      expect(screen.getByTestId('type-row-2')).toBeInTheDocument();
+    });
+
+    // 验证表格列
+    expect(screen.getByRole('columnheader', { name: 'ID' })).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: '类型名称' })).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: '调用别名' })).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: '状态' })).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: '创建时间' })).toBeInTheDocument();
+    expect(screen.getByRole('columnheader', { name: '操作' })).toBeInTheDocument();
+
+    // 验证状态显示 - 使用更精确的选择器
+    const statusElements = screen.getAllByText('启用');
+    expect(statusElements.length).toBeGreaterThan(0);
+    const disabledElements = screen.getAllByText('禁用');
+    expect(disabledElements.length).toBeGreaterThan(0);
+
+    // 验证银行代码显示
+    expect(screen.getByText('ICBC')).toBeInTheDocument();
+    expect(screen.getByText('CCB')).toBeInTheDocument();
+  });
+
+  it('应该显示加载状态', () => {
+    // Mock 延迟响应
+    (bankNameClient.index.$get as any).mockImplementation(
+      () => new Promise(() => {}) // 永不解析的Promise
+    );
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 验证加载状态
+    expect(screen.getByTestId('page-title')).toHaveTextContent('银行名称管理');
+    expect(screen.getByTestId('create-type-button')).toBeInTheDocument();
+    expect(screen.getByTestId('create-type-button')).toBeDisabled();
+  });
+
+  it('应该处理搜索功能', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByText('中国工商银行')).toBeInTheDocument();
+    });
+
+    // 获取搜索输入框
+    const searchInput = screen.getByTestId('search-input');
+    const searchButton = screen.getByTestId('search-button');
+
+    // 输入搜索关键词
+    fireEvent.change(searchInput, { target: { value: '首页' } });
+    fireEvent.click(searchButton);
+
+    // 验证API调用
+    await waitFor(() => {
+      expect(bankNameClient.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '首页',
+        },
+      });
+    });
+  });
+
+  it('应该打开创建银行名称模态框', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('type-row-1')).toBeInTheDocument();
+    });
+
+    // 点击创建按钮
+    const createButton = screen.getByTestId('create-type-button');
+    fireEvent.click(createButton);
+
+    // 验证模态框打开
+    await waitFor(() => {
+      expect(screen.getByTestId('modal-title')).toHaveTextContent('创建银行名称');
+    });
+
+    // 验证表单字段
+    expect(screen.getByTestId('create-name-input')).toBeInTheDocument();
+    expect(screen.getByTestId('create-code-input')).toBeInTheDocument();
+    expect(screen.getByTestId('create-remark-textarea')).toBeInTheDocument();
+    expect(screen.getByTestId('create-status-switch')).toBeInTheDocument();
+  });
+
+  it('应该打开编辑银行名称模态框', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('type-row-1')).toBeInTheDocument();
+    });
+
+    // 点击编辑按钮
+    const editButton = screen.getByTestId('edit-button-1');
+    fireEvent.click(editButton);
+
+    // 验证模态框打开
+    await waitFor(() => {
+      expect(screen.getByTestId('modal-title')).toHaveTextContent('编辑银行名称');
+    });
+
+    // 验证表单预填充数据
+    const nameInput = screen.getByTestId('edit-name-input');
+    expect(nameInput).toHaveValue('中国工商银行');
+  });
+
+  it('应该处理删除确认对话框', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('type-row-1')).toBeInTheDocument();
+    });
+
+    // 点击删除按钮
+    const deleteButton = screen.getByTestId('delete-button-1');
+    fireEvent.click(deleteButton);
+
+    // 验证删除确认对话框打开
+    await waitFor(() => {
+      expect(screen.getByTestId('delete-dialog')).toBeInTheDocument();
+      expect(screen.getByTestId('delete-dialog-title')).toHaveTextContent('确认删除');
+      expect(screen.getByTestId('delete-dialog-description')).toHaveTextContent(/确定要删除这个银行名称吗/);
+    });
+  });
+
+  it('应该显示空状态', async () => {
+    // Mock 空数据响应
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        data: [],
+        pagination: {
+          total: 0,
+          page: 1,
+          pageSize: 10,
+          totalPages: 0,
+        },
+      }),
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 验证空状态显示
+    await waitFor(() => {
+      expect(screen.getByText('暂无银行名称数据')).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理分页', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    });
+
+    render(
+      <TestWrapper>
+        <BankNameManagement />
+      </TestWrapper>
+    );
+
+    // 等待数据加载
+    await waitFor(() => {
+      expect(screen.getByTestId('type-row-1')).toBeInTheDocument();
+    });
+
+    // 验证分页信息
+    expect(screen.getByText('共 2 条记录')).toBeInTheDocument();
+    expect(screen.getByTestId('current-page-info')).toHaveTextContent('第 1 页');
+
+    // 验证分页按钮存在
+    const nextButton = screen.getByTestId('next-page-button');
+    expect(nextButton).toBeInTheDocument();
+
+    const prevButton = screen.getByTestId('prev-page-button');
+    expect(prevButton).toBeInTheDocument();
+
+    // 验证上一页按钮被禁用(当前是第一页)
+    expect(prevButton).toBeDisabled();
+  });
+});

+ 214 - 0
packages/bank-name-management-ui/tests/integration/bank-name-selector.integration.test.tsx

@@ -0,0 +1,214 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { render, screen, fireEvent, waitFor } from '@testing-library/react'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { BankNameSelector } from '../../src/components/BankNameSelector'
+import { bankNameClient } from '../../src/api/bankNameClient'
+
+// Mock the bank name client
+vi.mock('../../src/api/bankNameClient', () => {
+  const bankNameClient = {
+    index: {
+      $get: vi.fn(),
+    },
+  }
+  return {
+    bankNameClient,
+    bankNameClientManager: {
+      get: vi.fn(() => bankNameClient),
+    },
+  }
+})
+
+const mockBankNames = {
+  data: [
+    { id: 1, name: '中国工商银行', code: 'ICBC', status: 1, createdAt: '2024-01-01' },
+    { id: 2, name: '中国建设银行', code: 'CCB', status: 1, createdAt: '2024-01-01' },
+    { id: 3, name: '中国农业银行', code: 'ABC', status: 0, createdAt: '2024-01-01' },
+  ],
+  pagination: {
+    total: 3,
+    page: 1,
+    pageSize: 10,
+    totalPages: 1,
+  },
+}
+
+const TestWrapper = ({ children }: { children: React.ReactNode }) => {
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  })
+
+  return (
+    <QueryClientProvider client={queryClient}>
+      {children as any}
+    </QueryClientProvider>
+  )
+}
+
+describe('BankNameSelector 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
+
+  it('应该正确渲染银行名称选择器', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    })
+
+    render(
+      <TestWrapper>
+        <BankNameSelector testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('bank-name-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('请选择银行名称')).toBeInTheDocument()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('bank-name-selector')
+    fireEvent.click(selectTrigger)
+
+    // 验证下拉菜单中的选项
+    await waitFor(() => {
+      expect(screen.getByText('中国工商银行')).toBeInTheDocument()
+      expect(screen.getByText('中国建设银行')).toBeInTheDocument()
+      expect(screen.getByText('中国农业银行')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理加载状态', () => {
+    // Mock 延迟响应
+    (bankNameClient.index.$get as any).mockImplementation(
+      () => new Promise(() => {}) // 永不解析的Promise
+    )
+
+    render(
+      <TestWrapper>
+        <BankNameSelector testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('bank-name-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+  })
+
+  it('应该处理错误状态', async () => {
+    (bankNameClient.index.$get as any).mockRejectedValue(new Error('API错误'))
+
+    render(
+      <TestWrapper>
+        <BankNameSelector testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 等待错误状态
+    await waitFor(() => {
+      expect(screen.getByText('加载银行名称失败')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理选择器值变化', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    })
+
+    const mockOnChange = vi.fn()
+
+    render(
+      <TestWrapper>
+        <BankNameSelector onChange={mockOnChange} testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('bank-name-selector')).toBeEnabled()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('bank-name-selector')
+    fireEvent.click(selectTrigger)
+
+    // 选择第一个选项
+    await waitFor(() => {
+      const firstOption = screen.getByText('中国工商银行')
+      fireEvent.click(firstOption)
+    })
+
+    // 验证onChange被调用
+    expect(mockOnChange).toHaveBeenCalledWith(1)
+  })
+
+  it('应该支持自定义占位符', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    })
+
+    render(
+      <TestWrapper>
+        <BankNameSelector placeholder="选择银行名称" testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('选择银行名称')).toBeInTheDocument()
+    })
+  })
+
+  it('应该支持禁用状态', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    })
+
+    render(
+      <TestWrapper>
+        <BankNameSelector disabled={true} testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      const selectTrigger = screen.getByTestId('bank-name-selector')
+      expect(selectTrigger).toBeDisabled()
+    })
+  })
+
+  it('应该支持预选值', async () => {
+    (bankNameClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockBankNames,
+    })
+
+    render(
+      <TestWrapper>
+        <BankNameSelector value={2} testId="bank-name-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('bank-name-selector')).toBeEnabled()
+    })
+
+    // 验证预选值已正确设置
+    // 在Radix UI Select中,预选值会显示在选择器触发器中
+    const selectTrigger = screen.getByTestId('bank-name-selector')
+    expect(selectTrigger).toHaveTextContent('中国建设银行')
+  })
+})

+ 46 - 0
packages/bank-name-management-ui/tests/setup.ts

@@ -0,0 +1,46 @@
+import '@testing-library/jest-dom';
+import { vi } from 'vitest';
+
+// Mock window.matchMedia
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: vi.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: vi.fn(), // deprecated
+    removeListener: vi.fn(), // deprecated
+    addEventListener: vi.fn(),
+    removeEventListener: vi.fn(),
+    dispatchEvent: vi.fn(),
+  })),
+});
+
+// Mock ResizeObserver
+global.ResizeObserver = class MockResizeObserver {
+  constructor(callback: ResizeObserverCallback) {
+    // Store callback for testing
+    (this as any).callback = callback;
+  }
+  observe = vi.fn();
+  unobserve = vi.fn();
+  disconnect = vi.fn();
+};
+
+// Mock IntersectionObserver
+global.IntersectionObserver = class MockIntersectionObserver {
+  constructor(callback: IntersectionObserverCallback) {
+    // Store callback for testing
+    (this as any).callback = callback;
+  }
+  observe = vi.fn();
+  unobserve = vi.fn();
+  disconnect = vi.fn();
+  root: Element | null = null;
+  rootMargin: string = '';
+  thresholds: ReadonlyArray<number> = [];
+  takeRecords = vi.fn();
+};
+
+// Mock Element.scrollIntoView 避免shadcn/ui Select组件错误
+Element.prototype.scrollIntoView = vi.fn();

+ 36 - 0
packages/bank-name-management-ui/tsconfig.json

@@ -0,0 +1,36 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "skipLibCheck": true,
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx",
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "noFallthroughCasesInSwitch": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "outDir": "./dist",
+    "baseUrl": ".",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "include": [
+    "src/**/*",
+    "tests/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}

+ 24 - 0
packages/bank-name-management-ui/vitest.config.ts

@@ -0,0 +1,24 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'jsdom',
+    setupFiles: ['./tests/setup.ts'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'node_modules/',
+        'tests/',
+        '**/*.d.ts',
+        '**/*.config.*'
+      ]
+    }
+  },
+  resolve: {
+    alias: {
+      '@': './src'
+    }
+  }
+});

+ 78 - 0
packages/bank-names-module/package.json

@@ -0,0 +1,78 @@
+{
+  "name": "@d8d/bank-names-module",
+  "version": "1.0.0",
+  "description": "银行名称管理模块 - 提供银行名称的完整CRUD功能",
+  "type": "module",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./services": {
+      "types": "./src/services/index.ts",
+      "import": "./src/services/index.ts",
+      "require": "./src/services/index.ts"
+    },
+    "./schemas": {
+      "types": "./src/schemas/index.ts",
+      "import": "./src/schemas/index.ts",
+      "require": "./src/schemas/index.ts"
+    },
+    "./routes": {
+      "types": "./src/routes/index.ts",
+      "import": "./src/routes/index.ts",
+      "require": "./src/routes/index.ts"
+    },
+    "./entities": {
+      "types": "./src/entities/index.ts",
+      "import": "./src/entities/index.ts",
+      "require": "./src/entities/index.ts"
+    }
+  },
+  "files": [
+    "src"
+  ],
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "test": "vitest run",
+    "test:watch": "vitest",
+    "test:integration": "vitest run tests/integration",
+    "test:coverage": "vitest run --coverage",
+    "lint": "eslint src --ext .ts,.tsx",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "@d8d/shared-utils": "workspace:*",
+    "@d8d/shared-crud": "workspace:*",
+    "@d8d/auth-module": "workspace:*",
+    "@hono/zod-openapi": "^1.0.2",
+    "typeorm": "^0.3.20",
+    "zod": "^4.1.12"
+  },
+  "devDependencies": {
+    "@types/node": "^22.10.2",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4",
+    "@d8d/shared-test-util": "workspace:*",
+    "@typescript-eslint/eslint-plugin": "^8.18.1",
+    "@typescript-eslint/parser": "^8.18.1",
+    "eslint": "^9.17.0"
+  },
+  "peerDependencies": {
+    "hono": "^4.8.5"
+  },
+  "keywords": [
+    "bank",
+    "bank-names",
+    "financial",
+    "crud",
+    "api"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 73 - 0
packages/bank-names-module/src/entities/bank-name.entity.ts

@@ -0,0 +1,73 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('bank_name')
+export class BankName {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({
+    name: 'name',
+    type: 'varchar',
+    length: 50,
+    comment: '银行名称'
+  })
+  name!: string;
+
+  @Column({
+    name: 'code',
+    type: 'varchar',
+    length: 20,
+    unique: true,
+    comment: '银行代码'
+  })
+  code!: string;
+
+  @Column({
+    name: 'remark',
+    type: 'varchar',
+    length: 100,
+    nullable: true,
+    comment: '备注'
+  })
+  remark!: string | null;
+
+  @CreateDateColumn({
+    name: 'created_at',
+    type: 'timestamp',
+    comment: '创建时间'
+  })
+  createdAt!: Date;
+
+  @UpdateDateColumn({
+    name: 'updated_at',
+    type: 'timestamp',
+    comment: '更新时间'
+  })
+  updatedAt!: Date;
+
+  @Column({
+    name: 'created_by',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '创建用户ID'
+  })
+  createdBy!: number | null;
+
+  @Column({
+    name: 'updated_by',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '更新用户ID'
+  })
+  updatedBy!: number | null;
+
+  @Column({
+    name: 'status',
+    type: 'int',
+    default: 0,
+    comment: '状态 0禁用 1启用'
+  })
+  status!: number;
+}

+ 6 - 0
packages/bank-names-module/src/index.ts

@@ -0,0 +1,6 @@
+// 银行名称模块主导出文件
+
+export * from './entities/bank-name.entity';
+export * from './services/bank-name.service';
+export * from './schemas/bank-name.schema';
+export { bankNameRoutes } from './routes/bank-names';

+ 18 - 0
packages/bank-names-module/src/routes/bank-names.ts

@@ -0,0 +1,18 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { BankName } from '../entities/bank-name.entity';
+import { BankNameSchema, CreateBankNameDto, UpdateBankNameDto } from '../schemas/bank-name.schema';
+
+export const bankNameRoutes = createCrudRoutes({
+  entity: BankName,
+  createSchema: CreateBankNameDto,
+  updateSchema: UpdateBankNameDto,
+  getSchema: BankNameSchema,
+  listSchema: BankNameSchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});

+ 81 - 0
packages/bank-names-module/src/schemas/bank-name.schema.ts

@@ -0,0 +1,81 @@
+import { z } from '@hono/zod-openapi';
+
+// 银行名称实体Schema
+export const BankNameSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '银行名称ID',
+    example: 1
+  }),
+  name: z.string().max(50).openapi({
+    description: '银行名称',
+    example: '中国工商银行'
+  }),
+  code: z.string().max(20).openapi({
+    description: '银行代码',
+    example: 'ICBC'
+  }),
+  remark: z.string().max(100).nullable().openapi({
+    description: '备注',
+    example: '大型国有商业银行'
+  }),
+  createdAt: z.coerce.date().openapi({
+    description: '创建时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  updatedAt: z.coerce.date().openapi({
+    description: '更新时间',
+    example: '2024-01-01T00:00:00Z'
+  }),
+  createdBy: z.number().int().positive().nullable().openapi({
+    description: '创建用户ID',
+    example: 1
+  }),
+  updatedBy: z.number().int().positive().nullable().openapi({
+    description: '更新用户ID',
+    example: 1
+  }),
+  status: z.number().int().min(0).max(1).default(0).openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});
+
+// 创建银行名称DTO
+export const CreateBankNameDto = z.object({
+  name: z.string().min(1).max(50).openapi({
+    description: '银行名称',
+    example: '中国工商银行'
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '银行代码',
+    example: 'ICBC'
+  }),
+  remark: z.string().max(100).nullable().optional().openapi({
+    description: '备注',
+    example: '大型国有商业银行'
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});
+
+// 更新银行名称DTO
+export const UpdateBankNameDto = z.object({
+  name: z.string().min(1).max(50).optional().openapi({
+    description: '银行名称',
+    example: '中国工商银行'
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '银行代码',
+    example: 'ICBC'
+  }),
+  remark: z.string().max(100).nullable().optional().openapi({
+    description: '备注',
+    example: '大型国有商业银行'
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  })
+});

+ 1 - 0
packages/bank-names-module/src/schemas/index.ts

@@ -0,0 +1 @@
+export * from './bank-name.schema';

+ 9 - 0
packages/bank-names-module/src/services/bank-name.service.ts

@@ -0,0 +1,9 @@
+import { GenericCrudService } from '@d8d/shared-crud';
+import { DataSource } from 'typeorm';
+import { BankName } from '../entities/bank-name.entity';
+
+export class BankNameService extends GenericCrudService<BankName> {
+  constructor(dataSource: DataSource) {
+    super(dataSource, BankName);
+  }
+}

+ 268 - 0
packages/bank-names-module/tests/integration/bank-names.integration.test.ts

@@ -0,0 +1,268 @@
+import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
+import { testClient } from 'hono/testing';
+import { IntegrationTestDatabase, setupIntegrationDatabaseHooksWithEntities } from '@d8d/shared-test-util';
+import { JWTUtil } from '@d8d/shared-utils';
+import { UserEntity, Role } from '@d8d/user-module';
+import { File } from '@d8d/file-module';
+import { bankNameRoutes } from '../../src/routes/bank-names';
+import { BankName } from '../../src/entities/bank-name.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntity, File, Role, BankName])
+
+describe('银行名称管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof bankNameRoutes>>;
+  let testToken: string;
+  let testUser: UserEntity;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(bankNameRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntity);
+    testUser = userRepository.create({
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web'
+    });
+    await userRepository.save(testUser);
+
+    // 生成测试用户的token
+    testToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{ name: 'user' }]
+    });
+  });
+
+  describe('GET /bank-names', () => {
+    it('应该返回银行名称列表', async () => {
+      const response = await client.index.$get({
+        query: {
+
+        }
+      },
+      {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('银行名称列表响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data).toHaveProperty('data');
+        expect(Array.isArray(data.data)).toBe(true);
+      }
+    });
+
+    it('应该拒绝未认证用户的访问', async () => {
+      const response = await client.index.$get({
+        query: {}
+      });
+      expect(response.status).toBe(401);
+    });
+  });
+
+  describe('POST /bank-names', () => {
+    it('应该成功创建银行名称', async () => {
+      const createData = {
+        name: '测试银行名称',
+        code: 'test_type',
+        remark: '测试备注',
+        status: 1
+      };
+
+      const response = await client.index.$post({
+        json: createData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('创建银行名称响应状态:', response.status);
+      expect(response.status).toBe(201);
+
+      if (response.status === 201) {
+        const data = await response.json();
+        expect(data).toHaveProperty('id');
+        expect(data.name).toBe(createData.name);
+        expect(data.code).toBe(createData.code);
+        expect(data.status).toBe(createData.status);
+      }
+    });
+
+    it('应该验证创建银行名称的必填字段', async () => {
+      const invalidData = {
+        // 缺少必填字段
+        name: '',
+        code: '',
+        remark: '测试备注'
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+
+    it('应该验证银行名称编码的唯一性', async () => {
+      // 先创建一个银行名称
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const bankNameRepository = dataSource.getRepository(BankName);
+      const existingType = bankNameRepository.create({
+        name: '现有类型',
+        code: 'existing_code',
+        status: 1
+      });
+      await bankNameRepository.save(existingType);
+
+      // 尝试创建相同编码的类型
+      const duplicateData = {
+        name: '重复类型',
+        code: 'existing_code', // 重复的编码
+        status: 1
+      };
+
+      const response = await client.index.$post({
+        json: duplicateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /bank-names/:id', () => {
+    it('应该返回指定银行名称的详情', async () => {
+      // 先创建一个银行名称
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const bankNameRepository = dataSource.getRepository(BankName);
+      const testType = bankNameRepository.create({
+        name: '测试类型详情',
+        code: 'test_type_detail',
+        remark: '测试备注',
+        status: 1
+      });
+      await bankNameRepository.save(testType);
+
+      const response = await client[':id'].$get({
+        param: { id: testType.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('银行名称详情响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.id).toBe(testType.id);
+        expect(data.name).toBe(testType.name);
+        expect(data.code).toBe(testType.code);
+      }
+    });
+
+    it('应该处理不存在的银行名称', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /bank-names/:id', () => {
+    it('应该成功更新银行名称', async () => {
+      // 先创建一个银行名称
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const bankNameRepository = dataSource.getRepository(BankName);
+      const testType = bankNameRepository.create({
+        name: '原始类型',
+        code: 'original_type',
+        remark: '原始备注',
+        status: 1
+      });
+      await bankNameRepository.save(testType);
+
+      const updateData = {
+        name: '更新后的类型',
+        code: 'updated_type',
+        remark: '更新后的备注'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testType.id },
+        json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('更新银行名称响应状态:', response.status);
+      expect(response.status).toBe(200);
+
+      if (response.status === 200) {
+        const data = await response.json();
+        expect(data.name).toBe(updateData.name);
+        expect(data.code).toBe(updateData.code);
+        expect(data.remark).toBe(updateData.remark);
+      }
+    });
+  });
+
+  describe('DELETE /bank-names/:id', () => {
+    it('应该成功删除银行名称', async () => {
+      // 先创建一个银行名称
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const bankNameRepository = dataSource.getRepository(BankName);
+      const testType = bankNameRepository.create({
+        name: '待删除类型',
+        code: 'delete_type',
+        remark: '待删除备注',
+        status: 1
+      });
+      await bankNameRepository.save(testType);
+
+      const response = await client[':id'].$delete({
+        param: { id: testType.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('删除银行名称响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证银行名称确实被删除
+      const deletedType = await bankNameRepository.findOne({
+        where: { id: testType.id }
+      });
+      expect(deletedType).toBeNull();
+    });
+  });
+});

+ 16 - 0
packages/bank-names-module/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "rootDir": ".",
+    "outDir": "dist"
+  },
+  "include": [
+    "src/**/*",
+    "tests/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist"
+  ]
+}

+ 21 - 0
packages/bank-names-module/vitest.config.ts

@@ -0,0 +1,21 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: [
+        'tests/**',
+        '**/*.d.ts',
+        '**/*.config.*',
+        '**/dist/**'
+      ]
+    },
+    // 关闭并行测试以避免数据库连接冲突
+    fileParallelism: false
+  }
+});

+ 328 - 34
pnpm-lock.yaml

@@ -319,6 +319,9 @@ importers:
       '@d8d/auth-module':
         specifier: workspace:*
         version: link:../../packages/auth-module
+      '@d8d/bank-names-module':
+        specifier: workspace:*
+        version: link:../../packages/bank-names-module
       '@d8d/file-module':
         specifier: workspace:*
         version: link:../../packages/file-module
@@ -380,6 +383,9 @@ importers:
       '@d8d/area-management-ui':
         specifier: workspace:*
         version: link:../../packages/area-management-ui
+      '@d8d/bank-name-management-ui':
+        specifier: workspace:*
+        version: link:../../packages/bank-name-management-ui
       '@d8d/file-management-ui':
         specifier: workspace:*
         version: link:../../packages/file-management-ui
@@ -2135,6 +2141,155 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/bank-name-management-ui:
+    dependencies:
+      '@d8d/bank-names-module':
+        specifier: workspace:*
+        version: link:../bank-names-module
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-ui-components':
+        specifier: workspace:*
+        version: link:../shared-ui-components
+      '@hookform/resolvers':
+        specifier: ^5.2.1
+        version: 5.2.2(react-hook-form@7.65.0(react@19.2.0))
+      '@tanstack/react-query':
+        specifier: ^5.90.12
+        version: 5.90.12(react@19.2.0)
+      axios:
+        specifier: ^1.7.9
+        version: 1.12.2(debug@4.4.3)
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      date-fns:
+        specifier: ^4.1.0
+        version: 4.1.0
+      dayjs:
+        specifier: ^1.11.13
+        version: 1.11.18
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      lucide-react:
+        specifier: ^0.536.0
+        version: 0.536.0(react@19.2.0)
+      react:
+        specifier: ^19.1.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.1.0
+        version: 19.2.0(react@19.2.0)
+      react-hook-form:
+        specifier: ^7.61.1
+        version: 7.65.0(react@19.2.0)
+      react-router:
+        specifier: ^7.1.3
+        version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      sonner:
+        specifier: ^2.0.7
+        version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      tailwind-merge:
+        specifier: ^3.3.1
+        version: 3.3.1
+      zod:
+        specifier: ^4.0.15
+        version: 4.1.12
+    devDependencies:
+      '@testing-library/jest-dom':
+        specifier: ^6.8.0
+        version: 6.9.1
+      '@testing-library/react':
+        specifier: ^16.3.0
+        version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@testing-library/user-event':
+        specifier: ^14.6.1
+        version: 14.6.1(@testing-library/dom@10.4.1)
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.1
+      '@types/react':
+        specifier: ^19.2.2
+        version: 19.2.2
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.2)
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.38.0(jiti@2.6.1)
+      jsdom:
+        specifier: ^26.0.0
+        version: 26.1.0
+      typescript:
+        specifier: ^5.8.3
+        version: 5.9.3
+      unbuild:
+        specifier: ^3.4.0
+        version: 3.6.1(sass@1.93.2)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
+      vitest:
+        specifier: ^4.0.9
+        version: 4.0.14(@types/node@22.19.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
+  packages/bank-names-module:
+    dependencies:
+      '@d8d/auth-module':
+        specifier: workspace:*
+        version: link:../auth-module
+      '@d8d/shared-crud':
+        specifier: workspace:*
+        version: link:../shared-crud
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@d8d/shared-utils':
+        specifier: workspace:*
+        version: link:../shared-utils
+      '@hono/zod-openapi':
+        specifier: ^1.0.2
+        version: 1.0.2(hono@4.8.5)(zod@4.1.12)
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      typeorm:
+        specifier: ^0.3.20
+        version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
+      zod:
+        specifier: ^4.1.12
+        version: 4.1.12
+    devDependencies:
+      '@d8d/shared-test-util':
+        specifier: workspace:*
+        version: link:../shared-test-util
+      '@types/node':
+        specifier: ^22.10.2
+        version: 22.19.1
+      '@typescript-eslint/eslint-plugin':
+        specifier: ^8.18.1
+        version: 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/parser':
+        specifier: ^8.18.1
+        version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      eslint:
+        specifier: ^9.17.0
+        version: 9.38.0(jiti@2.6.1)
+      typescript:
+        specifier: ^5.8.3
+        version: 5.9.3
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/core-module:
     dependencies:
       '@d8d/shared-crud':
@@ -11948,10 +12103,6 @@ packages:
     engines: {node: '>=16 || 14 >=14.17'}
     hasBin: true
 
-  glob@10.4.5:
-    resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
-    hasBin: true
-
   glob@10.5.0:
     resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
     hasBin: true
@@ -13328,10 +13479,6 @@ packages:
   minimalistic-assert@1.0.1:
     resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
 
-  minimatch@10.0.3:
-    resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
-    engines: {node: 20 || >=22}
-
   minimatch@10.1.1:
     resolution: {integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==}
     engines: {node: 20 || >=22}
@@ -15020,9 +15167,6 @@ packages:
   sax@1.2.4:
     resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
 
-  sax@1.4.1:
-    resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
-
   sax@1.4.3:
     resolution: {integrity: sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==}
 
@@ -20829,7 +20973,7 @@ snapshots:
   '@ts-morph/common@0.27.0':
     dependencies:
       fast-glob: 3.3.3
-      minimatch: 10.0.3
+      minimatch: 10.1.1
       path-browserify: 1.0.1
 
   '@tybys/wasm-util@0.10.1':
@@ -21080,7 +21224,7 @@ snapshots:
 
   '@types/react@19.2.2':
     dependencies:
-      csstype: 3.1.3
+      csstype: 3.2.3
 
   '@types/resolve@1.20.2': {}
 
@@ -21178,6 +21322,23 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.1
+      '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/scope-manager': 8.46.2
+      '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.46.2
+      eslint: 9.38.0(jiti@2.6.1)
+      graphemer: 1.4.0
+      ignore: 7.0.5
+      natural-compare: 1.4.0
+      ts-api-utils: 2.1.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 6.21.0
@@ -21203,6 +21364,18 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.46.2
+      '@typescript-eslint/types': 8.46.2
+      '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.46.2
+      debug: 4.4.3
+      eslint: 9.38.0(jiti@2.6.1)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@typescript-eslint/project-service@8.46.2(typescript@5.8.3)':
     dependencies:
       '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.8.3)
@@ -21212,6 +21385,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/project-service@8.46.2(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
+      '@typescript-eslint/types': 8.46.2
+      debug: 4.4.3
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@typescript-eslint/scope-manager@6.21.0':
     dependencies:
       '@typescript-eslint/types': 6.21.0
@@ -21226,6 +21408,10 @@ snapshots:
     dependencies:
       typescript: 5.8.3
 
+  '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.9.3)':
+    dependencies:
+      typescript: 5.9.3
+
   '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)':
     dependencies:
       '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3)
@@ -21250,6 +21436,18 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/types': 8.46.2
+      '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)
+      debug: 4.4.3
+      eslint: 9.38.0(jiti@2.6.1)
+      ts-api-utils: 2.1.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@typescript-eslint/types@6.21.0': {}
 
   '@typescript-eslint/types@8.46.2': {}
@@ -21285,6 +21483,22 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/typescript-estree@8.46.2(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/project-service': 8.46.2(typescript@5.9.3)
+      '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.9.3)
+      '@typescript-eslint/types': 8.46.2
+      '@typescript-eslint/visitor-keys': 8.46.2
+      debug: 4.4.3
+      fast-glob: 3.3.3
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.7.3
+      ts-api-utils: 2.1.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)':
     dependencies:
       '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1)
@@ -21310,6 +21524,17 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1))
+      '@typescript-eslint/scope-manager': 8.46.2
+      '@typescript-eslint/types': 8.46.2
+      '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3)
+      eslint: 9.38.0(jiti@2.6.1)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
   '@typescript-eslint/visitor-keys@6.21.0':
     dependencies:
       '@typescript-eslint/types': 6.21.0
@@ -21560,7 +21785,7 @@ snapshots:
       '@vue/reactivity': 3.5.22
       '@vue/runtime-core': 3.5.22
       '@vue/shared': 3.5.22
-      csstype: 3.1.3
+      csstype: 3.2.3
 
   '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.8.3))':
     dependencies:
@@ -21568,6 +21793,13 @@ snapshots:
       '@vue/shared': 3.5.22
       vue: 3.5.22(typescript@5.8.3)
 
+  '@vue/server-renderer@3.5.22(vue@3.5.22(typescript@5.9.3))':
+    dependencies:
+      '@vue/compiler-ssr': 3.5.22
+      '@vue/shared': 3.5.22
+      vue: 3.5.22(typescript@5.9.3)
+    optional: true
+
   '@vue/shared@3.5.22': {}
 
   '@weapp-core/escape@4.0.1': {}
@@ -23135,7 +23367,7 @@ snapshots:
   dom-helpers@5.2.1:
     dependencies:
       '@babel/runtime': 7.28.4
-      csstype: 3.1.3
+      csstype: 3.2.3
 
   dom-serializer@1.4.1:
     dependencies:
@@ -24206,15 +24438,6 @@ snapshots:
       minipass: 6.0.2
       path-scurry: 1.11.1
 
-  glob@10.4.5:
-    dependencies:
-      foreground-child: 3.3.1
-      jackspeak: 3.4.3
-      minimatch: 9.0.5
-      minipass: 7.1.2
-      package-json-from-dist: 1.0.1
-      path-scurry: 1.11.1
-
   glob@10.5.0:
     dependencies:
       foreground-child: 3.3.1
@@ -25888,10 +26111,6 @@ snapshots:
 
   minimalistic-assert@1.0.1: {}
 
-  minimatch@10.0.3:
-    dependencies:
-      '@isaacs/brace-expansion': 5.0.0
-
   minimatch@10.1.1:
     dependencies:
       '@isaacs/brace-expansion': 5.0.0
@@ -25986,6 +26205,26 @@ snapshots:
       typescript: 5.8.3
       vue: 3.5.22(typescript@5.8.3)
 
+  mkdist@2.4.1(sass@1.93.2)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)):
+    dependencies:
+      autoprefixer: 10.4.21(postcss@8.5.6)
+      citty: 0.1.6
+      cssnano: 7.1.2(postcss@8.5.6)
+      defu: 6.1.4
+      esbuild: 0.25.11
+      jiti: 1.21.7
+      mlly: 1.8.0
+      pathe: 2.0.3
+      pkg-types: 2.3.0
+      postcss: 8.5.6
+      postcss-nested: 7.0.2(postcss@8.5.6)
+      semver: 7.7.3
+      tinyglobby: 0.2.15
+    optionalDependencies:
+      sass: 1.93.2
+      typescript: 5.9.3
+      vue: 3.5.22(typescript@5.9.3)
+
   mlly@1.8.0:
     dependencies:
       acorn: 8.15.0
@@ -27529,6 +27768,14 @@ snapshots:
     optionalDependencies:
       '@babel/code-frame': 7.27.1
 
+  rollup-plugin-dts@6.3.0(rollup@4.52.5)(typescript@5.9.3):
+    dependencies:
+      magic-string: 0.30.21
+      rollup: 4.52.5
+      typescript: 5.9.3
+    optionalDependencies:
+      '@babel/code-frame': 7.27.1
+
   rollup@3.29.5:
     optionalDependencies:
       fsevents: 2.3.3
@@ -27619,8 +27866,6 @@ snapshots:
 
   sax@1.2.4: {}
 
-  sax@1.4.1: {}
-
   sax@1.4.3: {}
 
   saxes@6.0.0:
@@ -28225,7 +28470,7 @@ snapshots:
       css-what: 6.2.2
       csso: 5.0.5
       picocolors: 1.1.1
-      sax: 1.4.1
+      sax: 1.4.3
 
   swiper@11.1.15: {}
 
@@ -28332,7 +28577,7 @@ snapshots:
   test-exclude@7.0.1:
     dependencies:
       '@istanbuljs/schema': 0.1.3
-      glob: 10.4.5
+      glob: 10.5.0
       minimatch: 9.0.5
 
   text-extensions@2.4.0: {}
@@ -28441,6 +28686,10 @@ snapshots:
     dependencies:
       typescript: 5.8.3
 
+  ts-api-utils@2.1.0(typescript@5.9.3):
+    dependencies:
+      typescript: 5.9.3
+
   ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0(@types/node@18.19.130))(typescript@5.8.3):
     dependencies:
       bs-logger: 0.2.6
@@ -28577,7 +28826,7 @@ snapshots:
       debug: 4.4.3
       dedent: 1.7.0
       dotenv: 16.6.1
-      glob: 10.4.5
+      glob: 10.5.0
       reflect-metadata: 0.2.2
       sha.js: 2.4.12
       sql-highlight: 6.1.0
@@ -28641,6 +28890,40 @@ snapshots:
       - vue-sfc-transformer
       - vue-tsc
 
+  unbuild@3.6.1(sass@1.93.2)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)):
+    dependencies:
+      '@rollup/plugin-alias': 5.1.1(rollup@4.52.5)
+      '@rollup/plugin-commonjs': 28.0.9(rollup@4.52.5)
+      '@rollup/plugin-json': 6.1.0(rollup@4.52.5)
+      '@rollup/plugin-node-resolve': 16.0.3(rollup@4.52.5)
+      '@rollup/plugin-replace': 6.0.3(rollup@4.52.5)
+      '@rollup/pluginutils': 5.3.0(rollup@4.52.5)
+      citty: 0.1.6
+      consola: 3.4.2
+      defu: 6.1.4
+      esbuild: 0.25.11
+      fix-dts-default-cjs-exports: 1.0.1
+      hookable: 5.5.3
+      jiti: 2.6.1
+      magic-string: 0.30.21
+      mkdist: 2.4.1(sass@1.93.2)(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
+      mlly: 1.8.0
+      pathe: 2.0.3
+      pkg-types: 2.3.0
+      pretty-bytes: 7.1.0
+      rollup: 4.52.5
+      rollup-plugin-dts: 6.3.0(rollup@4.52.5)(typescript@5.9.3)
+      scule: 1.3.0
+      tinyglobby: 0.2.15
+      untyped: 2.0.0
+    optionalDependencies:
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - sass
+      - vue
+      - vue-sfc-transformer
+      - vue-tsc
+
   unbzip2-stream@1.4.3:
     dependencies:
       buffer: 5.7.1
@@ -29067,6 +29350,17 @@ snapshots:
     optionalDependencies:
       typescript: 5.8.3
 
+  vue@3.5.22(typescript@5.9.3):
+    dependencies:
+      '@vue/compiler-dom': 3.5.22
+      '@vue/compiler-sfc': 3.5.22
+      '@vue/runtime-dom': 3.5.22
+      '@vue/server-renderer': 3.5.22(vue@3.5.22(typescript@5.9.3))
+      '@vue/shared': 3.5.22
+    optionalDependencies:
+      typescript: 5.9.3
+    optional: true
+
   w3c-xmlserializer@4.0.0:
     dependencies:
       xml-name-validator: 4.0.0
@@ -29401,7 +29695,7 @@ snapshots:
 
   xml2js@0.6.2:
     dependencies:
-      sax: 1.4.1
+      sax: 1.4.3
       xmlbuilder: 11.0.1
 
   xmlbuilder@11.0.1: {}