Explorar o código

反哺packages

yourname hai 2 meses
pai
achega
a8028dbfda
Modificáronse 100 ficheiros con 10498 adicións e 0 borrados
  1. 43 0
      packages/advertisement-management-ui-mt/eslint.config.js
  2. 94 0
      packages/advertisement-management-ui-mt/package.json
  3. 44 0
      packages/advertisement-management-ui-mt/src/api/advertisementClient.ts
  4. 5 0
      packages/advertisement-management-ui-mt/src/api/index.ts
  5. 763 0
      packages/advertisement-management-ui-mt/src/components/AdvertisementManagement.tsx
  6. 1 0
      packages/advertisement-management-ui-mt/src/components/index.ts
  7. 18 0
      packages/advertisement-management-ui-mt/src/index.ts
  8. 24 0
      packages/advertisement-management-ui-mt/src/types/advertisement.ts
  9. 8 0
      packages/advertisement-management-ui-mt/src/types/index.ts
  10. 348 0
      packages/advertisement-management-ui-mt/tests/integration/advertisement-management.integration.test.tsx
  11. 14 0
      packages/advertisement-management-ui-mt/tests/setup.ts
  12. 36 0
      packages/advertisement-management-ui-mt/tsconfig.json
  13. 24 0
      packages/advertisement-management-ui-mt/vitest.config.ts
  14. 43 0
      packages/advertisement-management-ui/eslint.config.js
  15. 92 0
      packages/advertisement-management-ui/package.json
  16. 44 0
      packages/advertisement-management-ui/src/api/advertisementClient.ts
  17. 5 0
      packages/advertisement-management-ui/src/api/index.ts
  18. 763 0
      packages/advertisement-management-ui/src/components/AdvertisementManagement.tsx
  19. 1 0
      packages/advertisement-management-ui/src/components/index.ts
  20. 18 0
      packages/advertisement-management-ui/src/index.ts
  21. 24 0
      packages/advertisement-management-ui/src/types/advertisement.ts
  22. 8 0
      packages/advertisement-management-ui/src/types/index.ts
  23. 348 0
      packages/advertisement-management-ui/tests/integration/advertisement-management.integration.test.tsx
  24. 14 0
      packages/advertisement-management-ui/tests/setup.ts
  25. 36 0
      packages/advertisement-management-ui/tsconfig.json
  26. 24 0
      packages/advertisement-management-ui/vitest.config.ts
  27. 36 0
      packages/advertisement-type-management-ui-mt/eslint.config.js
  28. 95 0
      packages/advertisement-type-management-ui-mt/package.json
  29. 44 0
      packages/advertisement-type-management-ui-mt/src/api/advertisementTypeClient.ts
  30. 3 0
      packages/advertisement-type-management-ui-mt/src/api/index.ts
  31. 586 0
      packages/advertisement-type-management-ui-mt/src/components/AdvertisementTypeManagement.tsx
  32. 78 0
      packages/advertisement-type-management-ui-mt/src/components/AdvertisementTypeSelector.tsx
  33. 4 0
      packages/advertisement-type-management-ui-mt/src/components/index.ts
  34. 19 0
      packages/advertisement-type-management-ui-mt/src/index.ts
  35. 58 0
      packages/advertisement-type-management-ui-mt/src/types/advertisementType.ts
  36. 341 0
      packages/advertisement-type-management-ui-mt/tests/integration/advertisement-type-management.integration.test.tsx
  37. 214 0
      packages/advertisement-type-management-ui-mt/tests/integration/advertisement-type-selector.integration.test.tsx
  38. 43 0
      packages/advertisement-type-management-ui-mt/tests/setup.ts
  39. 36 0
      packages/advertisement-type-management-ui-mt/tsconfig.json
  40. 24 0
      packages/advertisement-type-management-ui-mt/vitest.config.ts
  41. 36 0
      packages/advertisement-type-management-ui/eslint.config.js
  42. 93 0
      packages/advertisement-type-management-ui/package.json
  43. 44 0
      packages/advertisement-type-management-ui/src/api/advertisementTypeClient.ts
  44. 3 0
      packages/advertisement-type-management-ui/src/api/index.ts
  45. 586 0
      packages/advertisement-type-management-ui/src/components/AdvertisementTypeManagement.tsx
  46. 78 0
      packages/advertisement-type-management-ui/src/components/AdvertisementTypeSelector.tsx
  47. 4 0
      packages/advertisement-type-management-ui/src/components/index.ts
  48. 19 0
      packages/advertisement-type-management-ui/src/index.ts
  49. 58 0
      packages/advertisement-type-management-ui/src/types/advertisementType.ts
  50. 341 0
      packages/advertisement-type-management-ui/tests/integration/advertisement-type-management.integration.test.tsx
  51. 214 0
      packages/advertisement-type-management-ui/tests/integration/advertisement-type-selector.integration.test.tsx
  52. 43 0
      packages/advertisement-type-management-ui/tests/setup.ts
  53. 36 0
      packages/advertisement-type-management-ui/tsconfig.json
  54. 24 0
      packages/advertisement-type-management-ui/vitest.config.ts
  55. 82 0
      packages/advertisements-module-mt/package.json
  56. 81 0
      packages/advertisements-module-mt/src/entities/advertisement-type.entity.ts
  57. 134 0
      packages/advertisements-module-mt/src/entities/advertisement.entity.ts
  58. 2 0
      packages/advertisements-module-mt/src/entities/index.ts
  59. 6 0
      packages/advertisements-module-mt/src/index.ts
  60. 22 0
      packages/advertisements-module-mt/src/routes/advertisement-types.ts
  61. 23 0
      packages/advertisements-module-mt/src/routes/advertisements.ts
  62. 2 0
      packages/advertisements-module-mt/src/routes/index.ts
  63. 85 0
      packages/advertisements-module-mt/src/schemas/advertisement-type.schema.ts
  64. 149 0
      packages/advertisements-module-mt/src/schemas/advertisement.schema.ts
  65. 2 0
      packages/advertisements-module-mt/src/schemas/index.ts
  66. 9 0
      packages/advertisements-module-mt/src/services/advertisement-type.service.ts
  67. 9 0
      packages/advertisements-module-mt/src/services/advertisement.service.ts
  68. 2 0
      packages/advertisements-module-mt/src/services/index.ts
  69. 378 0
      packages/advertisements-module-mt/tests/integration/advertisement-types.integration.test.ts
  70. 356 0
      packages/advertisements-module-mt/tests/integration/advertisements.integration.test.ts
  71. 16 0
      packages/advertisements-module-mt/tsconfig.json
  72. 21 0
      packages/advertisements-module-mt/vitest.config.ts
  73. 80 0
      packages/advertisements-module/package.json
  74. 73 0
      packages/advertisements-module/src/entities/advertisement-type.entity.ts
  75. 125 0
      packages/advertisements-module/src/entities/advertisement.entity.ts
  76. 10 0
      packages/advertisements-module/src/index.ts
  77. 18 0
      packages/advertisements-module/src/routes/advertisement-types.ts
  78. 19 0
      packages/advertisements-module/src/routes/advertisements.ts
  79. 81 0
      packages/advertisements-module/src/schemas/advertisement-type.schema.ts
  80. 145 0
      packages/advertisements-module/src/schemas/advertisement.schema.ts
  81. 2 0
      packages/advertisements-module/src/schemas/index.ts
  82. 9 0
      packages/advertisements-module/src/services/advertisement-type.service.ts
  83. 9 0
      packages/advertisements-module/src/services/advertisement.service.ts
  84. 268 0
      packages/advertisements-module/tests/integration/advertisement-types.integration.test.ts
  85. 264 0
      packages/advertisements-module/tests/integration/advertisements.integration.test.ts
  86. 16 0
      packages/advertisements-module/tsconfig.json
  87. 21 0
      packages/advertisements-module/vitest.config.ts
  88. 36 0
      packages/area-management-ui-mt/eslint.config.js
  89. 96 0
      packages/area-management-ui-mt/package.json
  90. 44 0
      packages/area-management-ui-mt/src/api/areaClient.ts
  91. 9 0
      packages/area-management-ui-mt/src/api/index.ts
  92. 221 0
      packages/area-management-ui-mt/src/components/AreaForm.tsx
  93. 476 0
      packages/area-management-ui-mt/src/components/AreaManagement.tsx
  94. 258 0
      packages/area-management-ui-mt/src/components/AreaSelect.tsx
  95. 350 0
      packages/area-management-ui-mt/src/components/AreaSelect4Level.tsx
  96. 301 0
      packages/area-management-ui-mt/src/components/AreaTreeAsync.tsx
  97. 5 0
      packages/area-management-ui-mt/src/components/index.ts
  98. 8 0
      packages/area-management-ui-mt/src/hooks/index.ts
  99. 155 0
      packages/area-management-ui-mt/src/hooks/useAreas.ts
  100. 18 0
      packages/area-management-ui-mt/src/index.ts

+ 43 - 0
packages/advertisement-management-ui-mt/eslint.config.js

@@ -0,0 +1,43 @@
+import js from '@eslint/js';
+import tseslint from '@typescript-eslint/eslint-plugin';
+import tsparser from '@typescript-eslint/parser';
+import reactPlugin from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+
+export default [
+  {
+    files: ['**/*.{js,jsx,ts,tsx}'],
+    languageOptions: {
+      parser: tsparser,
+      ecmaVersion: 'latest',
+      sourceType: 'module',
+      parserOptions: {
+        ecmaFeatures: {
+          jsx: true
+        }
+      }
+    },
+    plugins: {
+      '@typescript-eslint': tseslint,
+      'react': reactPlugin,
+      'react-hooks': reactHooks
+    },
+    rules: {
+      ...js.configs.recommended.rules,
+      ...tseslint.configs.recommended.rules,
+      ...reactPlugin.configs.recommended.rules,
+      ...reactHooks.configs.recommended.rules,
+      'react/react-in-jsx-scope': 'off',
+      '@typescript-eslint/no-unused-vars': ['error', {
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_'
+      }],
+      '@typescript-eslint/no-explicit-any': 'warn'
+    },
+    settings: {
+      react: {
+        version: 'detect'
+      }
+    }
+  }
+];

+ 94 - 0
packages/advertisement-management-ui-mt/package.json

@@ -0,0 +1,94 @@
+{
+  "name": "@d8d/advertisement-management-ui-mt",
+  "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/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/advertisements-module-mt": "workspace:*",
+    "@d8d/file-management-ui-mt": "workspace:*",
+    "@d8d/advertisement-type-management-ui-mt": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "date-fns": "^4.1.0",
+    "hono": "^4.8.5",
+    "lucide-react": "^0.536.0",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-hook-form": "^7.61.1",
+    "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": [
+    "advertisement",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud",
+    "banner",
+    "multi-tenant",
+    "mt"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 44 - 0
packages/advertisement-management-ui-mt/src/api/advertisementClient.ts

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

+ 5 - 0
packages/advertisement-management-ui-mt/src/api/index.ts

@@ -0,0 +1,5 @@
+export {
+  AdvertisementClientManager,
+  advertisementClientManager,
+  advertisementClient
+} from './advertisementClient';

+ 763 - 0
packages/advertisement-management-ui-mt/src/components/AdvertisementManagement.tsx

@@ -0,0 +1,763 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation } from '@tanstack/react-query';
+import { Plus, Edit, Trash2, Search } from 'lucide-react';
+import { format } from 'date-fns';
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+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 { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
+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 { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination';
+import { FileSelector } from '@d8d/file-management-ui-mt';
+import { AdvertisementTypeSelector } from '@d8d/advertisement-type-management-ui-mt';
+import { advertisementClientManager } from '../api/advertisementClient';
+import { CreateAdvertisementDto, UpdateAdvertisementDto } from '@d8d/advertisements-module-mt/schemas';
+import type { AdvertisementSearchParams, CreateAdvertisementRequest, UpdateAdvertisementRequest, AdvertisementResponse } from '../types';
+
+type CreateRequest = CreateAdvertisementRequest;
+type UpdateRequest = UpdateAdvertisementRequest;
+
+const createFormSchema = CreateAdvertisementDto;
+const updateFormSchema = UpdateAdvertisementDto;
+
+export const AdvertisementManagement: React.FC = () => {
+  const [searchParams, setSearchParams] = useState<AdvertisementSearchParams>({ page: 1, limit: 10, search: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingAdvertisement, setEditingAdvertisement] = useState<AdvertisementResponse | null>(null);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [advertisementToDelete, setAdvertisementToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      title: '',
+      typeId: 1,
+      code: '',
+      url: '',
+      imageFileId: undefined,
+      sort: 0,
+      status: 1,
+      actionType: 1
+    }
+  });
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {}
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['advertisements', searchParams],
+    queryFn: async () => {
+      const res = await advertisementClientManager.get().index.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      });
+      if (res.status !== 200) throw new Error('获取广告列表失败');
+      return await res.json();
+    }
+  });
+
+  // 创建广告
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await advertisementClientManager.get().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 instanceof Error ? error.message : '创建广告失败');
+    }
+  });
+
+  // 更新广告
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await advertisementClientManager.get()[':id']['$put']({
+        param: { id },
+        json: data
+      });
+      if (res.status !== 200) throw new Error('更新广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告更新成功');
+      setIsModalOpen(false);
+      setEditingAdvertisement(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '更新广告失败');
+    }
+  });
+
+  // 删除广告
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await advertisementClientManager.get()[':id']['$delete']({
+        param: { id }
+      });
+      if (res.status !== 204) throw new Error('删除广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告删除成功');
+      setDeleteDialogOpen(false);
+      setAdvertisementToDelete(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '删除广告失败');
+    }
+  });
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+    refetch();
+  };
+
+  // 处理创建广告
+  const handleCreateAdvertisement = () => {
+    setIsCreateForm(true);
+    setEditingAdvertisement(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 处理编辑广告
+  const handleEditAdvertisement = (advertisement: AdvertisementResponse) => {
+    setIsCreateForm(false);
+    setEditingAdvertisement(advertisement);
+    updateForm.reset({
+      title: advertisement.title || undefined,
+      typeId: advertisement.typeId || undefined,
+      code: advertisement.code || undefined,
+      url: advertisement.url || undefined,
+      imageFileId: advertisement.imageFileId || undefined,
+      sort: advertisement.sort || undefined,
+      status: advertisement.status || undefined,
+      actionType: advertisement.actionType || undefined
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理删除广告
+  const handleDeleteAdvertisement = (id: number) => {
+    setAdvertisementToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = () => {
+    if (advertisementToDelete) {
+      deleteMutation.mutate(advertisementToDelete);
+    }
+  };
+
+  // 处理创建表单提交
+  const handleCreateSubmit = async (data: CreateRequest) => {
+    try {
+      await createMutation.mutateAsync(data);
+    } catch (error) {
+      throw error;
+    }
+  };
+
+  // 处理编辑表单提交
+  const handleUpdateSubmit = async (data: UpdateRequest) => {
+    if (!editingAdvertisement) return;
+
+    try {
+      await updateMutation.mutateAsync({
+        id: editingAdvertisement.id,
+        data
+      });
+    } catch (error) {
+      throw error;
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">广告管理</h1>
+        <Button onClick={handleCreateAdvertisement}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建广告
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>广告列表</CardTitle>
+          <CardDescription>管理网站的所有广告内容</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索广告标题或别名..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                  data-testid="search-input"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <div className="relative w-full overflow-x-auto">
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>ID</TableHead>
+                    <TableHead>标题</TableHead>
+                    <TableHead>类型</TableHead>
+                    <TableHead>别名</TableHead>
+                    <TableHead>图片</TableHead>
+                    <TableHead>状态</TableHead>
+                    <TableHead>排序</TableHead>
+                    <TableHead>创建时间</TableHead>
+                    <TableHead className="text-right">操作</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {isLoading ? (
+                    Array.from({ length: 5 }).map((_, index) => (
+                      <TableRow key={index}>
+                        <TableCell>
+                          <Skeleton className="h-4 w-8" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-32" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-20" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-24" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-8 w-8 rounded" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-6 w-12 rounded-full" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-8" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-24" />
+                        </TableCell>
+                        <TableCell>
+                          <div className="flex justify-end gap-2">
+                            <Skeleton className="h-8 w-8 rounded" />
+                            <Skeleton className="h-8 w-8 rounded" />
+                          </div>
+                        </TableCell>
+                      </TableRow>
+                    ))
+                  ) : data?.data && data.data.length > 0 ? (
+                    data.data.map((advertisement) => (
+                      <TableRow key={advertisement.id}>
+                        <TableCell>{advertisement.id}</TableCell>
+                        <TableCell>{advertisement.title || '-'}</TableCell>
+                        <TableCell>
+                          {advertisement.advertisementType?.name || '-'}
+                        </TableCell>
+                        <TableCell>
+                          <code className="text-xs bg-muted px-1 rounded">{advertisement.code || '-'}</code>
+                        </TableCell>
+                        <TableCell>
+                          {advertisement.imageFile?.fullUrl ? (
+                            <img
+                              src={advertisement.imageFile.fullUrl}
+                              alt={advertisement.title || '广告图片'}
+                              className="w-16 h-10 object-cover rounded"
+                              onError={(e) => {
+                                e.currentTarget.src = '/placeholder.png';
+                              }}
+                            />
+                          ) : (
+                            <span className="text-muted-foreground text-xs">无图片</span>
+                          )}
+                        </TableCell>
+                        <TableCell>
+                          <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
+                            {advertisement.status === 1 ? '启用' : '禁用'}
+                          </Badge>
+                        </TableCell>
+                        <TableCell>{advertisement.sort}</TableCell>
+                        <TableCell>
+                          {advertisement.createdAt ? format(new Date(advertisement.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
+                        </TableCell>
+                        <TableCell className="text-right">
+                          <div className="flex justify-end gap-2">
+                            <Button
+                              variant="ghost"
+                              size="icon"
+                              onClick={() => handleEditAdvertisement(advertisement)}
+                              data-testid={`edit-button-${advertisement.id}`}
+                            >
+                              <Edit className="h-4 w-4" />
+                            </Button>
+                            <Button
+                              variant="ghost"
+                              size="icon"
+                              onClick={() => handleDeleteAdvertisement(advertisement.id)}
+                              data-testid={`delete-button-${advertisement.id}`}
+                            >
+                              <Trash2 className="h-4 w-4" />
+                            </Button>
+                          </div>
+                        </TableCell>
+                      </TableRow>
+                    ))
+                  ) : (
+                    <TableRow>
+                      <TableCell colSpan={9} className="text-center py-8">
+                        <p className="text-muted-foreground">暂无广告数据</p>
+                      </TableCell>
+                    </TableRow>
+                  )}
+                </TableBody>
+              </Table>
+            </div>
+          </div>
+
+
+        <DataTablePagination
+          currentPage={searchParams.page}
+          pageSize={searchParams.limit}
+          totalCount={data?.pagination.total || 0}
+          onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+        />
+      </CardContent>
+    </Card>
+
+    {/* 创建/编辑对话框 */}
+    <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建广告' : '编辑广告'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的广告' : '编辑现有广告信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            // 创建表单(独立渲染)
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="title"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        标题 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入广告标题" {...field} data-testid="title-input" />
+                      </FormControl>
+                      <FormDescription>广告显示的标题文本,最多30个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="typeId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        广告类型 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <AdvertisementTypeSelector
+                          value={field.value}
+                          onChange={field.onChange}
+                          placeholder="请选择广告类型"
+                          testId="type-selector"
+                        />
+                      </FormControl>
+                      <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="code-input" />
+                      </FormControl>
+                      <FormDescription>用于程序调用的唯一标识,最多20个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="imageFileId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>广告图片</FormLabel>
+                      <FormControl>
+                        <FileSelector
+                          value={field.value || undefined}
+                          onChange={field.onChange}
+                          maxSize={2}
+                          uploadPath="/advertisements"
+                          previewSize="medium"
+                          placeholder="选择广告图片"
+                          title="选择广告图片"
+                          description="上传新图片或从已有图片中选择"
+                          filterType="image"
+                        />
+                      </FormControl>
+                      <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="url"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>跳转链接</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入跳转链接" {...field} data-testid="url-input" />
+                      </FormControl>
+                      <FormDescription>点击广告后跳转的URL地址</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <div className="grid grid-cols-2 gap-4">
+                  <FormField
+                    control={createForm.control}
+                    name="actionType"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>跳转类型</FormLabel>
+                        <FormControl>
+                          <select
+                            {...field}
+                            className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                            value={field.value || 1}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="action-type-select"
+                          >
+                            <option value={0}>不跳转</option>
+                            <option value={1}>Web页面</option>
+                            <option value={2}>小程序页面</option>
+                          </select>
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+
+                  <FormField
+                    control={createForm.control}
+                    name="sort"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>排序值</FormLabel>
+                        <FormControl>
+                          <Input
+                            type="number"
+                            placeholder="排序值"
+                            {...field}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="sort-input"
+                          />
+                        </FormControl>
+                        <FormDescription>数值越大排序越靠前</FormDescription>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                </div>
+
+                <FormField
+                  control={createForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                          value={field.value || 1}
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                          data-testid="status-select"
+                        >
+                          <option value={1}>启用</option>
+                          <option value={0}>禁用</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending} data-testid="create-submit-button">
+                    创建
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            // 编辑表单(独立渲染)
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="title"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        标题 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入广告标题" {...field} data-testid="title-input" />
+                      </FormControl>
+                      <FormDescription>广告显示的标题文本,最多30个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="typeId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        广告类型 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <AdvertisementTypeSelector
+                          value={field.value}
+                          onChange={field.onChange}
+                          testId="type-selector"
+                        />
+                      </FormControl>
+                      <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="code-input" />
+                      </FormControl>
+                      <FormDescription>用于程序调用的唯一标识,最多20个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="imageFileId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>广告图片</FormLabel>
+                      <FormControl>
+                        <FileSelector
+                          value={field.value || undefined}
+                          onChange={field.onChange}
+                          maxSize={2}
+                          uploadPath="/advertisements"
+                          previewSize="medium"
+                          placeholder="选择广告图片"
+                          title="选择广告图片"
+                          description="上传新图片或从已有图片中选择"
+                          filterType="image"
+                        />
+                      </FormControl>
+                      <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="url"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>跳转链接</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入跳转链接" {...field} data-testid="url-input" />
+                      </FormControl>
+                      <FormDescription>点击广告后跳转的URL地址</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <div className="grid grid-cols-2 gap-4">
+                  <FormField
+                    control={updateForm.control}
+                    name="actionType"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>跳转类型</FormLabel>
+                        <FormControl>
+                          <select
+                            {...field}
+                            className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                            value={field.value || 1}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="action-type-select"
+                          >
+                            <option value={0}>不跳转</option>
+                            <option value={1}>Web页面</option>
+                            <option value={2}>小程序页面</option>
+                          </select>
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+
+                  <FormField
+                    control={updateForm.control}
+                    name="sort"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>排序值</FormLabel>
+                        <FormControl>
+                          <Input
+                            type="number"
+                            placeholder="排序值"
+                            {...field}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="sort-input"
+                          />
+                        </FormControl>
+                        <FormDescription>数值越大排序越靠前</FormDescription>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                </div>
+
+                <FormField
+                  control={updateForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                          value={field.value || 1}
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                          data-testid="status-select"
+                        >
+                          <option value={1}>启用</option>
+                          <option value={0}>禁用</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={updateMutation.isPending} data-testid="update-submit-button">
+                    更新
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个广告吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={confirmDelete}
+              disabled={deleteMutation.isPending}
+              data-testid="confirm-delete-button"
+            >
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};
+
+export default AdvertisementManagement;

+ 1 - 0
packages/advertisement-management-ui-mt/src/components/index.ts

@@ -0,0 +1 @@
+export { default as AdvertisementManagement } from './AdvertisementManagement';

+ 18 - 0
packages/advertisement-management-ui-mt/src/index.ts

@@ -0,0 +1,18 @@
+// 主包导出入口
+
+export { AdvertisementManagement } from './components';
+
+export {
+  AdvertisementClientManager,
+  advertisementClientManager,
+  advertisementClient
+} from './api';
+
+export type {
+  CreateAdvertisementRequest,
+  UpdateAdvertisementRequest,
+  AdvertisementResponse,
+  AdvertisementListResponse,
+  AdvertisementFormData,
+  AdvertisementSearchParams
+} from './types';

+ 24 - 0
packages/advertisement-management-ui-mt/src/types/advertisement.ts

@@ -0,0 +1,24 @@
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { advertisementClient } from '../api/advertisementClient';
+
+export type CreateAdvertisementRequest = InferRequestType<typeof advertisementClient.index.$post>['json'];
+export type UpdateAdvertisementRequest = InferRequestType<typeof advertisementClient[':id']['$put']>['json'];
+export type AdvertisementResponse = InferResponseType<typeof advertisementClient.index.$get, 200>['data'][0];
+export type AdvertisementListResponse = InferResponseType<typeof advertisementClient.index.$get, 200>;
+
+export interface AdvertisementFormData {
+  title: string;
+  typeId: number;
+  code: string;
+  url?: string;
+  imageFileId?: number;
+  sort: number;
+  status: number;
+  actionType: number;
+}
+
+export interface AdvertisementSearchParams {
+  page: number;
+  limit: number;
+  search: string;
+}

+ 8 - 0
packages/advertisement-management-ui-mt/src/types/index.ts

@@ -0,0 +1,8 @@
+export type {
+  CreateAdvertisementRequest,
+  UpdateAdvertisementRequest,
+  AdvertisementResponse,
+  AdvertisementListResponse,
+  AdvertisementFormData,
+  AdvertisementSearchParams
+} from './advertisement';

+ 348 - 0
packages/advertisement-management-ui-mt/tests/integration/advertisement-management.integration.test.tsx

@@ -0,0 +1,348 @@
+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 { AdvertisementManagement } from '../../src/components/AdvertisementManagement';
+import { advertisementClientManager } from '../../src/api/advertisementClient';
+
+// 完整的mock响应对象
+const createMockResponse = (status: number, data?: any) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  body: null,
+  bodyUsed: false,
+  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
+  headers: new Headers(),
+  url: '',
+  redirected: false,
+  type: 'basic' as ResponseType,
+  json: async () => data || {},
+  text: async () => '',
+  blob: async () => new Blob(),
+  arrayBuffer: async () => new ArrayBuffer(0),
+  formData: async () => new FormData(),
+  clone: function() { return this; }
+});
+
+// Mock API client
+vi.mock('../../src/api/advertisementClient', () => {
+  const mockAdvertisementClient = {
+    index: {
+      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
+        data: [
+          {
+            id: 1,
+            title: '测试广告',
+            alias: 'test-ad',
+            typeId: 1,
+            imageUrl: 'https://example.com/image.jpg',
+            linkUrl: 'https://example.com',
+            status: 1,
+            sortOrder: 1,
+            remark: '测试备注',
+            createdAt: '2024-01-01T00:00:00Z',
+            updatedAt: '2024-01-01T00:00:00Z',
+            createdBy: 1,
+            updatedBy: 1
+          }
+        ],
+        pagination: {
+          page: 1,
+          pageSize: 10,
+          total: 1,
+          totalPages: 1
+        }
+      }))),
+      $post: vi.fn(() => Promise.resolve(createMockResponse(201, { id: 2, title: '新广告' }))),
+    },
+    ':id': {
+      $put: vi.fn(() => Promise.resolve(createMockResponse(200, { id: 1, title: '更新后的广告' }))),
+      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))),
+    },
+  };
+
+  const mockAdvertisementClientManager = {
+    get: vi.fn(() => mockAdvertisementClient),
+  };
+
+  return {
+    advertisementClientManager: mockAdvertisementClientManager,
+    advertisementClient: mockAdvertisementClient,
+  };
+});
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(() => {}),
+    error: vi.fn(() => {}),
+  },
+}));
+
+// Mock FileSelector
+vi.mock('@d8d/file-management-ui', () => ({
+  FileSelector: ({ value, onChange, testId, maxSize, uploadPath, previewSize, filterType, ...props }: any) => (
+    <div data-testid="file-selector">
+      <input
+        type="number"
+        value={value || ''}
+        onChange={(e) => onChange?.(parseInt(e.target.value))}
+        data-testid="file-selector-input"
+        {...props}
+      />
+    </div>
+  ),
+}));
+
+// Mock AdvertisementTypeSelector
+vi.mock('@d8d/advertisement-type-management-ui', () => ({
+  AdvertisementTypeSelector: ({ value, onChange, testId, ...props }: any) => (
+    <div data-testid="advertisement-type-selector">
+      <select
+        value={value?.toString() || ''}
+        onChange={(e) => onChange?.(parseInt(e.target.value))}
+        data-testid="type-selector"
+        {...props}
+      >
+        <option value="1">首页轮播</option>
+        <option value="2">侧边栏广告</option>
+      </select>
+    </div>
+  ),
+}));
+
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component as any}
+    </QueryClientProvider>
+  );
+};
+
+describe('广告管理集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该完成完整的广告CRUD流程', async () => {
+    const mockAdvertisements = {
+      data: [
+        {
+          id: 1,
+          title: '测试广告',
+          code: 'test-ad',
+          typeId: 1,
+          advertisementType: { id: 1, name: '首页轮播' },
+          url: 'https://example.com',
+          imageFileId: 1,
+          imageFile: { id: 1, fullUrl: 'https://example.com/image.jpg' },
+          sort: 1,
+          status: 1,
+          actionType: 1,
+          createdAt: '2024-01-01T00:00:00Z',
+        },
+      ],
+      pagination: {
+        total: 1,
+        page: 1,
+        pageSize: 10,
+      },
+    };
+
+    const { toast } = await import('sonner');
+
+    // Mock initial advertisement list
+    const client = advertisementClientManager.get();
+    (client.index.$get as any).mockResolvedValue({
+      ...createMockResponse(200),
+      json: async () => mockAdvertisements
+    });
+
+    renderWithProviders(<AdvertisementManagement />);
+
+    // Wait for initial data to load
+    await waitFor(() => {
+      expect(screen.getByText('测试广告')).toBeInTheDocument();
+    });
+
+    // Test create advertisement
+    const createButton = screen.getByText('创建广告');
+    fireEvent.click(createButton);
+
+    // Fill create form
+    const titleInput = screen.getByTestId('title-input');
+    const codeInput = screen.getByTestId('code-input');
+    const typeSelector = screen.getByTestId('type-selector');
+
+    fireEvent.change(titleInput, { target: { value: '新广告' } });
+    fireEvent.change(codeInput, { target: { value: 'new-ad' } });
+    fireEvent.change(typeSelector, { target: { value: '1' } });
+
+    // Mock successful creation
+    (client.index.$post as any).mockResolvedValue(createMockResponse(201, { id: 2, title: '新广告' }));
+
+    const submitButton = screen.getByTestId('create-submit-button');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(client.index.$post).toHaveBeenCalledWith({
+        json: {
+          title: '新广告',
+          code: 'new-ad',
+          typeId: 1,
+          url: '',
+          imageFileId: undefined,
+          sort: 0,
+          status: 1,
+          actionType: 1
+        },
+      });
+      expect(toast.success).toHaveBeenCalledWith('广告创建成功');
+    });
+
+    // Test edit advertisement
+    const editButton = screen.getByTestId('edit-button-1');
+    fireEvent.click(editButton);
+
+    // Verify edit form is populated
+    await waitFor(() => {
+      expect(screen.getByDisplayValue('测试广告')).toBeInTheDocument();
+    });
+
+    // Update advertisement
+    const updateTitleInput = screen.getByDisplayValue('测试广告');
+    fireEvent.change(updateTitleInput, { target: { value: '更新后的广告' } });
+
+    // Mock successful update
+    (client[':id']['$put'] as any).mockResolvedValue(createMockResponse(200));
+
+    const updateButton = screen.getByTestId('update-submit-button');
+    fireEvent.click(updateButton);
+
+    await waitFor(() => {
+      expect(client[':id']['$put']).toHaveBeenCalledWith({
+        param: { id: 1 },
+        json: {
+          title: '更新后的广告',
+          typeId: 1,
+          code: 'test-ad',
+          url: 'https://example.com',
+          imageFileId: 1,
+          sort: 1,
+          status: 1,
+          actionType: 1
+        },
+      });
+      expect(toast.success).toHaveBeenCalledWith('广告更新成功');
+    });
+
+    // Test delete advertisement
+    const deleteButton = screen.getByTestId('delete-button-1');
+    fireEvent.click(deleteButton);
+
+    // Confirm deletion
+    expect(screen.getByText('确认删除')).toBeInTheDocument();
+
+    // Mock successful deletion
+    (client[':id']['$delete'] as any).mockResolvedValue({
+      status: 204,
+      ok: true,
+      body: null,
+      bodyUsed: false,
+      statusText: 'No Content',
+      headers: new Headers(),
+      url: '',
+      redirected: false,
+      type: 'basic' as ResponseType,
+      json: async () => ({}),
+      text: async () => '',
+      blob: async () => new Blob(),
+      arrayBuffer: async () => new ArrayBuffer(0),
+      formData: async () => new FormData(),
+      clone: function() { return this; }
+    });
+
+    const confirmDeleteButton = screen.getByTestId('confirm-delete-button');
+    fireEvent.click(confirmDeleteButton);
+
+    await waitFor(() => {
+      expect(client[':id']['$delete']).toHaveBeenCalledWith({
+        param: { id: 1 },
+      });
+      expect(toast.success).toHaveBeenCalledWith('广告删除成功');
+    });
+  });
+
+  it('应该优雅处理API错误', async () => {
+    const client = advertisementClientManager.get();
+    const { toast } = await import('sonner');
+
+    // Mock API error
+    (client.index.$get as any).mockRejectedValue(new Error('API Error'));
+
+    renderWithProviders(<AdvertisementManagement />);
+
+    // Should handle error without crashing
+    await waitFor(() => {
+      expect(screen.getByText('广告管理')).toBeInTheDocument();
+    });
+
+    // Test create advertisement error
+    const createButton = screen.getByText('创建广告');
+    fireEvent.click(createButton);
+
+    const titleInput = screen.getByTestId('title-input');
+    const codeInput = screen.getByTestId('code-input');
+
+    fireEvent.change(titleInput, { target: { value: '测试广告' } });
+    fireEvent.change(codeInput, { target: { value: 'test-ad' } });
+
+    // Mock creation error
+    (client.index.$post as any).mockRejectedValue(new Error('Creation failed'));
+
+    const submitButton = screen.getByTestId('create-submit-button');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(toast.error).toHaveBeenCalledWith('Creation failed');
+    });
+  });
+
+  it('应该处理搜索功能', async () => {
+    const client = advertisementClientManager.get();
+    const mockAdvertisements = {
+      data: [],
+      pagination: { total: 0, page: 1, pageSize: 10 },
+    };
+
+    (client.index.$get as any).mockResolvedValue(createMockResponse(200, mockAdvertisements));
+
+    renderWithProviders(<AdvertisementManagement />);
+
+    // Test search
+    const searchInput = screen.getByTestId('search-input');
+    fireEvent.change(searchInput, { target: { value: '搜索关键词' } });
+
+    const searchButton = screen.getByText('搜索');
+    fireEvent.click(searchButton);
+
+    await waitFor(() => {
+      expect(client.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '搜索关键词',
+        },
+      });
+    });
+  });
+});

+ 14 - 0
packages/advertisement-management-ui-mt/tests/setup.ts

@@ -0,0 +1,14 @@
+import '@testing-library/jest-dom';
+import { vi } from 'vitest';
+
+
+
+// Mock sonner
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+    warning: vi.fn(),
+    info: vi.fn()
+  }
+}));

+ 36 - 0
packages/advertisement-management-ui-mt/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/advertisement-management-ui-mt/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'
+    }
+  }
+});

+ 43 - 0
packages/advertisement-management-ui/eslint.config.js

@@ -0,0 +1,43 @@
+import js from '@eslint/js';
+import tseslint from '@typescript-eslint/eslint-plugin';
+import tsparser from '@typescript-eslint/parser';
+import reactPlugin from 'eslint-plugin-react';
+import reactHooks from 'eslint-plugin-react-hooks';
+
+export default [
+  {
+    files: ['**/*.{js,jsx,ts,tsx}'],
+    languageOptions: {
+      parser: tsparser,
+      ecmaVersion: 'latest',
+      sourceType: 'module',
+      parserOptions: {
+        ecmaFeatures: {
+          jsx: true
+        }
+      }
+    },
+    plugins: {
+      '@typescript-eslint': tseslint,
+      'react': reactPlugin,
+      'react-hooks': reactHooks
+    },
+    rules: {
+      ...js.configs.recommended.rules,
+      ...tseslint.configs.recommended.rules,
+      ...reactPlugin.configs.recommended.rules,
+      ...reactHooks.configs.recommended.rules,
+      'react/react-in-jsx-scope': 'off',
+      '@typescript-eslint/no-unused-vars': ['error', {
+        argsIgnorePattern: '^_',
+        varsIgnorePattern: '^_'
+      }],
+      '@typescript-eslint/no-explicit-any': 'warn'
+    },
+    settings: {
+      react: {
+        version: 'detect'
+      }
+    }
+  }
+];

+ 92 - 0
packages/advertisement-management-ui/package.json

@@ -0,0 +1,92 @@
+{
+  "name": "@d8d/advertisement-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/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/advertisements-module": "workspace:*",
+    "@d8d/file-management-ui": "workspace:*",
+    "@d8d/advertisement-type-management-ui": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "date-fns": "^4.1.0",
+    "hono": "^4.8.5",
+    "lucide-react": "^0.536.0",
+    "react": "^19.1.0",
+    "react-dom": "^19.1.0",
+    "react-hook-form": "^7.61.1",
+    "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": [
+    "advertisement",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud",
+    "banner"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 44 - 0
packages/advertisement-management-ui/src/api/advertisementClient.ts

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

+ 5 - 0
packages/advertisement-management-ui/src/api/index.ts

@@ -0,0 +1,5 @@
+export {
+  AdvertisementClientManager,
+  advertisementClientManager,
+  advertisementClient
+} from './advertisementClient';

+ 763 - 0
packages/advertisement-management-ui/src/components/AdvertisementManagement.tsx

@@ -0,0 +1,763 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation } from '@tanstack/react-query';
+import { Plus, Edit, Trash2, Search } from 'lucide-react';
+import { format } from 'date-fns';
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+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 { Skeleton } from '@d8d/shared-ui-components/components/ui/skeleton';
+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 { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { toast } from 'sonner';
+import { DataTablePagination } from '@d8d/shared-ui-components/components/admin/DataTablePagination';
+import { FileSelector } from '@d8d/file-management-ui';
+import { AdvertisementTypeSelector } from '@d8d/advertisement-type-management-ui';
+import { advertisementClientManager } from '../api/advertisementClient';
+import { CreateAdvertisementDto, UpdateAdvertisementDto } from '@d8d/advertisements-module/schemas';
+import type { AdvertisementSearchParams, CreateAdvertisementRequest, UpdateAdvertisementRequest, AdvertisementResponse } from '../types';
+
+type CreateRequest = CreateAdvertisementRequest;
+type UpdateRequest = UpdateAdvertisementRequest;
+
+const createFormSchema = CreateAdvertisementDto;
+const updateFormSchema = UpdateAdvertisementDto;
+
+export const AdvertisementManagement: React.FC = () => {
+  const [searchParams, setSearchParams] = useState<AdvertisementSearchParams>({ page: 1, limit: 10, search: '' });
+  const [isModalOpen, setIsModalOpen] = useState(false);
+  const [editingAdvertisement, setEditingAdvertisement] = useState<AdvertisementResponse | null>(null);
+  const [isCreateForm, setIsCreateForm] = useState(true);
+  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+  const [advertisementToDelete, setAdvertisementToDelete] = useState<number | null>(null);
+
+  // 表单实例
+  const createForm = useForm<CreateRequest>({
+    resolver: zodResolver(createFormSchema),
+    defaultValues: {
+      title: '',
+      typeId: 1,
+      code: '',
+      url: '',
+      imageFileId: undefined,
+      sort: 0,
+      status: 1,
+      actionType: 1
+    }
+  });
+
+  const updateForm = useForm<UpdateRequest>({
+    resolver: zodResolver(updateFormSchema),
+    defaultValues: {}
+  });
+
+  // 数据查询
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['advertisements', searchParams],
+    queryFn: async () => {
+      const res = await advertisementClientManager.get().index.$get({
+        query: {
+          page: searchParams.page,
+          pageSize: searchParams.limit,
+          keyword: searchParams.search
+        }
+      });
+      if (res.status !== 200) throw new Error('获取广告列表失败');
+      return await res.json();
+    }
+  });
+
+  // 创建广告
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateRequest) => {
+      const res = await advertisementClientManager.get().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 instanceof Error ? error.message : '创建广告失败');
+    }
+  });
+
+  // 更新广告
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateRequest }) => {
+      const res = await advertisementClientManager.get()[':id']['$put']({
+        param: { id },
+        json: data
+      });
+      if (res.status !== 200) throw new Error('更新广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告更新成功');
+      setIsModalOpen(false);
+      setEditingAdvertisement(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '更新广告失败');
+    }
+  });
+
+  // 删除广告
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      const res = await advertisementClientManager.get()[':id']['$delete']({
+        param: { id }
+      });
+      if (res.status !== 204) throw new Error('删除广告失败');
+      return await res.json();
+    },
+    onSuccess: () => {
+      toast.success('广告删除成功');
+      setDeleteDialogOpen(false);
+      setAdvertisementToDelete(null);
+      refetch();
+    },
+    onError: (error) => {
+      toast.error(error instanceof Error ? error.message : '删除广告失败');
+    }
+  });
+
+  // 处理搜索
+  const handleSearch = (e: React.FormEvent) => {
+    e.preventDefault();
+    setSearchParams(prev => ({ ...prev, page: 1 }));
+    refetch();
+  };
+
+  // 处理创建广告
+  const handleCreateAdvertisement = () => {
+    setIsCreateForm(true);
+    setEditingAdvertisement(null);
+    createForm.reset();
+    setIsModalOpen(true);
+  };
+
+  // 处理编辑广告
+  const handleEditAdvertisement = (advertisement: AdvertisementResponse) => {
+    setIsCreateForm(false);
+    setEditingAdvertisement(advertisement);
+    updateForm.reset({
+      title: advertisement.title || undefined,
+      typeId: advertisement.typeId || undefined,
+      code: advertisement.code || undefined,
+      url: advertisement.url || undefined,
+      imageFileId: advertisement.imageFileId || undefined,
+      sort: advertisement.sort || undefined,
+      status: advertisement.status || undefined,
+      actionType: advertisement.actionType || undefined
+    });
+    setIsModalOpen(true);
+  };
+
+  // 处理删除广告
+  const handleDeleteAdvertisement = (id: number) => {
+    setAdvertisementToDelete(id);
+    setDeleteDialogOpen(true);
+  };
+
+  // 确认删除
+  const confirmDelete = () => {
+    if (advertisementToDelete) {
+      deleteMutation.mutate(advertisementToDelete);
+    }
+  };
+
+  // 处理创建表单提交
+  const handleCreateSubmit = async (data: CreateRequest) => {
+    try {
+      await createMutation.mutateAsync(data);
+    } catch (error) {
+      throw error;
+    }
+  };
+
+  // 处理编辑表单提交
+  const handleUpdateSubmit = async (data: UpdateRequest) => {
+    if (!editingAdvertisement) return;
+
+    try {
+      await updateMutation.mutateAsync({
+        id: editingAdvertisement.id,
+        data
+      });
+    } catch (error) {
+      throw error;
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      <div className="flex justify-between items-center">
+        <h1 className="text-2xl font-bold">广告管理</h1>
+        <Button onClick={handleCreateAdvertisement}>
+          <Plus className="mr-2 h-4 w-4" />
+          创建广告
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>广告列表</CardTitle>
+          <CardDescription>管理网站的所有广告内容</CardDescription>
+        </CardHeader>
+        <CardContent>
+          <div className="mb-4">
+            <form onSubmit={handleSearch} className="flex gap-2">
+              <div className="relative flex-1 max-w-sm">
+                <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
+                <Input
+                  placeholder="搜索广告标题或别名..."
+                  value={searchParams.search}
+                  onChange={(e) => setSearchParams(prev => ({ ...prev, search: e.target.value }))}
+                  className="pl-8"
+                  data-testid="search-input"
+                />
+              </div>
+              <Button type="submit" variant="outline">
+                搜索
+              </Button>
+            </form>
+          </div>
+
+          <div className="rounded-md border">
+            <div className="relative w-full overflow-x-auto">
+              <Table>
+                <TableHeader>
+                  <TableRow>
+                    <TableHead>ID</TableHead>
+                    <TableHead>标题</TableHead>
+                    <TableHead>类型</TableHead>
+                    <TableHead>别名</TableHead>
+                    <TableHead>图片</TableHead>
+                    <TableHead>状态</TableHead>
+                    <TableHead>排序</TableHead>
+                    <TableHead>创建时间</TableHead>
+                    <TableHead className="text-right">操作</TableHead>
+                  </TableRow>
+                </TableHeader>
+                <TableBody>
+                  {isLoading ? (
+                    Array.from({ length: 5 }).map((_, index) => (
+                      <TableRow key={index}>
+                        <TableCell>
+                          <Skeleton className="h-4 w-8" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-32" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-20" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-24" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-8 w-8 rounded" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-6 w-12 rounded-full" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-8" />
+                        </TableCell>
+                        <TableCell>
+                          <Skeleton className="h-4 w-24" />
+                        </TableCell>
+                        <TableCell>
+                          <div className="flex justify-end gap-2">
+                            <Skeleton className="h-8 w-8 rounded" />
+                            <Skeleton className="h-8 w-8 rounded" />
+                          </div>
+                        </TableCell>
+                      </TableRow>
+                    ))
+                  ) : data?.data && data.data.length > 0 ? (
+                    data.data.map((advertisement) => (
+                      <TableRow key={advertisement.id}>
+                        <TableCell>{advertisement.id}</TableCell>
+                        <TableCell>{advertisement.title || '-'}</TableCell>
+                        <TableCell>
+                          {advertisement.advertisementType?.name || '-'}
+                        </TableCell>
+                        <TableCell>
+                          <code className="text-xs bg-muted px-1 rounded">{advertisement.code || '-'}</code>
+                        </TableCell>
+                        <TableCell>
+                          {advertisement.imageFile?.fullUrl ? (
+                            <img
+                              src={advertisement.imageFile.fullUrl}
+                              alt={advertisement.title || '广告图片'}
+                              className="w-16 h-10 object-cover rounded"
+                              onError={(e) => {
+                                e.currentTarget.src = '/placeholder.png';
+                              }}
+                            />
+                          ) : (
+                            <span className="text-muted-foreground text-xs">无图片</span>
+                          )}
+                        </TableCell>
+                        <TableCell>
+                          <Badge variant={advertisement.status === 1 ? 'default' : 'secondary'}>
+                            {advertisement.status === 1 ? '启用' : '禁用'}
+                          </Badge>
+                        </TableCell>
+                        <TableCell>{advertisement.sort}</TableCell>
+                        <TableCell>
+                          {advertisement.createdAt ? format(new Date(advertisement.createdAt), 'yyyy-MM-dd HH:mm') : '-'}
+                        </TableCell>
+                        <TableCell className="text-right">
+                          <div className="flex justify-end gap-2">
+                            <Button
+                              variant="ghost"
+                              size="icon"
+                              onClick={() => handleEditAdvertisement(advertisement)}
+                              data-testid={`edit-button-${advertisement.id}`}
+                            >
+                              <Edit className="h-4 w-4" />
+                            </Button>
+                            <Button
+                              variant="ghost"
+                              size="icon"
+                              onClick={() => handleDeleteAdvertisement(advertisement.id)}
+                              data-testid={`delete-button-${advertisement.id}`}
+                            >
+                              <Trash2 className="h-4 w-4" />
+                            </Button>
+                          </div>
+                        </TableCell>
+                      </TableRow>
+                    ))
+                  ) : (
+                    <TableRow>
+                      <TableCell colSpan={9} className="text-center py-8">
+                        <p className="text-muted-foreground">暂无广告数据</p>
+                      </TableCell>
+                    </TableRow>
+                  )}
+                </TableBody>
+              </Table>
+            </div>
+          </div>
+
+
+        <DataTablePagination
+          currentPage={searchParams.page}
+          pageSize={searchParams.limit}
+          totalCount={data?.pagination.total || 0}
+          onPageChange={(page, limit) => setSearchParams(prev => ({ ...prev, page, limit }))}
+        />
+      </CardContent>
+    </Card>
+
+    {/* 创建/编辑对话框 */}
+    <Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
+        <DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
+          <DialogHeader>
+            <DialogTitle>{isCreateForm ? '创建广告' : '编辑广告'}</DialogTitle>
+            <DialogDescription>
+              {isCreateForm ? '创建一个新的广告' : '编辑现有广告信息'}
+            </DialogDescription>
+          </DialogHeader>
+
+          {isCreateForm ? (
+            // 创建表单(独立渲染)
+            <Form {...createForm}>
+              <form onSubmit={createForm.handleSubmit(handleCreateSubmit)} className="space-y-4">
+                <FormField
+                  control={createForm.control}
+                  name="title"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        标题 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入广告标题" {...field} data-testid="title-input" />
+                      </FormControl>
+                      <FormDescription>广告显示的标题文本,最多30个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="typeId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        广告类型 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <AdvertisementTypeSelector
+                          value={field.value}
+                          onChange={field.onChange}
+                          placeholder="请选择广告类型"
+                          testId="type-selector"
+                        />
+                      </FormControl>
+                      <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="code-input" />
+                      </FormControl>
+                      <FormDescription>用于程序调用的唯一标识,最多20个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="imageFileId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>广告图片</FormLabel>
+                      <FormControl>
+                        <FileSelector
+                          value={field.value || undefined}
+                          onChange={field.onChange}
+                          maxSize={2}
+                          uploadPath="/advertisements"
+                          previewSize="medium"
+                          placeholder="选择广告图片"
+                          title="选择广告图片"
+                          description="上传新图片或从已有图片中选择"
+                          filterType="image"
+                        />
+                      </FormControl>
+                      <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={createForm.control}
+                  name="url"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>跳转链接</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入跳转链接" {...field} data-testid="url-input" />
+                      </FormControl>
+                      <FormDescription>点击广告后跳转的URL地址</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <div className="grid grid-cols-2 gap-4">
+                  <FormField
+                    control={createForm.control}
+                    name="actionType"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>跳转类型</FormLabel>
+                        <FormControl>
+                          <select
+                            {...field}
+                            className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                            value={field.value || 1}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="action-type-select"
+                          >
+                            <option value={0}>不跳转</option>
+                            <option value={1}>Web页面</option>
+                            <option value={2}>小程序页面</option>
+                          </select>
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+
+                  <FormField
+                    control={createForm.control}
+                    name="sort"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>排序值</FormLabel>
+                        <FormControl>
+                          <Input
+                            type="number"
+                            placeholder="排序值"
+                            {...field}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="sort-input"
+                          />
+                        </FormControl>
+                        <FormDescription>数值越大排序越靠前</FormDescription>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                </div>
+
+                <FormField
+                  control={createForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                          value={field.value || 1}
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                          data-testid="status-select"
+                        >
+                          <option value={1}>启用</option>
+                          <option value={0}>禁用</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={createMutation.isPending} data-testid="create-submit-button">
+                    创建
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          ) : (
+            // 编辑表单(独立渲染)
+            <Form {...updateForm}>
+              <form onSubmit={updateForm.handleSubmit(handleUpdateSubmit)} className="space-y-4">
+                <FormField
+                  control={updateForm.control}
+                  name="title"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        标题 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入广告标题" {...field} data-testid="title-input" />
+                      </FormControl>
+                      <FormDescription>广告显示的标题文本,最多30个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="typeId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel className="flex items-center">
+                        广告类型 <span className="text-red-500 ml-1">*</span>
+                      </FormLabel>
+                      <FormControl>
+                        <AdvertisementTypeSelector
+                          value={field.value}
+                          onChange={field.onChange}
+                          testId="type-selector"
+                        />
+                      </FormControl>
+                      <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="code-input" />
+                      </FormControl>
+                      <FormDescription>用于程序调用的唯一标识,最多20个字符</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="imageFileId"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>广告图片</FormLabel>
+                      <FormControl>
+                        <FileSelector
+                          value={field.value || undefined}
+                          onChange={field.onChange}
+                          maxSize={2}
+                          uploadPath="/advertisements"
+                          previewSize="medium"
+                          placeholder="选择广告图片"
+                          title="选择广告图片"
+                          description="上传新图片或从已有图片中选择"
+                          filterType="image"
+                        />
+                      </FormControl>
+                      <FormDescription>推荐尺寸:1200x400px,支持jpg、png格式</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <FormField
+                  control={updateForm.control}
+                  name="url"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>跳转链接</FormLabel>
+                      <FormControl>
+                        <Input placeholder="请输入跳转链接" {...field} data-testid="url-input" />
+                      </FormControl>
+                      <FormDescription>点击广告后跳转的URL地址</FormDescription>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <div className="grid grid-cols-2 gap-4">
+                  <FormField
+                    control={updateForm.control}
+                    name="actionType"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>跳转类型</FormLabel>
+                        <FormControl>
+                          <select
+                            {...field}
+                            className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                            value={field.value || 1}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="action-type-select"
+                          >
+                            <option value={0}>不跳转</option>
+                            <option value={1}>Web页面</option>
+                            <option value={2}>小程序页面</option>
+                          </select>
+                        </FormControl>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+
+                  <FormField
+                    control={updateForm.control}
+                    name="sort"
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>排序值</FormLabel>
+                        <FormControl>
+                          <Input
+                            type="number"
+                            placeholder="排序值"
+                            {...field}
+                            onChange={(e) => field.onChange(parseInt(e.target.value))}
+                            data-testid="sort-input"
+                          />
+                        </FormControl>
+                        <FormDescription>数值越大排序越靠前</FormDescription>
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                </div>
+
+                <FormField
+                  control={updateForm.control}
+                  name="status"
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>状态</FormLabel>
+                      <FormControl>
+                        <select
+                          {...field}
+                          className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
+                          value={field.value || 1}
+                          onChange={(e) => field.onChange(parseInt(e.target.value))}
+                          data-testid="status-select"
+                        >
+                          <option value={1}>启用</option>
+                          <option value={0}>禁用</option>
+                        </select>
+                      </FormControl>
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                <DialogFooter>
+                  <Button type="button" variant="outline" onClick={() => setIsModalOpen(false)}>
+                    取消
+                  </Button>
+                  <Button type="submit" disabled={updateMutation.isPending} data-testid="update-submit-button">
+                    更新
+                  </Button>
+                </DialogFooter>
+              </form>
+            </Form>
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
+        <DialogContent>
+          <DialogHeader>
+            <DialogTitle>确认删除</DialogTitle>
+            <DialogDescription>
+              确定要删除这个广告吗?此操作无法撤销。
+            </DialogDescription>
+          </DialogHeader>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)}>
+              取消
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={confirmDelete}
+              disabled={deleteMutation.isPending}
+              data-testid="confirm-delete-button"
+            >
+              {deleteMutation.isPending ? '删除中...' : '删除'}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+};
+
+export default AdvertisementManagement;

+ 1 - 0
packages/advertisement-management-ui/src/components/index.ts

@@ -0,0 +1 @@
+export { default as AdvertisementManagement } from './AdvertisementManagement';

+ 18 - 0
packages/advertisement-management-ui/src/index.ts

@@ -0,0 +1,18 @@
+// 主包导出入口
+
+export { AdvertisementManagement } from './components';
+
+export {
+  AdvertisementClientManager,
+  advertisementClientManager,
+  advertisementClient
+} from './api';
+
+export type {
+  CreateAdvertisementRequest,
+  UpdateAdvertisementRequest,
+  AdvertisementResponse,
+  AdvertisementListResponse,
+  AdvertisementFormData,
+  AdvertisementSearchParams
+} from './types';

+ 24 - 0
packages/advertisement-management-ui/src/types/advertisement.ts

@@ -0,0 +1,24 @@
+import type { InferRequestType, InferResponseType } from 'hono/client';
+import { advertisementClient } from '../api/advertisementClient';
+
+export type CreateAdvertisementRequest = InferRequestType<typeof advertisementClient.index.$post>['json'];
+export type UpdateAdvertisementRequest = InferRequestType<typeof advertisementClient[':id']['$put']>['json'];
+export type AdvertisementResponse = InferResponseType<typeof advertisementClient.index.$get, 200>['data'][0];
+export type AdvertisementListResponse = InferResponseType<typeof advertisementClient.index.$get, 200>;
+
+export interface AdvertisementFormData {
+  title: string;
+  typeId: number;
+  code: string;
+  url?: string;
+  imageFileId?: number;
+  sort: number;
+  status: number;
+  actionType: number;
+}
+
+export interface AdvertisementSearchParams {
+  page: number;
+  limit: number;
+  search: string;
+}

+ 8 - 0
packages/advertisement-management-ui/src/types/index.ts

@@ -0,0 +1,8 @@
+export type {
+  CreateAdvertisementRequest,
+  UpdateAdvertisementRequest,
+  AdvertisementResponse,
+  AdvertisementListResponse,
+  AdvertisementFormData,
+  AdvertisementSearchParams
+} from './advertisement';

+ 348 - 0
packages/advertisement-management-ui/tests/integration/advertisement-management.integration.test.tsx

@@ -0,0 +1,348 @@
+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 { AdvertisementManagement } from '../../src/components/AdvertisementManagement';
+import { advertisementClientManager } from '../../src/api/advertisementClient';
+
+// 完整的mock响应对象
+const createMockResponse = (status: number, data?: any) => ({
+  status,
+  ok: status >= 200 && status < 300,
+  body: null,
+  bodyUsed: false,
+  statusText: status === 200 ? 'OK' : status === 201 ? 'Created' : status === 204 ? 'No Content' : 'Error',
+  headers: new Headers(),
+  url: '',
+  redirected: false,
+  type: 'basic' as ResponseType,
+  json: async () => data || {},
+  text: async () => '',
+  blob: async () => new Blob(),
+  arrayBuffer: async () => new ArrayBuffer(0),
+  formData: async () => new FormData(),
+  clone: function() { return this; }
+});
+
+// Mock API client
+vi.mock('../../src/api/advertisementClient', () => {
+  const mockAdvertisementClient = {
+    index: {
+      $get: vi.fn(() => Promise.resolve(createMockResponse(200, {
+        data: [
+          {
+            id: 1,
+            title: '测试广告',
+            alias: 'test-ad',
+            typeId: 1,
+            imageUrl: 'https://example.com/image.jpg',
+            linkUrl: 'https://example.com',
+            status: 1,
+            sortOrder: 1,
+            remark: '测试备注',
+            createdAt: '2024-01-01T00:00:00Z',
+            updatedAt: '2024-01-01T00:00:00Z',
+            createdBy: 1,
+            updatedBy: 1
+          }
+        ],
+        pagination: {
+          page: 1,
+          pageSize: 10,
+          total: 1,
+          totalPages: 1
+        }
+      }))),
+      $post: vi.fn(() => Promise.resolve(createMockResponse(201, { id: 2, title: '新广告' }))),
+    },
+    ':id': {
+      $put: vi.fn(() => Promise.resolve(createMockResponse(200, { id: 1, title: '更新后的广告' }))),
+      $delete: vi.fn(() => Promise.resolve(createMockResponse(204))),
+    },
+  };
+
+  const mockAdvertisementClientManager = {
+    get: vi.fn(() => mockAdvertisementClient),
+  };
+
+  return {
+    advertisementClientManager: mockAdvertisementClientManager,
+    advertisementClient: mockAdvertisementClient,
+  };
+});
+
+// Mock toast
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(() => {}),
+    error: vi.fn(() => {}),
+  },
+}));
+
+// Mock FileSelector
+vi.mock('@d8d/file-management-ui', () => ({
+  FileSelector: ({ value, onChange, testId, maxSize, uploadPath, previewSize, filterType, ...props }: any) => (
+    <div data-testid="file-selector">
+      <input
+        type="number"
+        value={value || ''}
+        onChange={(e) => onChange?.(parseInt(e.target.value))}
+        data-testid="file-selector-input"
+        {...props}
+      />
+    </div>
+  ),
+}));
+
+// Mock AdvertisementTypeSelector
+vi.mock('@d8d/advertisement-type-management-ui', () => ({
+  AdvertisementTypeSelector: ({ value, onChange, testId, ...props }: any) => (
+    <div data-testid="advertisement-type-selector">
+      <select
+        value={value?.toString() || ''}
+        onChange={(e) => onChange?.(parseInt(e.target.value))}
+        data-testid="type-selector"
+        {...props}
+      >
+        <option value="1">首页轮播</option>
+        <option value="2">侧边栏广告</option>
+      </select>
+    </div>
+  ),
+}));
+
+const createTestQueryClient = () =>
+  new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+
+const renderWithProviders = (component: React.ReactElement) => {
+  const queryClient = createTestQueryClient();
+  return render(
+    <QueryClientProvider client={queryClient}>
+      {component as any}
+    </QueryClientProvider>
+  );
+};
+
+describe('广告管理集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该完成完整的广告CRUD流程', async () => {
+    const mockAdvertisements = {
+      data: [
+        {
+          id: 1,
+          title: '测试广告',
+          code: 'test-ad',
+          typeId: 1,
+          advertisementType: { id: 1, name: '首页轮播' },
+          url: 'https://example.com',
+          imageFileId: 1,
+          imageFile: { id: 1, fullUrl: 'https://example.com/image.jpg' },
+          sort: 1,
+          status: 1,
+          actionType: 1,
+          createdAt: '2024-01-01T00:00:00Z',
+        },
+      ],
+      pagination: {
+        total: 1,
+        page: 1,
+        pageSize: 10,
+      },
+    };
+
+    const { toast } = await import('sonner');
+
+    // Mock initial advertisement list
+    const client = advertisementClientManager.get();
+    (client.index.$get as any).mockResolvedValue({
+      ...createMockResponse(200),
+      json: async () => mockAdvertisements
+    });
+
+    renderWithProviders(<AdvertisementManagement />);
+
+    // Wait for initial data to load
+    await waitFor(() => {
+      expect(screen.getByText('测试广告')).toBeInTheDocument();
+    });
+
+    // Test create advertisement
+    const createButton = screen.getByText('创建广告');
+    fireEvent.click(createButton);
+
+    // Fill create form
+    const titleInput = screen.getByTestId('title-input');
+    const codeInput = screen.getByTestId('code-input');
+    const typeSelector = screen.getByTestId('type-selector');
+
+    fireEvent.change(titleInput, { target: { value: '新广告' } });
+    fireEvent.change(codeInput, { target: { value: 'new-ad' } });
+    fireEvent.change(typeSelector, { target: { value: '1' } });
+
+    // Mock successful creation
+    (client.index.$post as any).mockResolvedValue(createMockResponse(201, { id: 2, title: '新广告' }));
+
+    const submitButton = screen.getByTestId('create-submit-button');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(client.index.$post).toHaveBeenCalledWith({
+        json: {
+          title: '新广告',
+          code: 'new-ad',
+          typeId: 1,
+          url: '',
+          imageFileId: undefined,
+          sort: 0,
+          status: 1,
+          actionType: 1
+        },
+      });
+      expect(toast.success).toHaveBeenCalledWith('广告创建成功');
+    });
+
+    // Test edit advertisement
+    const editButton = screen.getByTestId('edit-button-1');
+    fireEvent.click(editButton);
+
+    // Verify edit form is populated
+    await waitFor(() => {
+      expect(screen.getByDisplayValue('测试广告')).toBeInTheDocument();
+    });
+
+    // Update advertisement
+    const updateTitleInput = screen.getByDisplayValue('测试广告');
+    fireEvent.change(updateTitleInput, { target: { value: '更新后的广告' } });
+
+    // Mock successful update
+    (client[':id']['$put'] as any).mockResolvedValue(createMockResponse(200));
+
+    const updateButton = screen.getByTestId('update-submit-button');
+    fireEvent.click(updateButton);
+
+    await waitFor(() => {
+      expect(client[':id']['$put']).toHaveBeenCalledWith({
+        param: { id: 1 },
+        json: {
+          title: '更新后的广告',
+          typeId: 1,
+          code: 'test-ad',
+          url: 'https://example.com',
+          imageFileId: 1,
+          sort: 1,
+          status: 1,
+          actionType: 1
+        },
+      });
+      expect(toast.success).toHaveBeenCalledWith('广告更新成功');
+    });
+
+    // Test delete advertisement
+    const deleteButton = screen.getByTestId('delete-button-1');
+    fireEvent.click(deleteButton);
+
+    // Confirm deletion
+    expect(screen.getByText('确认删除')).toBeInTheDocument();
+
+    // Mock successful deletion
+    (client[':id']['$delete'] as any).mockResolvedValue({
+      status: 204,
+      ok: true,
+      body: null,
+      bodyUsed: false,
+      statusText: 'No Content',
+      headers: new Headers(),
+      url: '',
+      redirected: false,
+      type: 'basic' as ResponseType,
+      json: async () => ({}),
+      text: async () => '',
+      blob: async () => new Blob(),
+      arrayBuffer: async () => new ArrayBuffer(0),
+      formData: async () => new FormData(),
+      clone: function() { return this; }
+    });
+
+    const confirmDeleteButton = screen.getByTestId('confirm-delete-button');
+    fireEvent.click(confirmDeleteButton);
+
+    await waitFor(() => {
+      expect(client[':id']['$delete']).toHaveBeenCalledWith({
+        param: { id: 1 },
+      });
+      expect(toast.success).toHaveBeenCalledWith('广告删除成功');
+    });
+  });
+
+  it('应该优雅处理API错误', async () => {
+    const client = advertisementClientManager.get();
+    const { toast } = await import('sonner');
+
+    // Mock API error
+    (client.index.$get as any).mockRejectedValue(new Error('API Error'));
+
+    renderWithProviders(<AdvertisementManagement />);
+
+    // Should handle error without crashing
+    await waitFor(() => {
+      expect(screen.getByText('广告管理')).toBeInTheDocument();
+    });
+
+    // Test create advertisement error
+    const createButton = screen.getByText('创建广告');
+    fireEvent.click(createButton);
+
+    const titleInput = screen.getByTestId('title-input');
+    const codeInput = screen.getByTestId('code-input');
+
+    fireEvent.change(titleInput, { target: { value: '测试广告' } });
+    fireEvent.change(codeInput, { target: { value: 'test-ad' } });
+
+    // Mock creation error
+    (client.index.$post as any).mockRejectedValue(new Error('Creation failed'));
+
+    const submitButton = screen.getByTestId('create-submit-button');
+    fireEvent.click(submitButton);
+
+    await waitFor(() => {
+      expect(toast.error).toHaveBeenCalledWith('Creation failed');
+    });
+  });
+
+  it('应该处理搜索功能', async () => {
+    const client = advertisementClientManager.get();
+    const mockAdvertisements = {
+      data: [],
+      pagination: { total: 0, page: 1, pageSize: 10 },
+    };
+
+    (client.index.$get as any).mockResolvedValue(createMockResponse(200, mockAdvertisements));
+
+    renderWithProviders(<AdvertisementManagement />);
+
+    // Test search
+    const searchInput = screen.getByTestId('search-input');
+    fireEvent.change(searchInput, { target: { value: '搜索关键词' } });
+
+    const searchButton = screen.getByText('搜索');
+    fireEvent.click(searchButton);
+
+    await waitFor(() => {
+      expect(client.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '搜索关键词',
+        },
+      });
+    });
+  });
+});

+ 14 - 0
packages/advertisement-management-ui/tests/setup.ts

@@ -0,0 +1,14 @@
+import '@testing-library/jest-dom';
+import { vi } from 'vitest';
+
+
+
+// Mock sonner
+vi.mock('sonner', () => ({
+  toast: {
+    success: vi.fn(),
+    error: vi.fn(),
+    warning: vi.fn(),
+    info: vi.fn()
+  }
+}));

+ 36 - 0
packages/advertisement-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/advertisement-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'
+    }
+  }
+});

+ 36 - 0
packages/advertisement-type-management-ui-mt/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',
+    },
+  },
+];

+ 95 - 0
packages/advertisement-type-management-ui-mt/package.json

@@ -0,0 +1,95 @@
+{
+  "name": "@d8d/advertisement-type-management-ui-mt",
+  "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/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/advertisements-module-mt": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "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": [
+    "advertisement",
+    "type",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud",
+    "multi-tenant",
+    "mt"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 44 - 0
packages/advertisement-type-management-ui-mt/src/api/advertisementTypeClient.ts

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

+ 3 - 0
packages/advertisement-type-management-ui-mt/src/api/index.ts

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

+ 586 - 0
packages/advertisement-type-management-ui-mt/src/components/AdvertisementTypeManagement.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 { advertisementTypeClientManager } from '../api/advertisementTypeClient'
+import type { AdvertisementType, AdvertisementTypeFormData, AdvertisementTypeQueryParams } from '../types/advertisementType'
+import { CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '@d8d/advertisements-module-mt/schemas'
+
+// 表单Schema直接使用后端定义
+const createFormSchema = CreateAdvertisementTypeDto
+const updateFormSchema = UpdateAdvertisementTypeDto
+
+export const AdvertisementTypeManagement = () => {
+  // 状态管理
+  const [searchParams, setSearchParams] = useState<AdvertisementTypeQueryParams>({ page: 1, limit: 10, search: '' })
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [editingType, setEditingType] = useState<AdvertisementType | 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 = advertisementTypeClientManager.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: AdvertisementTypeFormData) => {
+      const client = advertisementTypeClientManager.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: AdvertisementTypeFormData }) => {
+      const client = advertisementTypeClientManager.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 = advertisementTypeClientManager.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/advertisement-type-management-ui-mt/src/components/AdvertisementTypeSelector.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 { advertisementTypeClientManager } from '../api/advertisementTypeClient';
+
+interface AdvertisementTypeSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+  className?: string;
+  testId?: string;
+}
+
+export const AdvertisementTypeSelector: React.FC<AdvertisementTypeSelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "请选择广告类型",
+  disabled = false,
+  className,
+  testId,
+}) => {
+  const {
+    data: advertisementTypes,
+    isLoading,
+    isError,
+  } = useQuery({
+    queryKey: ['advertisement-types'],
+    queryFn: async () => {
+      const client = advertisementTypeClientManager.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 = advertisementTypes?.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 AdvertisementTypeSelector;

+ 4 - 0
packages/advertisement-type-management-ui-mt/src/components/index.ts

@@ -0,0 +1,4 @@
+// 组件导出入口
+
+export { AdvertisementTypeManagement } from './AdvertisementTypeManagement';
+export { default as AdvertisementTypeSelector } from './AdvertisementTypeSelector';

+ 19 - 0
packages/advertisement-type-management-ui-mt/src/index.ts

@@ -0,0 +1,19 @@
+// 主包导出入口
+
+export { AdvertisementTypeManagement, AdvertisementTypeSelector } from './components';
+
+export { advertisementTypeClient, advertisementTypeClientManager } from './api/advertisementTypeClient';
+
+export type {
+  AdvertisementType,
+  AdvertisementTypeFormData,
+  AdvertisementTypeQueryParams,
+  CreateAdvertisementTypeRequest,
+  CreateAdvertisementTypeResponse,
+  UpdateAdvertisementTypeRequest,
+  UpdateAdvertisementTypeResponse,
+  ListAdvertisementTypesResponse,
+  GetAdvertisementTypeResponse,
+  DeleteAdvertisementTypeResponse,
+  AdvertisementTypeStatus
+} from './types/advertisementType';

+ 58 - 0
packages/advertisement-type-management-ui-mt/src/types/advertisementType.ts

@@ -0,0 +1,58 @@
+import { InferRequestType, InferResponseType } from 'hono';
+import { advertisementTypeRoutes } from '@d8d/advertisements-module-mt';
+
+// 广告分类实体类型
+export interface AdvertisementType {
+  id: number;
+  name: string;
+  code: string;
+  remark: string | null;
+  status: number;
+  createdAt: string;
+  updatedAt: string;
+  createdBy: number | null;
+  updatedBy: number | null;
+}
+
+// 广告分类创建请求类型
+export type CreateAdvertisementTypeRequest = InferRequestType<typeof advertisementTypeRoutes>['post'];
+
+// 广告分类创建响应类型
+export type CreateAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['post'];
+
+// 广告分类更新请求类型
+export type UpdateAdvertisementTypeRequest = InferRequestType<typeof advertisementTypeRoutes>['put'];
+
+// 广告分类更新响应类型
+export type UpdateAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['put'];
+
+// 广告分类列表响应类型
+export type ListAdvertisementTypesResponse = InferResponseType<typeof advertisementTypeRoutes>['get'];
+
+// 广告分类详情响应类型
+export type GetAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['/:id']['get'];
+
+// 广告分类删除响应类型
+export type DeleteAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['/:id']['delete'];
+
+// 广告分类状态枚举
+export enum AdvertisementTypeStatus {
+  DISABLED = 0,
+  ENABLED = 1
+}
+
+// 广告分类表单数据
+export interface AdvertisementTypeFormData {
+  name: string;
+  code: string;
+  remark?: string | null;
+  status: number;
+}
+
+// 广告分类查询参数
+export interface AdvertisementTypeQueryParams {
+  page?: number;
+  limit?: number;
+  search?: string;
+  status?: number;
+}

+ 341 - 0
packages/advertisement-type-management-ui-mt/tests/integration/advertisement-type-management.integration.test.tsx

@@ -0,0 +1,341 @@
+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 { AdvertisementTypeManagement } from '../../src/components/AdvertisementTypeManagement';
+import { advertisementTypeClient } from '../../src/api/advertisementTypeClient';
+
+// Mock the advertisement type client
+vi.mock('../../src/api/advertisementTypeClient', () => {
+  const advertisementTypeClient = {
+    index: {
+      $get: vi.fn(),
+      $post: vi.fn(),
+    },
+    ':id': {
+      $put: vi.fn(),
+      $delete: vi.fn(),
+    },
+  }
+  return {
+    advertisementTypeClient,
+    advertisementTypeClientManager: {
+      get: vi.fn(() => advertisementTypeClient),
+    },
+  }
+})
+
+// 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 mockAdvertisementTypes = {
+  data: [
+    {
+      id: 1,
+      name: '首页轮播',
+      code: 'home_carousel',
+      remark: '首页轮播广告位',
+      status: 1,
+      createdAt: '2024-01-01T00:00:00Z',
+      updatedAt: '2024-01-01T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      id: 2,
+      name: '侧边广告',
+      code: 'sidebar_ad',
+      remark: '侧边栏广告位',
+      status: 0,
+      createdAt: '2024-01-02T00:00:00Z',
+      updatedAt: '2024-01-02T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+  ],
+  pagination: {
+    total: 2,
+    page: 1,
+    pageSize: 10,
+    totalPages: 1,
+  },
+};
+
+describe('AdvertisementTypeManagement 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染广告类型管理界面', async () => {
+    // Mock API响应
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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.getByText('ID')).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();
+    expect(screen.getByText('禁用')).toBeInTheDocument();
+
+    // 验证调用别名显示
+    expect(screen.getByText('home_carousel')).toBeInTheDocument();
+    expect(screen.getByText('sidebar_ad')).toBeInTheDocument();
+  });
+
+  it('应该显示加载状态', () => {
+    // Mock 延迟响应
+    (advertisementTypeClient.index.$get as any).mockImplementation(
+      () => new Promise(() => {}) // 永不解析的Promise
+    );
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </TestWrapper>
+    );
+
+    // 验证加载状态
+    expect(screen.getByTestId('page-title')).toHaveTextContent('广告类型管理');
+    expect(screen.getByTestId('create-type-button')).toBeInTheDocument();
+    expect(screen.getByTestId('create-type-button')).toBeDisabled();
+  });
+
+  it('应该处理搜索功能', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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(advertisementTypeClient.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '首页',
+        },
+      });
+    });
+  });
+
+  it('应该打开创建广告类型模态框', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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 () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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 () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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 空数据响应
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        data: [],
+        pagination: {
+          total: 0,
+          page: 1,
+          pageSize: 10,
+          totalPages: 0,
+        },
+      }),
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </TestWrapper>
+    );
+
+    // 验证空状态显示
+    await waitFor(() => {
+      expect(screen.getByText('暂无广告类型数据')).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理分页', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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/advertisement-type-management-ui-mt/tests/integration/advertisement-type-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 { AdvertisementTypeSelector } from '../../src/components/AdvertisementTypeSelector'
+import { advertisementTypeClient } from '../../src/api/advertisementTypeClient'
+
+// Mock the advertisement type client
+vi.mock('../../src/api/advertisementTypeClient', () => {
+  const advertisementTypeClient = {
+    index: {
+      $get: vi.fn(),
+    },
+  }
+  return {
+    advertisementTypeClient,
+    advertisementTypeClientManager: {
+      get: vi.fn(() => advertisementTypeClient),
+    },
+  }
+})
+
+const mockAdvertisementTypes = {
+  data: [
+    { id: 1, name: '首页轮播', code: 'home_carousel', status: 1, createdAt: '2024-01-01' },
+    { id: 2, name: '侧边广告', code: 'sidebar_ad', status: 1, createdAt: '2024-01-01' },
+    { id: 3, name: '弹窗广告', code: 'popup_ad', 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('AdvertisementTypeSelector 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
+
+  it('应该正确渲染广告类型选择器', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('advertisement-type-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('请选择广告类型')).toBeInTheDocument()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('advertisement-type-selector')
+    fireEvent.click(selectTrigger)
+
+    // 验证下拉菜单中的选项
+    await waitFor(() => {
+      expect(screen.getByText('首页轮播')).toBeInTheDocument()
+      expect(screen.getByText('侧边广告')).toBeInTheDocument()
+      expect(screen.getByText('弹窗广告')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理加载状态', () => {
+    // Mock 延迟响应
+    (advertisementTypeClient.index.$get as any).mockImplementation(
+      () => new Promise(() => { }) // 永不解析的Promise
+    )
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('advertisement-type-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+  })
+
+  it('应该处理错误状态', async () => {
+    (advertisementTypeClient.index.$get as any).mockRejectedValue(new Error('API错误'))
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待错误状态
+    await waitFor(() => {
+      expect(screen.getByText('加载广告类型失败')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理选择器值变化', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    const mockOnChange = vi.fn()
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector onChange={mockOnChange} testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('advertisement-type-selector')).toBeEnabled()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('advertisement-type-selector')
+    fireEvent.click(selectTrigger)
+
+    // 选择第一个选项
+    await waitFor(() => {
+      const firstOption = screen.getByText('首页轮播')
+      fireEvent.click(firstOption)
+    })
+
+    // 验证onChange被调用
+    expect(mockOnChange).toHaveBeenCalledWith(1)
+  })
+
+  it('应该支持自定义占位符', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector placeholder="选择广告分类" testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('选择广告分类')).toBeInTheDocument()
+    })
+  })
+
+  it('应该支持禁用状态', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector disabled={true} testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      const selectTrigger = screen.getByTestId('advertisement-type-selector')
+      expect(selectTrigger).toBeDisabled()
+    })
+  })
+
+  it('应该支持预选值', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector value={2} testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('advertisement-type-selector')).toBeEnabled()
+    })
+
+    // 验证预选值已正确设置
+    // 在Radix UI Select中,预选值会显示在选择器触发器中
+    const selectTrigger = screen.getByTestId('advertisement-type-selector')
+    expect(selectTrigger).toHaveTextContent('侧边广告')
+  })
+})

+ 43 - 0
packages/advertisement-type-management-ui-mt/tests/setup.ts

@@ -0,0 +1,43 @@
+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();
+};

+ 36 - 0
packages/advertisement-type-management-ui-mt/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/advertisement-type-management-ui-mt/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'
+    }
+  }
+});

+ 36 - 0
packages/advertisement-type-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/advertisement-type-management-ui/package.json

@@ -0,0 +1,93 @@
+{
+  "name": "@d8d/advertisement-type-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/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/advertisements-module": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "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": [
+    "advertisement",
+    "type",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 44 - 0
packages/advertisement-type-management-ui/src/api/advertisementTypeClient.ts

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

+ 3 - 0
packages/advertisement-type-management-ui/src/api/index.ts

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

+ 586 - 0
packages/advertisement-type-management-ui/src/components/AdvertisementTypeManagement.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 { advertisementTypeClientManager } from '../api/advertisementTypeClient'
+import type { AdvertisementType, AdvertisementTypeFormData, AdvertisementTypeQueryParams } from '../types/advertisementType'
+import { CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '@d8d/advertisements-module/schemas'
+
+// 表单Schema直接使用后端定义
+const createFormSchema = CreateAdvertisementTypeDto
+const updateFormSchema = UpdateAdvertisementTypeDto
+
+export const AdvertisementTypeManagement = () => {
+  // 状态管理
+  const [searchParams, setSearchParams] = useState<AdvertisementTypeQueryParams>({ page: 1, limit: 10, search: '' })
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [editingType, setEditingType] = useState<AdvertisementType | 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 = advertisementTypeClientManager.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: AdvertisementTypeFormData) => {
+      const client = advertisementTypeClientManager.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: AdvertisementTypeFormData }) => {
+      const client = advertisementTypeClientManager.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 = advertisementTypeClientManager.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/advertisement-type-management-ui/src/components/AdvertisementTypeSelector.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 { advertisementTypeClientManager } from '../api/advertisementTypeClient';
+
+interface AdvertisementTypeSelectorProps {
+  value?: number;
+  onChange?: (value: number) => void;
+  placeholder?: string;
+  disabled?: boolean;
+  className?: string;
+  testId?: string;
+}
+
+export const AdvertisementTypeSelector: React.FC<AdvertisementTypeSelectorProps> = ({
+  value,
+  onChange,
+  placeholder = "请选择广告类型",
+  disabled = false,
+  className,
+  testId,
+}) => {
+  const {
+    data: advertisementTypes,
+    isLoading,
+    isError,
+  } = useQuery({
+    queryKey: ['advertisement-types'],
+    queryFn: async () => {
+      const client = advertisementTypeClientManager.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 = advertisementTypes?.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 AdvertisementTypeSelector;

+ 4 - 0
packages/advertisement-type-management-ui/src/components/index.ts

@@ -0,0 +1,4 @@
+// 组件导出入口
+
+export { AdvertisementTypeManagement } from './AdvertisementTypeManagement';
+export { default as AdvertisementTypeSelector } from './AdvertisementTypeSelector';

+ 19 - 0
packages/advertisement-type-management-ui/src/index.ts

@@ -0,0 +1,19 @@
+// 主包导出入口
+
+export { AdvertisementTypeManagement, AdvertisementTypeSelector } from './components';
+
+export { advertisementTypeClient, advertisementTypeClientManager } from './api/advertisementTypeClient';
+
+export type {
+  AdvertisementType,
+  AdvertisementTypeFormData,
+  AdvertisementTypeQueryParams,
+  CreateAdvertisementTypeRequest,
+  CreateAdvertisementTypeResponse,
+  UpdateAdvertisementTypeRequest,
+  UpdateAdvertisementTypeResponse,
+  ListAdvertisementTypesResponse,
+  GetAdvertisementTypeResponse,
+  DeleteAdvertisementTypeResponse,
+  AdvertisementTypeStatus
+} from './types/advertisementType';

+ 58 - 0
packages/advertisement-type-management-ui/src/types/advertisementType.ts

@@ -0,0 +1,58 @@
+import { InferRequestType, InferResponseType } from 'hono';
+import { advertisementTypeRoutes } from '@d8d/advertisements-module';
+
+// 广告分类实体类型
+export interface AdvertisementType {
+  id: number;
+  name: string;
+  code: string;
+  remark: string | null;
+  status: number;
+  createdAt: string;
+  updatedAt: string;
+  createdBy: number | null;
+  updatedBy: number | null;
+}
+
+// 广告分类创建请求类型
+export type CreateAdvertisementTypeRequest = InferRequestType<typeof advertisementTypeRoutes>['post'];
+
+// 广告分类创建响应类型
+export type CreateAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['post'];
+
+// 广告分类更新请求类型
+export type UpdateAdvertisementTypeRequest = InferRequestType<typeof advertisementTypeRoutes>['put'];
+
+// 广告分类更新响应类型
+export type UpdateAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['put'];
+
+// 广告分类列表响应类型
+export type ListAdvertisementTypesResponse = InferResponseType<typeof advertisementTypeRoutes>['get'];
+
+// 广告分类详情响应类型
+export type GetAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['/:id']['get'];
+
+// 广告分类删除响应类型
+export type DeleteAdvertisementTypeResponse = InferResponseType<typeof advertisementTypeRoutes>['/:id']['delete'];
+
+// 广告分类状态枚举
+export enum AdvertisementTypeStatus {
+  DISABLED = 0,
+  ENABLED = 1
+}
+
+// 广告分类表单数据
+export interface AdvertisementTypeFormData {
+  name: string;
+  code: string;
+  remark?: string | null;
+  status: number;
+}
+
+// 广告分类查询参数
+export interface AdvertisementTypeQueryParams {
+  page?: number;
+  limit?: number;
+  search?: string;
+  status?: number;
+}

+ 341 - 0
packages/advertisement-type-management-ui/tests/integration/advertisement-type-management.integration.test.tsx

@@ -0,0 +1,341 @@
+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 { AdvertisementTypeManagement } from '../../src/components/AdvertisementTypeManagement';
+import { advertisementTypeClient } from '../../src/api/advertisementTypeClient';
+
+// Mock the advertisement type client
+vi.mock('../../src/api/advertisementTypeClient', () => {
+  const advertisementTypeClient = {
+    index: {
+      $get: vi.fn(),
+      $post: vi.fn(),
+    },
+    ':id': {
+      $put: vi.fn(),
+      $delete: vi.fn(),
+    },
+  }
+  return {
+    advertisementTypeClient,
+    advertisementTypeClientManager: {
+      get: vi.fn(() => advertisementTypeClient),
+    },
+  }
+})
+
+// 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 mockAdvertisementTypes = {
+  data: [
+    {
+      id: 1,
+      name: '首页轮播',
+      code: 'home_carousel',
+      remark: '首页轮播广告位',
+      status: 1,
+      createdAt: '2024-01-01T00:00:00Z',
+      updatedAt: '2024-01-01T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      id: 2,
+      name: '侧边广告',
+      code: 'sidebar_ad',
+      remark: '侧边栏广告位',
+      status: 0,
+      createdAt: '2024-01-02T00:00:00Z',
+      updatedAt: '2024-01-02T00:00:00Z',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+  ],
+  pagination: {
+    total: 2,
+    page: 1,
+    pageSize: 10,
+    totalPages: 1,
+  },
+};
+
+describe('AdvertisementTypeManagement 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染广告类型管理界面', async () => {
+    // Mock API响应
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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.getByText('ID')).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();
+    expect(screen.getByText('禁用')).toBeInTheDocument();
+
+    // 验证调用别名显示
+    expect(screen.getByText('home_carousel')).toBeInTheDocument();
+    expect(screen.getByText('sidebar_ad')).toBeInTheDocument();
+  });
+
+  it('应该显示加载状态', () => {
+    // Mock 延迟响应
+    (advertisementTypeClient.index.$get as any).mockImplementation(
+      () => new Promise(() => {}) // 永不解析的Promise
+    );
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </TestWrapper>
+    );
+
+    // 验证加载状态
+    expect(screen.getByTestId('page-title')).toHaveTextContent('广告类型管理');
+    expect(screen.getByTestId('create-type-button')).toBeInTheDocument();
+    expect(screen.getByTestId('create-type-button')).toBeDisabled();
+  });
+
+  it('应该处理搜索功能', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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(advertisementTypeClient.index.$get).toHaveBeenCalledWith({
+        query: {
+          page: 1,
+          pageSize: 10,
+          keyword: '首页',
+        },
+      });
+    });
+  });
+
+  it('应该打开创建广告类型模态框', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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 () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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 () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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 空数据响应
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => ({
+        data: [],
+        pagination: {
+          total: 0,
+          page: 1,
+          pageSize: 10,
+          totalPages: 0,
+        },
+      }),
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </TestWrapper>
+    );
+
+    // 验证空状态显示
+    await waitFor(() => {
+      expect(screen.getByText('暂无广告类型数据')).toBeInTheDocument();
+    });
+  });
+
+  it('应该处理分页', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    });
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeManagement />
+      </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/advertisement-type-management-ui/tests/integration/advertisement-type-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 { AdvertisementTypeSelector } from '../../src/components/AdvertisementTypeSelector'
+import { advertisementTypeClient } from '../../src/api/advertisementTypeClient'
+
+// Mock the advertisement type client
+vi.mock('../../src/api/advertisementTypeClient', () => {
+  const advertisementTypeClient = {
+    index: {
+      $get: vi.fn(),
+    },
+  }
+  return {
+    advertisementTypeClient,
+    advertisementTypeClientManager: {
+      get: vi.fn(() => advertisementTypeClient),
+    },
+  }
+})
+
+const mockAdvertisementTypes = {
+  data: [
+    { id: 1, name: '首页轮播', code: 'home_carousel', status: 1, createdAt: '2024-01-01' },
+    { id: 2, name: '侧边广告', code: 'sidebar_ad', status: 1, createdAt: '2024-01-01' },
+    { id: 3, name: '弹窗广告', code: 'popup_ad', 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('AdvertisementTypeSelector 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks()
+  })
+
+  it('应该正确渲染广告类型选择器', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('advertisement-type-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('请选择广告类型')).toBeInTheDocument()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('advertisement-type-selector')
+    fireEvent.click(selectTrigger)
+
+    // 验证下拉菜单中的选项
+    await waitFor(() => {
+      expect(screen.getByText('首页轮播')).toBeInTheDocument()
+      expect(screen.getByText('侧边广告')).toBeInTheDocument()
+      expect(screen.getByText('弹窗广告')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理加载状态', () => {
+    // Mock 延迟响应
+    (advertisementTypeClient.index.$get as any).mockImplementation(
+      () => new Promise(() => {}) // 永不解析的Promise
+    )
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 验证加载状态
+    expect(screen.getByTestId('advertisement-type-selector')).toBeInTheDocument()
+    expect(screen.getByText('加载中...')).toBeInTheDocument()
+  })
+
+  it('应该处理错误状态', async () => {
+    (advertisementTypeClient.index.$get as any).mockRejectedValue(new Error('API错误'))
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待错误状态
+    await waitFor(() => {
+      expect(screen.getByText('加载广告类型失败')).toBeInTheDocument()
+    })
+  })
+
+  it('应该处理选择器值变化', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    const mockOnChange = vi.fn()
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector onChange={mockOnChange} testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('advertisement-type-selector')).toBeEnabled()
+    })
+
+    // 点击选择器打开下拉菜单
+    const selectTrigger = screen.getByTestId('advertisement-type-selector')
+    fireEvent.click(selectTrigger)
+
+    // 选择第一个选项
+    await waitFor(() => {
+      const firstOption = screen.getByText('首页轮播')
+      fireEvent.click(firstOption)
+    })
+
+    // 验证onChange被调用
+    expect(mockOnChange).toHaveBeenCalledWith(1)
+  })
+
+  it('应该支持自定义占位符', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector placeholder="选择广告分类" testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByText('选择广告分类')).toBeInTheDocument()
+    })
+  })
+
+  it('应该支持禁用状态', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector disabled={true} testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      const selectTrigger = screen.getByTestId('advertisement-type-selector')
+      expect(selectTrigger).toBeDisabled()
+    })
+  })
+
+  it('应该支持预选值', async () => {
+    (advertisementTypeClient.index.$get as any).mockResolvedValue({
+      status: 200,
+      json: async () => mockAdvertisementTypes,
+    })
+
+    render(
+      <TestWrapper>
+        <AdvertisementTypeSelector value={2} testId="advertisement-type-selector" />
+      </TestWrapper>
+    )
+
+    // 等待数据加载完成
+    await waitFor(() => {
+      expect(screen.getByTestId('advertisement-type-selector')).toBeEnabled()
+    })
+
+    // 验证预选值已正确设置
+    // 在Radix UI Select中,预选值会显示在选择器触发器中
+    const selectTrigger = screen.getByTestId('advertisement-type-selector')
+    expect(selectTrigger).toHaveTextContent('侧边广告')
+  })
+})

+ 43 - 0
packages/advertisement-type-management-ui/tests/setup.ts

@@ -0,0 +1,43 @@
+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();
+};

+ 36 - 0
packages/advertisement-type-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/advertisement-type-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'
+    }
+  }
+});

+ 82 - 0
packages/advertisements-module-mt/package.json

@@ -0,0 +1,82 @@
+{
+  "name": "@d8d/advertisements-module-mt",
+  "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/file-module-mt": "workspace:*",
+    "@d8d/auth-module-mt": "workspace:*",
+    "@d8d/user-module-mt": "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": [
+    "advertisements",
+    "ads",
+    "banners",
+    "crud",
+    "api",
+    "multi-tenant",
+    "tenant-isolation"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 81 - 0
packages/advertisements-module-mt/src/entities/advertisement-type.entity.ts

@@ -0,0 +1,81 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('ad_type_mt')
+export class AdvertisementType {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({
+    name: 'tenant_id',
+    type: 'int',
+    unsigned: true,
+    nullable: false,
+    comment: '租户ID'
+  })
+  tenantId!: number;
+
+  @Column({
+    name: 'name',
+    type: 'varchar',
+    length: 50,
+    comment: '类型名称'
+  })
+  name!: string;
+
+  @Column({
+    name: 'code',
+    type: 'varchar',
+    length: 20,
+    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;
+}

+ 134 - 0
packages/advertisements-module-mt/src/entities/advertisement.entity.ts

@@ -0,0 +1,134 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { FileMt } from '@d8d/file-module-mt';
+import { AdvertisementType } from './advertisement-type.entity';
+
+@Entity('ad_mt')
+export class Advertisement {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({
+    name: 'tenant_id',
+    type: 'int',
+    unsigned: true,
+    nullable: false,
+    comment: '租户ID'
+  })
+  tenantId!: number;
+
+  @Column({
+    name: 'title',
+    type: 'varchar',
+    length: 30,
+    nullable: true,
+    comment: '标题'
+  })
+  title!: string | null;
+
+  @Column({
+    name: 'type_id',
+    type: 'int',
+    nullable: true,
+    unsigned: true,
+    comment: '广告类型'
+  })
+  typeId!: number | null;
+
+  @Column({
+    name: 'code',
+    type: 'varchar',
+    length: 20,
+    nullable: true,
+    comment: '调用别名'
+  })
+  code!: string | null;
+
+  @Column({
+    name: 'url',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+    comment: 'url'
+  })
+  url!: string | null;
+
+  @Column({
+    name: 'image_file_id',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '图片文件ID'
+  })
+  imageFileId!: number | null;
+
+  @ManyToOne('FileMt', { nullable: true })
+  @JoinColumn({
+    name: 'image_file_id',
+    referencedColumnName: 'id'
+  })
+  imageFile!: any | null;
+
+  @ManyToOne('AdvertisementType', { nullable: true })
+  @JoinColumn({
+    name: 'type_id',
+    referencedColumnName: 'id'
+  })
+  advertisementType!: any | null;
+
+  @Column({
+    name: 'sort',
+    type: 'int',
+    default: 0,
+    comment: '排序'
+  })
+  sort!: number;
+
+  @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',
+    unsigned: true,
+    default: 0,
+    comment: '状态'
+  })
+  status!: number;
+
+  @Column({
+    name: 'action_type',
+    type: 'int',
+    default: 1,
+    comment: '跳转类型 0 不跳转 1webview 2小程序页面'
+  })
+  actionType!: number;
+}

+ 2 - 0
packages/advertisements-module-mt/src/entities/index.ts

@@ -0,0 +1,2 @@
+export { Advertisement } from './advertisement.entity';
+export { AdvertisementType } from './advertisement-type.entity';

+ 6 - 0
packages/advertisements-module-mt/src/index.ts

@@ -0,0 +1,6 @@
+// 多租户广告模块主导出文件
+
+export * from './entities';
+export * from './services';
+export * from './schemas';
+export * from './routes';

+ 22 - 0
packages/advertisements-module-mt/src/routes/advertisement-types.ts

@@ -0,0 +1,22 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module-mt';
+import { AdvertisementType } from '../entities/advertisement-type.entity';
+import { AdvertisementTypeSchema, CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '../schemas/advertisement-type.schema';
+
+export const advertisementTypeRoutes = createCrudRoutes({
+  entity: AdvertisementType,
+  createSchema: CreateAdvertisementTypeDto,
+  updateSchema: UpdateAdvertisementTypeDto,
+  getSchema: AdvertisementTypeSchema,
+  listSchema: AdvertisementTypeSchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  tenantOptions: {
+    enabled: true,
+    tenantIdField: 'tenantId'
+  }
+});

+ 23 - 0
packages/advertisements-module-mt/src/routes/advertisements.ts

@@ -0,0 +1,23 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module-mt';
+import { Advertisement } from '../entities/advertisement.entity';
+import { AdvertisementSchema, CreateAdvertisementDto, UpdateAdvertisementDto } from '../schemas/advertisement.schema';
+
+export const advertisementRoutes = createCrudRoutes({
+  entity: Advertisement,
+  createSchema: CreateAdvertisementDto,
+  updateSchema: UpdateAdvertisementDto,
+  getSchema: AdvertisementSchema,
+  listSchema: AdvertisementSchema,
+  searchFields: ['title', 'code'],
+  relations: ['imageFile', 'advertisementType'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  },
+  tenantOptions: {
+    enabled: true,
+    tenantIdField: 'tenantId'
+  }
+});

+ 2 - 0
packages/advertisements-module-mt/src/routes/index.ts

@@ -0,0 +1,2 @@
+export { advertisementRoutes } from './advertisements';
+export { advertisementTypeRoutes } from './advertisement-types';

+ 85 - 0
packages/advertisements-module-mt/src/schemas/advertisement-type.schema.ts

@@ -0,0 +1,85 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告类型实体Schema
+export const AdvertisementTypeSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告类型ID',
+    example: 1
+  }),
+  tenantId: 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: 'home_banner'
+  }),
+  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 CreateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  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 UpdateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).optional().openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  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
+  })
+});

+ 149 - 0
packages/advertisements-module-mt/src/schemas/advertisement.schema.ts

@@ -0,0 +1,149 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告实体Schema
+export const AdvertisementSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告ID',
+    example: 1
+  }),
+  tenantId: z.number().int().positive().openapi({
+    description: '租户ID',
+    example: 1
+  }),
+  title: z.string().max(30).nullable().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.number().int().positive().nullable().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().max(20).nullable().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'banner.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/banner.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '图片文件信息'
+  }),
+  advertisementType: z.object({
+    id: z.number().int().positive().openapi({ description: '广告类型ID' }),
+    name: z.string().max(50).openapi({ description: '类型名称', example: '首页轮播' }),
+    code: z.string().max(20).openapi({ description: '类型编码', example: 'home_banner' })
+  }).nullable().optional().openapi({
+    description: '广告类型信息'
+  }),
+  sort: z.number().int().default(0).openapi({
+    description: '排序值',
+    example: 10
+  }),
+  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
+  }),
+  actionType: z.number().int().min(0).max(2).default(1).openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 创建广告DTO
+export const CreateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().default(0).optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).default(1).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 更新广告DTO
+export const UpdateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).optional().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});

+ 2 - 0
packages/advertisements-module-mt/src/schemas/index.ts

@@ -0,0 +1,2 @@
+export * from './advertisement.schema';
+export * from './advertisement-type.schema';

+ 9 - 0
packages/advertisements-module-mt/src/services/advertisement-type.service.ts

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

+ 9 - 0
packages/advertisements-module-mt/src/services/advertisement.service.ts

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

+ 2 - 0
packages/advertisements-module-mt/src/services/index.ts

@@ -0,0 +1,2 @@
+export { AdvertisementService } from './advertisement.service';
+export { AdvertisementTypeService } from './advertisement-type.service';

+ 378 - 0
packages/advertisements-module-mt/tests/integration/advertisement-types.integration.test.ts

@@ -0,0 +1,378 @@
+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 { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { advertisementTypeRoutes } from '../../src/routes';
+import { AdvertisementType } from '../../src/entities/advertisement-type.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntityMt, FileMt, RoleMt, AdvertisementType])
+
+describe('多租户广告类型管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof advertisementTypeRoutes>>;
+  let testToken: string;
+  let testUser: UserEntityMt;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(advertisementTypeRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntityMt);
+    testUser = userRepository.create({
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(testUser);
+
+    // 生成测试用户的token
+    testToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{ name: 'user' }]
+    });
+  });
+
+  describe('GET /advertisement-types', () => {
+    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 /advertisement-types', () => {
+    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 advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const existingType = advertisementTypeRepository.create({
+        name: '现有类型',
+        code: 'existing_code',
+        status: 1,
+        tenantId: 1
+      });
+      await advertisementTypeRepository.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(201);
+    });
+  });
+
+  describe('GET /advertisement-types/:id', () => {
+    it('应该返回指定广告类型的详情', async () => {
+      // 先创建一个广告类型
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const testType = advertisementTypeRepository.create({
+        name: '测试类型详情',
+        code: 'test_type_detail',
+        remark: '测试备注',
+        status: 1,
+        tenantId: 1
+      });
+      await advertisementTypeRepository.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 /advertisement-types/:id', () => {
+    it('应该成功更新广告类型', async () => {
+      // 先创建一个广告类型
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const testType = advertisementTypeRepository.create({
+        name: '原始类型',
+        code: 'original_type',
+        remark: '原始备注',
+        status: 1,
+        tenantId: 1
+      });
+      await advertisementTypeRepository.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 /advertisement-types/:id', () => {
+    it('应该成功删除广告类型', async () => {
+      // 先创建一个广告类型
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const testType = advertisementTypeRepository.create({
+        name: '待删除类型',
+        code: 'delete_type',
+        remark: '待删除备注',
+        status: 1,
+        tenantId: 1
+      });
+      await advertisementTypeRepository.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 advertisementTypeRepository.findOne({
+        where: { id: testType.id }
+      });
+      expect(deletedType).toBeNull();
+    });
+  });
+
+  describe('租户数据隔离测试', () => {
+    it('应该确保租户只能访问自己的广告类型', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+
+      // 创建租户1的广告类型
+      const tenant1Type = advertisementTypeRepository.create({
+        name: '租户1类型',
+        code: 'tenant1_type',
+        remark: '租户1的广告类型',
+        status: 1,
+        tenantId: 1
+      });
+      await advertisementTypeRepository.save(tenant1Type);
+
+      // 创建租户2的广告类型
+      const tenant2Type = advertisementTypeRepository.create({
+        name: '租户2类型',
+        code: 'tenant2_type',
+        remark: '租户2的广告类型',
+        status: 1,
+        tenantId: 2
+      });
+      await advertisementTypeRepository.save(tenant2Type);
+
+      // 测试租户1只能看到自己的广告类型
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      // 验证返回的数据只包含租户1的广告类型
+      if ('data' in data) {
+        const tenant1Types = data.data.filter((type: any) => type.tenantId === 1);
+        const tenant2Types = data.data.filter((type: any) => type.tenantId === 2);
+
+        expect(tenant1Types.length).toBeGreaterThan(0);
+        expect(tenant2Types.length).toBe(0); // 租户1不应该看到租户2的广告类型
+      }
+    });
+
+    it('应该防止跨租户广告类型访问', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+
+      // 创建租户2的广告类型
+      const tenant2Type = advertisementTypeRepository.create({
+        name: '租户2私有类型',
+        code: 'tenant2_private',
+        remark: '租户2的私有类型',
+        status: 1,
+        tenantId: 2
+      });
+      await advertisementTypeRepository.save(tenant2Type);
+
+      // 租户1尝试访问租户2的广告类型
+      const response = await client[':id'].$get({
+        param: { id: tenant2Type.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 应该返回404,因为租户1不能访问租户2的数据
+      expect(response.status).toBe(404);
+    });
+
+    it('应该允许不同租户使用相同的广告类型编码', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+
+      // 创建租户1的广告类型
+      const tenant1Type = advertisementTypeRepository.create({
+        name: '租户1类型',
+        code: 'shared_code', // 相同的编码
+        remark: '租户1的广告类型',
+        status: 1,
+        tenantId: 1
+      });
+      await advertisementTypeRepository.save(tenant1Type);
+
+      // 创建租户2的广告类型,使用相同的编码
+      const tenant2Type = advertisementTypeRepository.create({
+        name: '租户2类型',
+        code: 'shared_code', // 相同的编码
+        remark: '租户2的广告类型',
+        status: 1,
+        tenantId: 2
+      });
+      await advertisementTypeRepository.save(tenant2Type);
+
+      // 两个租户都应该能成功创建相同编码的类型
+      expect(tenant1Type.id).toBeDefined();
+      expect(tenant2Type.id).toBeDefined();
+    });
+  });
+});

+ 356 - 0
packages/advertisements-module-mt/tests/integration/advertisements.integration.test.ts

@@ -0,0 +1,356 @@
+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 { UserEntityMt, RoleMt } from '@d8d/user-module-mt';
+import { FileMt } from '@d8d/file-module-mt';
+import { advertisementRoutes } from '../../src/routes';
+import { Advertisement } from '../../src/entities/advertisement.entity';
+import { AdvertisementType } from '../../src/entities/advertisement-type.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntityMt, FileMt, RoleMt, Advertisement, AdvertisementType])
+
+describe('多租户广告管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof advertisementRoutes>>;
+  let testToken: string;
+  let testUser: UserEntityMt;
+  let testAdvertisementType: AdvertisementType;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(advertisementRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+
+    // 创建测试用户
+    const userRepository = dataSource.getRepository(UserEntityMt);
+    testUser = userRepository.create({
+      username: `test_user_${Date.now()}`,
+      password: 'test_password',
+      nickname: '测试用户',
+      registrationSource: 'web',
+      tenantId: 1
+    });
+    await userRepository.save(testUser);
+
+    // 创建测试广告类型
+    const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+    testAdvertisementType = advertisementTypeRepository.create({
+      name: '首页轮播',
+      code: 'home_banner',
+      remark: '用于首页轮播图展示',
+      status: 1,
+      tenantId: 1
+    });
+    await advertisementTypeRepository.save(testAdvertisementType);
+
+    // 生成测试用户的token
+    testToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{name:'user'}]
+    });
+  });
+
+  describe('GET /advertisements', () => {
+    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 /advertisements', () => {
+    it('应该成功创建广告', async () => {
+      const createData = {
+        title: '测试广告',
+        typeId: testAdvertisementType.id,
+        code: 'test_ad',
+        url: 'https://example.com',
+        sort: 10,
+        status: 1,
+        actionType: 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.title).toBe(createData.title);
+        expect(data.code).toBe(createData.code);
+        expect(data.status).toBe(createData.status);
+      }
+    });
+
+    it('应该验证创建广告的必填字段', async () => {
+      const invalidData = {
+        // 缺少必填字段
+        title: '',
+        typeId: 0,
+        code: '',
+        url: 'https://example.com'
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /advertisements/:id', () => {
+    it('应该返回指定广告的详情', async () => {
+      // 先创建一个广告
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+      const testAdvertisement = advertisementRepository.create({
+        title: '测试广告详情',
+        typeId: testAdvertisementType.id,
+        code: 'test_ad_detail',
+        url: 'https://example.com',
+        sort: 5,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id,
+        tenantId: 1
+      });
+      await advertisementRepository.save(testAdvertisement);
+
+      const response = await client[':id'].$get({
+        param: { id: testAdvertisement.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(testAdvertisement.id);
+        expect(data.title).toBe(testAdvertisement.title);
+        expect(data.code).toBe(testAdvertisement.code);
+      }
+    });
+
+    it('应该处理不存在的广告', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /advertisements/:id', () => {
+    it('应该成功更新广告', async () => {
+      // 先创建一个广告
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+      const testAdvertisement = advertisementRepository.create({
+        title: '原始广告',
+        typeId: testAdvertisementType.id,
+        code: 'original_ad',
+        url: 'https://example.com',
+        sort: 5,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id,
+        tenantId: 1
+      });
+      await advertisementRepository.save(testAdvertisement);
+
+      const updateData = {
+        title: '更新后的广告',
+        code: 'updated_ad',
+        sort: 15
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testAdvertisement.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.title).toBe(updateData.title);
+        expect(data.code).toBe(updateData.code);
+        expect(data.sort).toBe(updateData.sort);
+      }
+    });
+  });
+
+  describe('DELETE /advertisements/:id', () => {
+    it('应该成功删除广告', async () => {
+      // 先创建一个广告
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+      const testAdvertisement = advertisementRepository.create({
+        title: '待删除广告',
+        typeId: testAdvertisementType.id,
+        code: 'delete_ad',
+        url: 'https://example.com',
+        sort: 5,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id,
+        tenantId: 1
+      });
+      await advertisementRepository.save(testAdvertisement);
+
+      const response = await client[':id'].$delete({
+        param: { id: testAdvertisement.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('删除广告响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证广告确实被删除
+      const deletedAdvertisement = await advertisementRepository.findOne({
+        where: { id: testAdvertisement.id }
+      });
+      expect(deletedAdvertisement).toBeNull();
+    });
+  });
+
+  describe('租户数据隔离测试', () => {
+    it('应该确保租户只能访问自己的数据', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+
+      // 创建租户1的广告
+      const tenant1Advertisement = advertisementRepository.create({
+        title: '租户1广告',
+        typeId: testAdvertisementType.id,
+        code: 'tenant1_ad',
+        url: 'https://example.com',
+        sort: 1,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id,
+        tenantId: 1
+      });
+      await advertisementRepository.save(tenant1Advertisement);
+
+      // 创建租户2的广告
+      const tenant2Advertisement = advertisementRepository.create({
+        title: '租户2广告',
+        typeId: testAdvertisementType.id,
+        code: 'tenant2_ad',
+        url: 'https://example.com',
+        sort: 1,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id,
+        tenantId: 2
+      });
+      await advertisementRepository.save(tenant2Advertisement);
+
+      // 测试租户1只能看到自己的广告
+      const response = await client.index.$get({
+        query: {}
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(200);
+      const data = await response.json();
+
+      // 验证返回的数据只包含租户1的广告
+      if ('data' in data) {
+        const tenant1Ads = data.data.filter((ad: any) => ad.tenantId === 1);
+        const tenant2Ads = data.data.filter((ad: any) => ad.tenantId === 2);
+
+        expect(tenant1Ads.length).toBeGreaterThan(0);
+        expect(tenant2Ads.length).toBe(0); // 租户1不应该看到租户2的广告
+      }
+    });
+
+    it('应该防止跨租户数据访问', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+
+      // 创建租户2的广告
+      const tenant2Advertisement = advertisementRepository.create({
+        title: '租户2私有广告',
+        typeId: testAdvertisementType.id,
+        code: 'tenant2_private',
+        url: 'https://example.com',
+        sort: 1,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id,
+        tenantId: 2
+      });
+      await advertisementRepository.save(tenant2Advertisement);
+
+      // 租户1尝试访问租户2的广告
+      const response = await client[':id'].$get({
+        param: { id: tenant2Advertisement.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      // 应该返回404,因为租户1不能访问租户2的数据
+      expect(response.status).toBe(404);
+    });
+  });
+});

+ 16 - 0
packages/advertisements-module-mt/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/advertisements-module-mt/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
+  }
+});

+ 80 - 0
packages/advertisements-module/package.json

@@ -0,0 +1,80 @@
+{
+  "name": "@d8d/advertisements-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/file-module": "workspace:*",
+    "@d8d/auth-module": "workspace:*",
+    "@d8d/user-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": [
+    "advertisements",
+    "ads",
+    "banners",
+    "crud",
+    "api"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 73 - 0
packages/advertisements-module/src/entities/advertisement-type.entity.ts

@@ -0,0 +1,73 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+
+@Entity('ad_type')
+export class AdvertisementType {
+  @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;
+}

+ 125 - 0
packages/advertisements-module/src/entities/advertisement.entity.ts

@@ -0,0 +1,125 @@
+import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { File } from '@d8d/file-module';
+import { AdvertisementType } from './advertisement-type.entity';
+
+@Entity('ad')
+export class Advertisement {
+  @PrimaryGeneratedColumn({ unsigned: true })
+  id!: number;
+
+  @Column({
+    name: 'title',
+    type: 'varchar',
+    length: 30,
+    nullable: true,
+    comment: '标题'
+  })
+  title!: string | null;
+
+  @Column({
+    name: 'type_id',
+    type: 'int',
+    nullable: true,
+    unsigned: true,
+    comment: '广告类型'
+  })
+  typeId!: number | null;
+
+  @Column({
+    name: 'code',
+    type: 'varchar',
+    length: 20,
+    nullable: true,
+    comment: '调用别名'
+  })
+  code!: string | null;
+
+  @Column({
+    name: 'url',
+    type: 'varchar',
+    length: 255,
+    nullable: true,
+    comment: 'url'
+  })
+  url!: string | null;
+
+  @Column({
+    name: 'image_file_id',
+    type: 'int',
+    unsigned: true,
+    nullable: true,
+    comment: '图片文件ID'
+  })
+  imageFileId!: number | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({
+    name: 'image_file_id',
+    referencedColumnName: 'id'
+  })
+  imageFile!: File | null;
+
+  @ManyToOne(() => AdvertisementType, { nullable: true })
+  @JoinColumn({
+    name: 'type_id',
+    referencedColumnName: 'id'
+  })
+  advertisementType!: AdvertisementType | null;
+
+  @Column({
+    name: 'sort',
+    type: 'int',
+    default: 0,
+    comment: '排序'
+  })
+  sort!: number;
+
+  @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',
+    unsigned: true,
+    default: 0,
+    comment: '状态'
+  })
+  status!: number;
+
+  @Column({
+    name: 'action_type',
+    type: 'int',
+    default: 1,
+    comment: '跳转类型 0 不跳转 1webview 2小程序页面'
+  })
+  actionType!: number;
+}

+ 10 - 0
packages/advertisements-module/src/index.ts

@@ -0,0 +1,10 @@
+// 广告模块主导出文件
+
+export * from './entities/advertisement.entity';
+export * from './entities/advertisement-type.entity';
+export * from './services/advertisement.service';
+export * from './services/advertisement-type.service';
+export * from './schemas/advertisement.schema';
+export * from './schemas/advertisement-type.schema';
+export { advertisementRoutes } from './routes/advertisements';
+export { advertisementTypeRoutes } from './routes/advertisement-types';

+ 18 - 0
packages/advertisements-module/src/routes/advertisement-types.ts

@@ -0,0 +1,18 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { AdvertisementType } from '../entities/advertisement-type.entity';
+import { AdvertisementTypeSchema, CreateAdvertisementTypeDto, UpdateAdvertisementTypeDto } from '../schemas/advertisement-type.schema';
+
+export const advertisementTypeRoutes = createCrudRoutes({
+  entity: AdvertisementType,
+  createSchema: CreateAdvertisementTypeDto,
+  updateSchema: UpdateAdvertisementTypeDto,
+  getSchema: AdvertisementTypeSchema,
+  listSchema: AdvertisementTypeSchema,
+  searchFields: ['name', 'code'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});

+ 19 - 0
packages/advertisements-module/src/routes/advertisements.ts

@@ -0,0 +1,19 @@
+import { createCrudRoutes } from '@d8d/shared-crud';
+import { authMiddleware } from '@d8d/auth-module';
+import { Advertisement } from '../entities/advertisement.entity';
+import { AdvertisementSchema, CreateAdvertisementDto, UpdateAdvertisementDto } from '../schemas/advertisement.schema';
+
+export const advertisementRoutes = createCrudRoutes({
+  entity: Advertisement,
+  createSchema: CreateAdvertisementDto,
+  updateSchema: UpdateAdvertisementDto,
+  getSchema: AdvertisementSchema,
+  listSchema: AdvertisementSchema,
+  searchFields: ['title', 'code'],
+  relations: ['imageFile', 'advertisementType'],
+  middleware: [authMiddleware],
+  userTracking: {
+    createdByField: 'createdBy',
+    updatedByField: 'updatedBy'
+  }
+});

+ 81 - 0
packages/advertisements-module/src/schemas/advertisement-type.schema.ts

@@ -0,0 +1,81 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告类型实体Schema
+export const AdvertisementTypeSchema = 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: 'home_banner'
+  }),
+  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 CreateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  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 UpdateAdvertisementTypeDto = z.object({
+  name: z.string().min(1).max(50).optional().openapi({
+    description: '类型名称',
+    example: '首页轮播'
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  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
+  })
+});

+ 145 - 0
packages/advertisements-module/src/schemas/advertisement.schema.ts

@@ -0,0 +1,145 @@
+import { z } from '@hono/zod-openapi';
+
+// 广告实体Schema
+export const AdvertisementSchema = z.object({
+  id: z.number().int().positive().openapi({
+    description: '广告ID',
+    example: 1
+  }),
+  title: z.string().max(30).nullable().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.number().int().positive().nullable().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().max(20).nullable().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'banner.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/banner.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '图片文件信息'
+  }),
+  advertisementType: z.object({
+    id: z.number().int().positive().openapi({ description: '广告类型ID' }),
+    name: z.string().max(50).openapi({ description: '类型名称', example: '首页轮播' }),
+    code: z.string().max(20).openapi({ description: '类型编码', example: 'home_banner' })
+  }).nullable().optional().openapi({
+    description: '广告类型信息'
+  }),
+  sort: z.number().int().default(0).openapi({
+    description: '排序值',
+    example: 10
+  }),
+  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
+  }),
+  actionType: z.number().int().min(0).max(2).default(1).openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 创建广告DTO
+export const CreateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().default(0).optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).default(0).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).default(1).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});
+
+// 更新广告DTO
+export const UpdateAdvertisementDto = z.object({
+  title: z.string().min(1).max(30).optional().openapi({
+    description: '标题',
+    example: '首页轮播图'
+  }),
+  typeId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '广告类型',
+    example: 1
+  }),
+  code: z.string().min(1).max(20).optional().openapi({
+    description: '调用别名',
+    example: 'home_banner'
+  }),
+  url: z.string().max(255).nullable().optional().openapi({
+    description: '跳转URL',
+    example: 'https://example.com'
+  }),
+  imageFileId: z.coerce.number<number>().int().positive().optional().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  sort: z.coerce.number<number>().int().optional().openapi({
+    description: '排序值',
+    example: 10
+  }),
+  status: z.coerce.number<number>().int().min(0).max(1).optional().openapi({
+    description: '状态 0禁用 1启用',
+    example: 1
+  }),
+  actionType: z.coerce.number<number>().int().min(0).max(2).optional().openapi({
+    description: '跳转类型 0不跳转 1webview 2小程序页面',
+    example: 1
+  })
+});

+ 2 - 0
packages/advertisements-module/src/schemas/index.ts

@@ -0,0 +1,2 @@
+export * from './advertisement.schema';
+export * from './advertisement-type.schema';

+ 9 - 0
packages/advertisements-module/src/services/advertisement-type.service.ts

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

+ 9 - 0
packages/advertisements-module/src/services/advertisement.service.ts

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

+ 268 - 0
packages/advertisements-module/tests/integration/advertisement-types.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 advertisementTypeRoutes from '../../src/routes/advertisement-types';
+import { AdvertisementType } from '../../src/entities/advertisement-type.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntity, File, Role, AdvertisementType])
+
+describe('广告类型管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof advertisementTypeRoutes>>;
+  let testToken: string;
+  let testUser: UserEntity;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(advertisementTypeRoutes);
+
+    // 获取数据源
+    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 /advertisement-types', () => {
+    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 /advertisement-types', () => {
+    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 advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const existingType = advertisementTypeRepository.create({
+        name: '现有类型',
+        code: 'existing_code',
+        status: 1
+      });
+      await advertisementTypeRepository.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 /advertisement-types/:id', () => {
+    it('应该返回指定广告类型的详情', async () => {
+      // 先创建一个广告类型
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const testType = advertisementTypeRepository.create({
+        name: '测试类型详情',
+        code: 'test_type_detail',
+        remark: '测试备注',
+        status: 1
+      });
+      await advertisementTypeRepository.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 /advertisement-types/:id', () => {
+    it('应该成功更新广告类型', async () => {
+      // 先创建一个广告类型
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const testType = advertisementTypeRepository.create({
+        name: '原始类型',
+        code: 'original_type',
+        remark: '原始备注',
+        status: 1
+      });
+      await advertisementTypeRepository.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 /advertisement-types/:id', () => {
+    it('应该成功删除广告类型', async () => {
+      // 先创建一个广告类型
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+      const testType = advertisementTypeRepository.create({
+        name: '待删除类型',
+        code: 'delete_type',
+        remark: '待删除备注',
+        status: 1
+      });
+      await advertisementTypeRepository.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 advertisementTypeRepository.findOne({
+        where: { id: testType.id }
+      });
+      expect(deletedType).toBeNull();
+    });
+  });
+});

+ 264 - 0
packages/advertisements-module/tests/integration/advertisements.integration.test.ts

@@ -0,0 +1,264 @@
+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 advertisementRoutes from '../../src/routes/advertisements';
+import { Advertisement } from '../../src/entities/advertisement.entity';
+import { AdvertisementType } from '../../src/entities/advertisement-type.entity';
+
+// 设置集成测试钩子
+setupIntegrationDatabaseHooksWithEntities([UserEntity, File, Role, Advertisement, AdvertisementType])
+
+describe('广告管理API集成测试', () => {
+  let client: ReturnType<typeof testClient<typeof advertisementRoutes>>;
+  let testToken: string;
+  let testUser: UserEntity;
+  let testAdvertisementType: AdvertisementType;
+
+  beforeEach(async () => {
+    // 创建测试客户端
+    client = testClient(advertisementRoutes);
+
+    // 获取数据源
+    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);
+
+    // 创建测试广告类型
+    const advertisementTypeRepository = dataSource.getRepository(AdvertisementType);
+    testAdvertisementType = advertisementTypeRepository.create({
+      name: '首页轮播',
+      code: 'home_banner',
+      remark: '用于首页轮播图展示',
+      status: 1
+    });
+    await advertisementTypeRepository.save(testAdvertisementType);
+
+    // 生成测试用户的token
+    testToken = JWTUtil.generateToken({
+      id: testUser.id,
+      username: testUser.username,
+      roles: [{name:'user'}]
+    });
+  });
+
+  describe('GET /advertisements', () => {
+    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 /advertisements', () => {
+    it('应该成功创建广告', async () => {
+      const createData = {
+        title: '测试广告',
+        typeId: testAdvertisementType.id,
+        code: 'test_ad',
+        url: 'https://example.com',
+        sort: 10,
+        status: 1,
+        actionType: 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.title).toBe(createData.title);
+        expect(data.code).toBe(createData.code);
+        expect(data.status).toBe(createData.status);
+      }
+    });
+
+    it('应该验证创建广告的必填字段', async () => {
+      const invalidData = {
+        // 缺少必填字段
+        title: '',
+        typeId: 0,
+        code: '',
+        url: 'https://example.com'
+      };
+
+      const response = await client.index.$post({
+        json: invalidData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(400);
+    });
+  });
+
+  describe('GET /advertisements/:id', () => {
+    it('应该返回指定广告的详情', async () => {
+      // 先创建一个广告
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+      const testAdvertisement = advertisementRepository.create({
+        title: '测试广告详情',
+        typeId: testAdvertisementType.id,
+        code: 'test_ad_detail',
+        url: 'https://example.com',
+        sort: 5,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id
+      });
+      await advertisementRepository.save(testAdvertisement);
+
+      const response = await client[':id'].$get({
+        param: { id: testAdvertisement.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(testAdvertisement.id);
+        expect(data.title).toBe(testAdvertisement.title);
+        expect(data.code).toBe(testAdvertisement.code);
+      }
+    });
+
+    it('应该处理不存在的广告', async () => {
+      const response = await client[':id'].$get({
+        param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      expect(response.status).toBe(404);
+    });
+  });
+
+  describe('PUT /advertisements/:id', () => {
+    it('应该成功更新广告', async () => {
+      // 先创建一个广告
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+      const testAdvertisement = advertisementRepository.create({
+        title: '原始广告',
+        typeId: testAdvertisementType.id,
+        code: 'original_ad',
+        url: 'https://example.com',
+        sort: 5,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id
+      });
+      await advertisementRepository.save(testAdvertisement);
+
+      const updateData = {
+        title: '更新后的广告',
+        code: 'updated_ad',
+        sort: 15
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testAdvertisement.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.title).toBe(updateData.title);
+        expect(data.code).toBe(updateData.code);
+        expect(data.sort).toBe(updateData.sort);
+      }
+    });
+  });
+
+  describe('DELETE /advertisements/:id', () => {
+    it('应该成功删除广告', async () => {
+      // 先创建一个广告
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      const advertisementRepository = dataSource.getRepository(Advertisement);
+      const testAdvertisement = advertisementRepository.create({
+        title: '待删除广告',
+        typeId: testAdvertisementType.id,
+        code: 'delete_ad',
+        url: 'https://example.com',
+        sort: 5,
+        status: 1,
+        actionType: 1,
+        createdBy: testUser.id
+      });
+      await advertisementRepository.save(testAdvertisement);
+
+      const response = await client[':id'].$delete({
+        param: { id: testAdvertisement.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
+      });
+
+      console.debug('删除广告响应状态:', response.status);
+      expect(response.status).toBe(204);
+
+      // 验证广告确实被删除
+      const deletedAdvertisement = await advertisementRepository.findOne({
+        where: { id: testAdvertisement.id }
+      });
+      expect(deletedAdvertisement).toBeNull();
+    });
+  });
+});

+ 16 - 0
packages/advertisements-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/advertisements-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
+  }
+});

+ 36 - 0
packages/area-management-ui-mt/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',
+    },
+  },
+];

+ 96 - 0
packages/area-management-ui-mt/package.json

@@ -0,0 +1,96 @@
+{
+  "name": "@d8d/area-management-ui-mt",
+  "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/shared-types": "workspace:*",
+    "@d8d/shared-ui-components": "workspace:*",
+    "@d8d/geo-areas-mt": "workspace:*",
+    "@hookform/resolvers": "^5.2.1",
+    "@tanstack/react-query": "^5.90.9",
+    "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": [
+    "area",
+    "management",
+    "admin",
+    "ui",
+    "react",
+    "crud",
+    "tree",
+    "geography",
+    "multi-tenant",
+    "tenant-isolation"
+  ],
+  "author": "D8D Team",
+  "license": "MIT"
+}

+ 44 - 0
packages/area-management-ui-mt/src/api/areaClient.ts

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

+ 9 - 0
packages/area-management-ui-mt/src/api/index.ts

@@ -0,0 +1,9 @@
+export { areaClient, areaClientManager } from './areaClient';
+export type {
+  AreaResponse,
+  CreateAreaRequest,
+  UpdateAreaRequest,
+  AreaNode,
+  AreaFormData,
+  AreaQueryParams
+} from '../types/area';

+ 221 - 0
packages/area-management-ui-mt/src/components/AreaForm.tsx

@@ -0,0 +1,221 @@
+import React from 'react';
+import { useForm } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Input } from '@d8d/shared-ui-components/components/ui/input';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { createAreaSchemaMt, updateAreaSchemaMt, AreaLevel } from '@d8d/geo-areas-mt/schemas';
+import type { CreateAreaInputMt, UpdateAreaInputMt } from '@d8d/geo-areas-mt/schemas';
+
+// 禁用状态枚举
+enum DisabledStatus {
+  ENABLED = 0,
+  DISABLED = 1
+}
+
+interface AreaFormProps {
+  area?: UpdateAreaInputMt & { id?: number };
+  onSubmit: (data: CreateAreaInputMt | UpdateAreaInputMt) => Promise<void>;
+  onCancel: () => void;
+  isLoading?: boolean;
+  /** 智能预填的层级 */
+  smartLevel?: number;
+  /** 智能预填的父级ID */
+  smartParentId?: number;
+}
+
+// 辅助函数:根据层级值获取显示名称
+const getLevelDisplayName = (level: number | undefined): string => {
+  switch (level) {
+    case AreaLevel.PROVINCE:
+      return '省/直辖市';
+    case AreaLevel.CITY:
+      return '市';
+    case AreaLevel.DISTRICT:
+      return '区/县';
+    case AreaLevel.TOWN:
+      return '街道/乡镇';
+    default:
+      return '未知层级';
+  }
+};
+
+export const AreaForm: React.FC<AreaFormProps> = ({
+  area,
+  onSubmit,
+  onCancel,
+  isLoading = false,
+  smartLevel,
+  smartParentId
+}) => {
+  const isEditing = !!area;
+
+  const form = useForm<CreateAreaInputMt | UpdateAreaInputMt>({
+    resolver: zodResolver(isEditing ? updateAreaSchemaMt : createAreaSchemaMt),
+    defaultValues: area ? {
+      tenantId: area.tenantId,
+      parentId: area.parentId,
+      name: area.name,
+      level: area.level,
+      code: area.code,
+      isDisabled: area.isDisabled,
+    } : {
+      tenantId: 1, // 测试环境使用默认tenantId
+      parentId: smartParentId || null,
+      name: '',
+      level: smartLevel ?? AreaLevel.PROVINCE,
+      code: '',
+      isDisabled: DisabledStatus.ENABLED,
+    },
+  });
+
+  const handleSubmit = async (data: CreateAreaInputMt | UpdateAreaInputMt) => {
+    await onSubmit(data);
+  };
+
+
+  return (
+    <Form {...form}>
+      <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6">
+        <div className="grid grid-cols-1 gap-6">
+          {/* 层级显示(只读) */}
+          <FormField
+            control={form.control}
+            name="level"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>层级</FormLabel>
+                <FormControl>
+                  <Input
+                    value={getLevelDisplayName(field.value)}
+                    disabled
+                    className="bg-muted"
+                  />
+                </FormControl>
+                <FormDescription>
+                  根据操作上下文自动设置的层级
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 父级区域显示(只读) */}
+          <FormField
+            control={form.control}
+            name="parentId"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>父级区域</FormLabel>
+                <FormControl>
+                  <Input
+                    type="number"
+                    value={field.value || ''}
+                    disabled
+                    className="bg-muted"
+                    placeholder="顶级区域(无父级)"
+                  />
+                </FormControl>
+                <FormDescription>
+                  根据操作上下文自动设置的父级区域ID
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 区域名称 */}
+          <FormField
+            control={form.control}
+            name="name"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>区域名称</FormLabel>
+                <FormControl>
+                  <Input
+                    placeholder="输入区域名称"
+                    {...field}
+                  />
+                </FormControl>
+                <FormDescription>
+                  输入省市区名称,如:北京市、上海市、朝阳区等
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 行政区划代码 */}
+          <FormField
+            control={form.control}
+            name="code"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>行政区划代码</FormLabel>
+                <FormControl>
+                  <Input
+                    placeholder="输入行政区划代码"
+                    {...field}
+                  />
+                </FormControl>
+                <FormDescription>
+                  输入标准的行政区划代码
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+
+          {/* 状态选择 */}
+          <FormField
+            control={form.control}
+            name="isDisabled"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>状态</FormLabel>
+                <Select onValueChange={(value) => field.onChange(Number(value))} defaultValue={field.value?.toString()}>
+                  <FormControl>
+                    <SelectTrigger>
+                      <SelectValue placeholder="选择状态" />
+                    </SelectTrigger>
+                  </FormControl>
+                  <SelectContent>
+                    <SelectItem value={DisabledStatus.ENABLED.toString()}>
+                      启用
+                    </SelectItem>
+                    <SelectItem value={DisabledStatus.DISABLED.toString()}>
+                      禁用
+                    </SelectItem>
+                  </SelectContent>
+                </Select>
+                <FormDescription>
+                  选择省市区状态
+                </FormDescription>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        </div>
+
+        {/* 表单操作按钮 */}
+        <div className="flex justify-end gap-4">
+          <Button
+            type="button"
+            variant="outline"
+            onClick={onCancel}
+            disabled={isLoading}
+          >
+            取消
+          </Button>
+          <Button
+            type="submit"
+            disabled={isLoading}
+          >
+            {isLoading ? '提交中...' : isEditing ? '更新' : '创建'}
+          </Button>
+        </div>
+      </form>
+    </Form>
+  );
+};

+ 476 - 0
packages/area-management-ui-mt/src/components/AreaManagement.tsx

@@ -0,0 +1,476 @@
+import React from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@d8d/shared-ui-components/components/ui/card';
+import { Plus } from 'lucide-react';
+import { useState } from 'react';
+import { areaClient, areaClientManager } from '../api/areaClient';
+import type { InferResponseType, InferRequestType } from 'hono/client';
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@d8d/shared-ui-components/components/ui/dialog';
+import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@d8d/shared-ui-components/components/ui/alert-dialog';
+import { AreaForm } from './AreaForm';
+import { AreaTreeAsync } from './AreaTreeAsync';
+import type { CreateAreaInputMt, UpdateAreaInputMt } from '@d8d/geo-areas-mt/schemas';
+import { toast } from 'sonner';
+
+// 类型提取规范
+type AreaResponse = InferResponseType<typeof areaClient.index.$get, 200>['data'][0];
+type CreateAreaRequest = InferRequestType<typeof areaClient.index.$post>['json'];
+type UpdateAreaRequest = InferRequestType<typeof areaClient[':id']['$put']>['json'];
+
+// 树形节点类型
+interface AreaNode {
+  id: number;
+  name: string;
+  code: string;
+  level: number;
+  parentId: number | null;
+  isDisabled: number;
+  children?: AreaNode[];
+}
+
+// 统一操作处理函数
+const handleOperation = async (operation: () => Promise<void>) => {
+  try {
+    await operation();
+    // toast.success('操作成功');
+  } catch (error) {
+    // toast.error('操作失败,请重试');
+    throw error;
+  }
+};
+
+
+export const AreaManagement: React.FC = () => {
+  const queryClient = useQueryClient();
+  const [expandedNodes, setExpandedNodes] = useState<Set<number>>(new Set());
+  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
+  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
+  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+  const [isStatusDialogOpen, setIsStatusDialogOpen] = useState(false);
+  const [selectedArea, setSelectedArea] = useState<AreaResponse | null>(null);
+  const [isAddChildDialogOpen, setIsAddChildDialogOpen] = useState(false);
+  const [parentAreaForChild, setParentAreaForChild] = useState<AreaNode | null>(null);
+
+  // 查询省级数据(异步加载)
+  const { data: provinceData, isLoading: isProvinceLoading } = useQuery({
+    queryKey: ['areas-tree-province'],
+    queryFn: async () => {
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100 ,
+          filters: JSON.stringify({ level: 1}),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取省级数据失败');
+      const response = await res.json();
+      return response.data;
+    },
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+  });
+
+  // 创建省市区
+  const createMutation = useMutation({
+    mutationFn: async (data: CreateAreaRequest) => {
+      await handleOperation(async () => {
+        const res = await areaClientManager.get().index.$post({ json: data });
+        if (res.status !== 201) throw new Error('创建省市区失败');
+      });
+    },
+    onSuccess: (_, variables) => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+
+      // 如果创建的是子节点,更新父节点的子树缓存
+      if (variables.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['areas-subtree', variables.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success('省市区创建成功');
+
+      // 关闭对话框
+      setIsCreateDialogOpen(false);
+
+      // 如果是创建子节点,还需要关闭子节点对话框
+      if (variables.parentId) {
+        setIsAddChildDialogOpen(false);
+        setParentAreaForChild(null);
+      }
+    },
+    onError: () => {
+      toast.error('创建失败,请重试');
+    }
+  });
+
+  // 更新省市区
+  const updateMutation = useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateAreaRequest }) => {
+      await handleOperation(async () => {
+        const res = await areaClientManager.get()[':id'].$put({
+          param: { id },
+          json: data
+        });
+        if (res.status !== 200) throw new Error('更新省市区失败');
+      });
+    },
+    onSuccess: () => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+
+      // 如果更新的节点有父节点,更新父节点的子树缓存
+      if (selectedArea?.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success('省市区更新成功');
+
+      setIsEditDialogOpen(false);
+      setSelectedArea(null);
+    },
+    onError: () => {
+      toast.error('更新失败,请重试');
+    }
+  });
+
+  // 删除省市区
+  const deleteMutation = useMutation({
+    mutationFn: async (id: number) => {
+      await handleOperation(async () => {
+        const res = await areaClientManager.get()[':id'].$delete({
+          param: { id }
+        });
+        if (res.status !== 204) throw new Error('删除省市区失败');
+      });
+    },
+    onSuccess: () => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+
+      // 如果删除的节点有父节点,更新父节点的子树缓存
+      if (selectedArea?.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success('省市区删除成功');
+
+      setIsDeleteDialogOpen(false);
+      setSelectedArea(null);
+    },
+    onError: () => {
+      toast.error('删除失败,请重试');
+    }
+  });
+
+  // 启用/禁用省市区
+  const toggleStatusMutation = useMutation({
+    mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
+      await handleOperation(async () => {
+        const res = await areaClientManager.get()[':id'].$put({
+          param: { id },
+          json: { isDisabled }
+        });
+        if (res.status !== 200) throw new Error('更新省市区状态失败');
+      });
+    },
+    onSuccess: () => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+
+      // 如果状态切换的节点有父节点,更新父节点的子树缓存
+      if (selectedArea?.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['areas-subtree', selectedArea.parentId] });
+      }
+
+      // 显示成功提示
+      toast.success(`省市区${selectedArea?.isDisabled === 0 ? '禁用' : '启用'}成功`);
+
+      setIsStatusDialogOpen(false);
+      setSelectedArea(null);
+    },
+    onError: () => {
+      toast.error('状态切换失败,请重试');
+    }
+  });
+
+
+  // 处理创建省市区
+  const handleCreateArea = async (data: CreateAreaInputMt | UpdateAreaInputMt) => {
+    const createData = {
+      ...data,
+      tenantId: 1 // 测试环境使用默认tenantId
+    } as CreateAreaInputMt;
+    await createMutation.mutateAsync(createData);
+  };
+
+  // 处理更新省市区
+  const handleUpdateArea = async (data: UpdateAreaInputMt) => {
+    if (!selectedArea) return;
+    const updateData = {
+      ...data,
+      tenantId: 1 // 测试环境使用默认tenantId
+    } as UpdateAreaInputMt;
+    await updateMutation.mutateAsync({ id: selectedArea.id, data: updateData });
+  };
+
+  // 处理删除省市区
+  const handleDeleteArea = async () => {
+    if (!selectedArea) return;
+    await deleteMutation.mutateAsync(selectedArea.id);
+  };
+
+  // 处理启用/禁用省市区
+  const handleToggleStatus = async (isDisabled: number) => {
+    if (!selectedArea) return;
+    await toggleStatusMutation.mutateAsync({ id: selectedArea.id, isDisabled });
+  };
+
+  // 处理新增子节点
+  const handleAddChild = (area: AreaNode) => {
+    setParentAreaForChild(area);
+    setIsAddChildDialogOpen(true);
+  };
+
+  // 处理创建子节点
+  const handleCreateChildArea = async (data: CreateAreaInputMt | UpdateAreaInputMt) => {
+    const createData = {
+      ...data,
+      tenantId: 1 // 测试环境使用默认tenantId
+    } as CreateAreaInputMt;
+    await createMutation.mutateAsync(createData);
+  };
+
+  // 打开编辑对话框
+  const handleEdit = (area: AreaNode) => {
+    // 将 AreaNode 转换为 AreaResponse
+    const areaResponse: AreaResponse = {
+      ...area,
+      isDeleted: 0,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      createdBy: null,
+      updatedBy: null
+    };
+    setSelectedArea(areaResponse);
+    setIsEditDialogOpen(true);
+  };
+
+  // 打开删除对话框
+  const handleDelete = (area: AreaNode) => {
+    // 将 AreaNode 转换为 AreaResponse
+    const areaResponse: AreaResponse = {
+      ...area,
+      isDeleted: 0,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      createdBy: null,
+      updatedBy: null
+    };
+    setSelectedArea(areaResponse);
+    setIsDeleteDialogOpen(true);
+  };
+
+  // 打开状态切换对话框
+  const handleToggleStatusDialog = (area: AreaNode) => {
+    // 将 AreaNode 转换为 AreaResponse
+    const areaResponse: AreaResponse = {
+      ...area,
+      isDeleted: 0,
+      createdAt: new Date().toISOString(),
+      updatedAt: new Date().toISOString(),
+      createdBy: null,
+      updatedBy: null
+    };
+    setSelectedArea(areaResponse);
+    setIsStatusDialogOpen(true);
+  };
+
+  // 切换节点展开状态
+  const handleToggleNode = (nodeId: number) => {
+    setExpandedNodes(prev => {
+      const newSet = new Set(prev);
+      if (newSet.has(nodeId)) {
+        newSet.delete(nodeId);
+      } else {
+        newSet.add(nodeId);
+      }
+      return newSet;
+    });
+  };
+
+
+  return (
+    <div className="space-y-6">
+      <div className="flex items-center justify-between">
+        <div>
+          <h1 className="text-3xl font-bold tracking-tight">省市区树形管理</h1>
+          <p className="text-muted-foreground">
+            异步加载树形结构,高效管理省市区数据
+          </p>
+        </div>
+        <Button onClick={() => setIsCreateDialogOpen(true)}>
+          <Plus className="mr-2 h-4 w-4" />
+          新增省
+        </Button>
+      </div>
+
+      <Card>
+        <CardHeader>
+          <CardTitle>省市区树形结构</CardTitle>
+          <CardDescription>
+            以树形结构查看和管理省市区层级关系,默认只加载省级数据
+          </CardDescription>
+        </CardHeader>
+        <CardContent>
+          {/* 树形视图 */}
+          {isProvinceLoading ? (
+            <div className="text-center py-8">
+              加载中...
+            </div>
+          ) : !provinceData || provinceData.length === 0 ? (
+            <div className="text-center py-8">
+              暂无数据
+            </div>
+          ) : (
+            <AreaTreeAsync
+              areas={provinceData}
+              expandedNodes={expandedNodes}
+              onToggleNode={handleToggleNode}
+              onEdit={handleEdit}
+              onDelete={handleDelete}
+              onToggleStatus={handleToggleStatusDialog}
+              onAddChild={handleAddChild}
+            />
+          )}
+        </CardContent>
+      </Card>
+
+      {/* 创建省市区对话框 */}
+      <Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>新增省</DialogTitle>
+            <DialogDescription>
+              填写省信息
+            </DialogDescription>
+          </DialogHeader>
+          <AreaForm
+            onSubmit={handleCreateArea}
+            isLoading={createMutation.isPending}
+            onCancel={() => setIsCreateDialogOpen(false)}
+            smartLevel={1} // 默认设置为省级
+          />
+        </DialogContent>
+      </Dialog>
+
+      {/* 编辑省市区对话框 */}
+      <Dialog open={isEditDialogOpen} onOpenChange={setIsEditDialogOpen}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>编辑省市区</DialogTitle>
+            <DialogDescription>
+              修改省市区信息
+            </DialogDescription>
+          </DialogHeader>
+          {selectedArea && (
+            <AreaForm
+              area={{
+                id: selectedArea.id,
+                parentId: selectedArea.parentId || 0,
+                name: selectedArea.name,
+                level: selectedArea.level,
+                code: selectedArea.code,
+                isDisabled: selectedArea.isDisabled
+              }}
+              onSubmit={handleUpdateArea}
+              isLoading={updateMutation.isPending}
+              onCancel={() => {
+                setIsEditDialogOpen(false);
+                setSelectedArea(null);
+              }}
+            />
+          )}
+        </DialogContent>
+      </Dialog>
+
+      {/* 新增子节点对话框 */}
+      <Dialog open={isAddChildDialogOpen} onOpenChange={setIsAddChildDialogOpen}>
+        <DialogContent className="max-w-2xl">
+          <DialogHeader>
+            <DialogTitle>
+              {parentAreaForChild?.level === 1 ? '新增市' :
+               parentAreaForChild?.level === 2 ? '新增区' : '新增乡镇'}
+            </DialogTitle>
+            <DialogDescription>
+              {parentAreaForChild?.level === 1
+                ? `在省份 "${parentAreaForChild?.name}" 下新增市`
+                : parentAreaForChild?.level === 2
+                ? `在城市 "${parentAreaForChild?.name}" 下新增区/县`
+                : `在区县 "${parentAreaForChild?.name}" 下新增街道/乡镇`}
+            </DialogDescription>
+          </DialogHeader>
+          <AreaForm
+            onSubmit={handleCreateChildArea}
+            isLoading={createMutation.isPending}
+            onCancel={() => {
+              setIsAddChildDialogOpen(false);
+              setParentAreaForChild(null);
+            }}
+            smartLevel={(parentAreaForChild?.level ?? 0) + 1}
+            smartParentId={parentAreaForChild?.id}
+          />
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除确认对话框 */}
+      <AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>确认删除</AlertDialogTitle>
+            <AlertDialogDescription>
+              确定要删除省市区 "{selectedArea?.name}" 吗?此操作不可恢复。
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>取消</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={handleDeleteArea}
+              disabled={deleteMutation.isPending}
+            >
+              {deleteMutation.isPending ? '删除中...' : '确认删除'}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+
+      {/* 状态切换确认对话框 */}
+      <AlertDialog open={isStatusDialogOpen} onOpenChange={setIsStatusDialogOpen}>
+        <AlertDialogContent>
+          <AlertDialogHeader>
+            <AlertDialogTitle>
+              {selectedArea?.isDisabled === 0 ? '禁用' : '启用'}确认
+            </AlertDialogTitle>
+            <AlertDialogDescription>
+              确定要{selectedArea?.isDisabled === 0 ? '禁用' : '启用'}省市区 "{selectedArea?.name}" 吗?
+            </AlertDialogDescription>
+          </AlertDialogHeader>
+          <AlertDialogFooter>
+            <AlertDialogCancel>取消</AlertDialogCancel>
+            <AlertDialogAction
+              onClick={() => handleToggleStatus(selectedArea?.isDisabled === 0 ? 1 : 0)}
+              disabled={toggleStatusMutation.isPending}
+            >
+              {toggleStatusMutation.isPending ? '处理中...' : '确认'}
+            </AlertDialogAction>
+          </AlertDialogFooter>
+        </AlertDialogContent>
+      </AlertDialog>
+    </div>
+  );
+};

+ 258 - 0
packages/area-management-ui-mt/src/components/AreaSelect.tsx

@@ -0,0 +1,258 @@
+import React, { useState, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { areaClient, areaClientManager } from '../api/areaClient';
+import type { InferResponseType } from 'hono/client';
+
+// 类型定义
+type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
+
+interface AreaSelectProps {
+  value?: {
+    provinceId?: number;
+    cityId?: number;
+    districtId?: number;
+  };
+  onChange?: (value: {
+    provinceId?: number;
+    cityId?: number;
+    districtId?: number;
+  }) => void;
+  disabled?: boolean;
+  required?: boolean;
+  className?: string;
+}
+
+export const AreaSelect: React.FC<AreaSelectProps> = ({
+  value = {},
+  onChange,
+  disabled = false,
+  required = false,
+  className
+}) => {
+  const [selectedProvince, setSelectedProvince] = useState<number | undefined>(value.provinceId);
+  const [selectedCity, setSelectedCity] = useState<number | undefined>(value.cityId);
+  const [selectedDistrict, setSelectedDistrict] = useState<number | undefined>(value.districtId);
+
+  // 查询省份列表
+  const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
+    queryKey: ['areas', 'provinces'],
+    queryFn: async () => {
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 1,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取省份列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+  });
+
+  // 查询城市列表
+  const { data: cities, isLoading: isLoadingCities } = useQuery({
+    queryKey: ['areas', 'cities', selectedProvince],
+    queryFn: async () => {
+      if (!selectedProvince) return { data: [] };
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 2,
+            parentId: selectedProvince,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取城市列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!selectedProvince,
+  });
+
+  // 查询区县列表
+  const { data: districts, isLoading: isLoadingDistricts } = useQuery({
+    queryKey: ['areas', 'districts', selectedCity],
+    queryFn: async () => {
+      if (!selectedCity) return { data: [] };
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 3,
+            parentId: selectedCity,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取区县列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!selectedCity,
+  });
+
+  // 处理省份选择
+  const handleProvinceChange = (provinceId: string) => {
+    const id = provinceId && provinceId !== 'none' ? Number(provinceId) : undefined;
+    setSelectedProvince(id);
+    setSelectedCity(undefined);
+    setSelectedDistrict(undefined);
+
+    onChange?.({
+      provinceId: id,
+      cityId: undefined,
+      districtId: undefined
+    });
+  };
+
+  // 处理城市选择
+  const handleCityChange = (cityId: string) => {
+    const id = cityId && cityId !== 'none' ? Number(cityId) : undefined;
+    setSelectedCity(id);
+    setSelectedDistrict(undefined);
+
+    onChange?.({
+      provinceId: selectedProvince,
+      cityId: id,
+      districtId: undefined
+    });
+  };
+
+  // 处理区县选择
+  const handleDistrictChange = (districtId: string) => {
+    const id = districtId && districtId !== 'none' ? Number(districtId) : undefined;
+    setSelectedDistrict(id);
+
+    onChange?.({
+      provinceId: selectedProvince,
+      cityId: selectedCity,
+      districtId: id
+    });
+  };
+
+  // 同步外部值变化
+  useEffect(() => {
+    setSelectedProvince(value.provinceId);
+    setSelectedCity(value.cityId);
+    setSelectedDistrict(value.districtId);
+  }, [value.provinceId, value.cityId, value.districtId]);
+
+  return (
+    <div className={`grid grid-cols-1 md:grid-cols-3 gap-4 ${className}`}>
+      {/* 省份选择 */}
+      <div>
+        <FormItem>
+          <FormLabel>
+            省份{required && <span className="text-destructive">*</span>}
+          </FormLabel>
+          <Select
+            value={selectedProvince?.toString() || ''}
+            onValueChange={handleProvinceChange}
+            disabled={disabled || isLoadingProvinces}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择省份" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="none">请选择省份</SelectItem>
+              {provinces?.data.map((province: AreaResponse) => (
+                <SelectItem key={province.id} value={province.id.toString()}>
+                  {province.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          <FormDescription>
+            选择所在省份
+          </FormDescription>
+          <FormMessage />
+        </FormItem>
+      </div>
+
+      {/* 城市选择 */}
+      <div>
+        <FormItem>
+          <FormLabel>
+            城市{required && selectedProvince && <span className="text-destructive">*</span>}
+          </FormLabel>
+          <Select
+            value={selectedCity?.toString() || ''}
+            onValueChange={handleCityChange}
+            disabled={disabled || !selectedProvince || isLoadingCities}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择城市" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="none">请选择城市</SelectItem>
+              {cities?.data.map((city: AreaResponse) => (
+                <SelectItem key={city.id} value={city.id.toString()}>
+                  {city.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          <FormDescription>
+            选择所在城市
+          </FormDescription>
+          <FormMessage />
+        </FormItem>
+      </div>
+
+      {/* 区县选择 */}
+      <div>
+        <FormItem>
+          <FormLabel>
+            区县{required && selectedCity && <span className="text-destructive">*</span>}
+          </FormLabel>
+          <Select
+            value={selectedDistrict?.toString() || ''}
+            onValueChange={handleDistrictChange}
+            disabled={disabled || !selectedCity || isLoadingDistricts}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择区县" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="none">请选区县</SelectItem>
+              {districts?.data.map((district: AreaResponse) => (
+                <SelectItem key={district.id} value={district.id.toString()}>
+                  {district.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          <FormDescription>
+            选择所在区县
+          </FormDescription>
+          <FormMessage />
+        </FormItem>
+      </div>
+    </div>
+  );
+};

+ 350 - 0
packages/area-management-ui-mt/src/components/AreaSelect4Level.tsx

@@ -0,0 +1,350 @@
+import React, { useState, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@d8d/shared-ui-components/components/ui/select';
+import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from '@d8d/shared-ui-components/components/ui/form';
+import { areaClient, areaClientManager } from '../api/areaClient';
+import type { InferResponseType } from 'hono/client';
+
+// 类型定义
+type AreaResponse = InferResponseType<typeof areaClient.$get, 200>['data'][0];
+
+interface AreaSelect4LevelProps {
+  provinceValue?: number;
+  cityValue?: number;
+  districtValue?: number;
+  townValue?: number;
+  onProvinceChange?: (value: number) => void;
+  onCityChange?: (value: number) => void;
+  onDistrictChange?: (value: number) => void;
+  onTownChange?: (value: number) => void;
+  disabled?: boolean;
+  required?: boolean;
+  className?: string;
+  showLabels?: boolean;
+}
+
+export const AreaSelect4Level: React.FC<AreaSelect4LevelProps> = ({
+  provinceValue = 0,
+  cityValue = 0,
+  districtValue = 0,
+  townValue = 0,
+  onProvinceChange,
+  onCityChange,
+  onDistrictChange,
+  onTownChange,
+  disabled = false,
+  required = false,
+  className = '',
+  showLabels = true
+}) => {
+  const [selectedProvince, setSelectedProvince] = useState<number>(provinceValue);
+  const [selectedCity, setSelectedCity] = useState<number>(cityValue);
+  const [selectedDistrict, setSelectedDistrict] = useState<number>(districtValue);
+  const [selectedTown, setSelectedTown] = useState<number>(townValue);
+
+  // 查询省份列表
+  const { data: provinces, isLoading: isLoadingProvinces } = useQuery({
+    queryKey: ['areas', 'provinces'],
+    queryFn: async () => {
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 1,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取省份列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+  });
+
+  // 查询城市列表
+  const { data: cities, isLoading: isLoadingCities } = useQuery({
+    queryKey: ['areas', 'cities', selectedProvince],
+    queryFn: async () => {
+      if (!selectedProvince) return { data: [] };
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 2,
+            parentId: selectedProvince,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取城市列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!selectedProvince,
+  });
+
+  // 查询区县列表
+  const { data: districts, isLoading: isLoadingDistricts } = useQuery({
+    queryKey: ['areas', 'districts', selectedCity],
+    queryFn: async () => {
+      if (!selectedCity) return { data: [] };
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 3,
+            parentId: selectedCity,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取区县列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!selectedCity,
+  });
+
+  // 查询乡镇列表
+  const { data: towns, isLoading: isLoadingTowns } = useQuery({
+    queryKey: ['areas', 'towns', selectedDistrict],
+    queryFn: async () => {
+      if (!selectedDistrict) return { data: [] };
+      const res = await areaClientManager.get().$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({
+            level: 4,
+            parentId: selectedDistrict,
+            isDisabled: 0
+          }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取乡镇列表失败');
+      return await res.json();
+    },
+    staleTime: 10 * 60 * 1000,
+    gcTime: 30 * 60 * 1000,
+    enabled: !!selectedDistrict,
+  });
+
+  // 处理省份选择
+  const handleProvinceChange = (provinceId: string) => {
+    const id = provinceId && provinceId !== 'none' ? Number(provinceId) : 0;
+    setSelectedProvince(id);
+    setSelectedCity(0);
+    setSelectedDistrict(0);
+    setSelectedTown(0);
+    onProvinceChange?.(id);
+    onCityChange?.(0);
+    onDistrictChange?.(0);
+    onTownChange?.(0);
+  };
+
+  // 处理城市选择
+  const handleCityChange = (cityId: string) => {
+    const id = cityId && cityId !== 'none' ? Number(cityId) : 0;
+    setSelectedCity(id);
+    setSelectedDistrict(0);
+    setSelectedTown(0);
+    onCityChange?.(id);
+    onDistrictChange?.(0);
+    onTownChange?.(0);
+  };
+
+  // 处理区县选择
+  const handleDistrictChange = (districtId: string) => {
+    const id = districtId && districtId !== 'none' ? Number(districtId) : 0;
+    setSelectedDistrict(id);
+    setSelectedTown(0);
+    onDistrictChange?.(id);
+    onTownChange?.(0);
+  };
+
+  // 处理乡镇选择
+  const handleTownChange = (townId: string) => {
+    const id = townId && townId !== 'none' ? Number(townId) : 0;
+    setSelectedTown(id);
+    onTownChange?.(id);
+  };
+
+  // 同步外部值变化
+  useEffect(() => {
+    setSelectedProvince(provinceValue);
+  }, [provinceValue]);
+
+  useEffect(() => {
+    setSelectedCity(cityValue);
+  }, [cityValue]);
+
+  useEffect(() => {
+    setSelectedDistrict(districtValue);
+  }, [districtValue]);
+
+  useEffect(() => {
+    setSelectedTown(townValue);
+  }, [townValue]);
+
+  return (
+    <div className={`grid grid-cols-1 md:grid-cols-4 gap-4 ${className}`}>
+      {/* 省份选择 */}
+      <div>
+        <FormItem>
+          {showLabels && (
+            <FormLabel>
+              省份{required && <span className="text-destructive">*</span>}
+            </FormLabel>
+          )}
+          <Select
+            value={selectedProvince?.toString() || '0'}
+            onValueChange={handleProvinceChange}
+            disabled={disabled || isLoadingProvinces}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择省份" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="0">请选择省份</SelectItem>
+              {provinces?.data.map((province: AreaResponse) => (
+                <SelectItem key={province.id} value={province.id.toString()}>
+                  {province.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          {showLabels && (
+            <FormDescription>
+              选择所在省份
+            </FormDescription>
+          )}
+          <FormMessage />
+        </FormItem>
+      </div>
+
+      {/* 城市选择 */}
+      <div>
+        <FormItem>
+          {showLabels && (
+            <FormLabel>
+              城市{required && selectedProvince && <span className="text-destructive">*</span>}
+            </FormLabel>
+          )}
+          <Select
+            value={selectedCity?.toString() || '0'}
+            onValueChange={handleCityChange}
+            disabled={disabled || !selectedProvince || isLoadingCities}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择城市" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="0">请选择城市</SelectItem>
+              {cities?.data.map((city: AreaResponse) => (
+                <SelectItem key={city.id} value={city.id.toString()}>
+                  {city.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          {showLabels && (
+            <FormDescription>
+              选择所在城市
+            </FormDescription>
+          )}
+          <FormMessage />
+        </FormItem>
+      </div>
+
+      {/* 区县选择 */}
+      <div>
+        <FormItem>
+          {showLabels && (
+            <FormLabel>
+              区县{required && selectedCity && <span className="text-destructive">*</span>}
+            </FormLabel>
+          )}
+          <Select
+            value={selectedDistrict?.toString() || '0'}
+            onValueChange={handleDistrictChange}
+            disabled={disabled || !selectedCity || isLoadingDistricts}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择区县" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="0">请选区县</SelectItem>
+              {districts?.data.map((district: AreaResponse) => (
+                <SelectItem key={district.id} value={district.id.toString()}>
+                  {district.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          {showLabels && (
+            <FormDescription>
+              选择所在区县
+            </FormDescription>
+          )}
+          <FormMessage />
+        </FormItem>
+      </div>
+
+      {/* 乡镇选择 */}
+      <div>
+        <FormItem>
+          {showLabels && (
+            <FormLabel>
+              乡镇{required && selectedDistrict && <span className="text-destructive">*</span>}
+            </FormLabel>
+          )}
+          <Select
+            value={selectedTown?.toString() || '0'}
+            onValueChange={handleTownChange}
+            disabled={disabled || !selectedDistrict || isLoadingTowns}
+          >
+            <FormControl>
+              <SelectTrigger>
+                <SelectValue placeholder="选择乡镇" />
+              </SelectTrigger>
+            </FormControl>
+            <SelectContent>
+              <SelectItem value="0">请选择乡镇</SelectItem>
+              {towns?.data.map((town: AreaResponse) => (
+                <SelectItem key={town.id} value={town.id.toString()}>
+                  {town.name}
+                </SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+          {showLabels && (
+            <FormDescription>
+              选择所在乡镇
+            </FormDescription>
+          )}
+          <FormMessage />
+        </FormItem>
+      </div>
+    </div>
+  );
+};

+ 301 - 0
packages/area-management-ui-mt/src/components/AreaTreeAsync.tsx

@@ -0,0 +1,301 @@
+import React from 'react';
+import { ChevronRight, ChevronDown, Folder, FolderOpen, Loader2 } from 'lucide-react';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Badge } from '@d8d/shared-ui-components/components/ui/badge';
+import { cn } from '@d8d/shared-ui-components/utils';
+import { useQuery } from '@tanstack/react-query';
+import { areaClientManager } from '../api/areaClient';
+import type { AreaNode } from '../types/area';
+
+interface AreaTreeAsyncProps {
+  areas: AreaNode[];
+  expandedNodes: Set<number>;
+  onToggleNode: (nodeId: number) => void;
+  onEdit: (area: AreaNode) => void;
+  onDelete: (area: AreaNode) => void;
+  onToggleStatus: (area: AreaNode) => void;
+  onAddChild: (area: AreaNode) => void;
+}
+
+// 子树加载组件
+interface SubTreeLoaderProps {
+  nodeId: number;
+  isExpanded: boolean;
+  hasChildren: boolean;
+  depth: number;
+  expandedNodes: Set<number>;
+  onToggleNode: (nodeId: number) => void;
+  onEdit: (area: AreaNode) => void;
+  onDelete: (area: AreaNode) => void;
+  onToggleStatus: (area: AreaNode) => void;
+  onAddChild: (area: AreaNode) => void;
+}
+
+const SubTreeLoader: React.FC<SubTreeLoaderProps> = ({
+  nodeId,
+  isExpanded,
+  hasChildren,
+  depth,
+  expandedNodes,
+  onToggleNode,
+  onEdit,
+  onDelete,
+  onToggleStatus,
+  onAddChild
+}) => {
+  const { data: subTreeData, isLoading: isSubTreeLoading } = useQuery({
+    queryKey: ['areas-subtree', nodeId],
+    queryFn: async () => {
+      const res = await areaClientManager.get().index.$get({
+        query: {
+          page: 1,
+          pageSize: 100 ,
+          filters: JSON.stringify({ parentId: nodeId}),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取子树失败');
+      const response = await res.json();
+      return response.data;
+    },
+    enabled: isExpanded && hasChildren,
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+  });
+
+  if (isSubTreeLoading) {
+    return (
+      <div className="flex items-center justify-center py-2 px-3 text-muted-foreground">
+        <Loader2 className="h-4 w-4 animate-spin mr-2" />
+        加载中...
+      </div>
+    );
+  }
+
+  if (!subTreeData) {
+    return (
+      <div className="py-2 px-3 text-muted-foreground text-sm">
+        暂无子节点
+      </div>
+    );
+  }
+
+  // subTreeData 是一个 AreaNode 数组,直接使用
+  const childNodes = subTreeData || [];
+
+  if (childNodes.length === 0) {
+    return (
+      <div className="py-2 px-3 text-muted-foreground text-sm">
+        暂无子节点
+      </div>
+    );
+  }
+
+  return (
+    <div>
+      {childNodes.map((node: AreaNode) => (
+        <TreeNode
+          key={node.id}
+          node={node}
+          depth={depth + 1}
+          expandedNodes={expandedNodes}
+          onToggleNode={onToggleNode}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          onToggleStatus={onToggleStatus}
+          onAddChild={onAddChild}
+        />
+      ))}
+    </div>
+  );
+};
+
+// 树节点组件
+interface TreeNodeProps {
+  node: AreaNode;
+  depth?: number;
+  expandedNodes: Set<number>;
+  onToggleNode: (nodeId: number) => void;
+  onEdit: (area: AreaNode) => void;
+  onDelete: (area: AreaNode) => void;
+  onToggleStatus: (area: AreaNode) => void;
+  onAddChild: (area: AreaNode) => void;
+}
+
+const TreeNode: React.FC<TreeNodeProps> = ({
+  node,
+  depth = 0,
+  expandedNodes,
+  onToggleNode,
+  onEdit,
+  onDelete,
+  onToggleStatus,
+  onAddChild
+}) => {
+  const isExpanded = expandedNodes.has(node.id);
+  const isDisabled = node.isDisabled === 1;
+  const hasChildren = node.level < 4; // 省级、市级和区县级节点可能有子节点
+
+  return (
+    <div key={node.id} className="select-none">
+      {/* 节点行 */}
+      <div
+        className={cn(
+          "group flex items-center gap-2 py-2 px-3 hover:bg-muted/50 cursor-pointer border-b",
+          depth > 0 && "ml-6"
+        )}
+        style={{ marginLeft: `${depth * 24}px` }}
+      >
+        {/* 展开/收起按钮 */}
+        {hasChildren && (
+          <Button
+            variant="ghost"
+            size="sm"
+            className="h-6 w-6 p-0"
+            onClick={() => onToggleNode(node.id)}
+          >
+            {isExpanded ? (
+              <ChevronDown className="h-4 w-4" />
+            ) : (
+              <ChevronRight className="h-4 w-4" />
+            )}
+          </Button>
+        )}
+        {!hasChildren && <div className="w-6" />}
+
+        {/* 图标 */}
+        <div className="flex-shrink-0">
+          {hasChildren ? (
+            isExpanded ? (
+              <FolderOpen className="h-4 w-4 text-blue-500" />
+            ) : (
+              <Folder className="h-4 w-4 text-blue-400" />
+            )
+          ) : (
+            <div className="h-4 w-4" />
+          )}
+        </div>
+
+        {/* 节点信息 */}
+        <div className="flex-1 flex items-center gap-3">
+          <span className={cn("font-medium", isDisabled && "text-muted-foreground line-through")}>
+            {node.name}
+          </span>
+          <Badge variant="outline" className="text-xs">
+            {getLevelName(node.level)}
+          </Badge>
+          <span className="text-xs text-muted-foreground">
+            {node.code}
+          </span>
+          <Badge variant={isDisabled ? "secondary" : "default"} className="text-xs">
+            {isDisabled ? '禁用' : '启用'}
+          </Badge>
+        </div>
+
+        {/* 操作按钮 */}
+        <div className="flex gap-1 opacity-100 transition-opacity">
+          {/* 新增子节点按钮 - 根据层级显示不同文本 */}
+          {node.level < 4 && (
+            <Button
+              variant="outline"
+              size="sm"
+              onClick={(e) => {
+                e.stopPropagation();
+                onAddChild(node);
+              }}
+            >
+              {node.level === 1 ? '新增市' :
+               node.level === 2 ? '新增区' : '新增乡镇'}
+            </Button>
+          )}
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onEdit(node);
+            }}
+          >
+            编辑
+          </Button>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onToggleStatus(node);
+            }}
+          >
+            {isDisabled ? '启用' : '禁用'}
+          </Button>
+          <Button
+            variant="outline"
+            size="sm"
+            onClick={(e) => {
+              e.stopPropagation();
+              onDelete(node);
+            }}
+          >
+            删除
+          </Button>
+        </div>
+      </div>
+
+      {/* 子节点 */}
+      {isExpanded && hasChildren && (
+        <SubTreeLoader
+          nodeId={node.id}
+          isExpanded={isExpanded}
+          hasChildren={hasChildren}
+          depth={depth}
+          expandedNodes={expandedNodes}
+          onToggleNode={onToggleNode}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          onToggleStatus={onToggleStatus}
+          onAddChild={onAddChild}
+        />
+      )}
+    </div>
+  );
+};
+
+export const AreaTreeAsync: React.FC<AreaTreeAsyncProps> = ({
+  areas,
+  expandedNodes,
+  onToggleNode,
+  onEdit,
+  onDelete,
+  onToggleStatus,
+  onAddChild
+}) => {
+  return (
+    <div className="border rounded-lg bg-background">
+      {areas.map(area => (
+        <TreeNode
+          key={area.id}
+          node={area}
+          depth={0}
+          expandedNodes={expandedNodes}
+          onToggleNode={onToggleNode}
+          onEdit={onEdit}
+          onDelete={onDelete}
+          onToggleStatus={onToggleStatus}
+          onAddChild={onAddChild}
+        />
+      ))}
+    </div>
+  );
+};
+
+// 获取层级显示名称
+const getLevelName = (level: number) => {
+  switch (level) {
+    case 1: return '省/直辖市';
+    case 2: return '市';
+    case 3: return '区/县';
+    case 4: return '街道/乡镇';
+    default: return '未知';
+  }
+};

+ 5 - 0
packages/area-management-ui-mt/src/components/index.ts

@@ -0,0 +1,5 @@
+export { AreaManagement } from './AreaManagement';
+export { AreaForm } from './AreaForm';
+export { AreaTreeAsync } from './AreaTreeAsync';
+export { AreaSelect } from './AreaSelect';
+export { AreaSelect4Level } from './AreaSelect4Level';

+ 8 - 0
packages/area-management-ui-mt/src/hooks/index.ts

@@ -0,0 +1,8 @@
+export {
+  useAreas,
+  useAreaSubtree,
+  useCreateArea,
+  useUpdateArea,
+  useDeleteArea,
+  useToggleAreaStatus
+} from './useAreas';

+ 155 - 0
packages/area-management-ui-mt/src/hooks/useAreas.ts

@@ -0,0 +1,155 @@
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { areaClient } from '../api/areaClient';
+import type { CreateAreaRequest, UpdateAreaRequest, AreaQueryParams } from '../types/area';
+import { toast } from 'sonner';
+
+// 获取区域列表的hook
+export const useAreas = (params?: AreaQueryParams) => {
+  return useQuery({
+    queryKey: ['areas', params],
+    queryFn: async () => {
+      const res = await areaClient.index.$get({
+        query: {
+          page: params?.page || 1,
+          pageSize: params?.pageSize || 100,
+          filters: params?.filters || '',
+          sortBy: params?.sortBy || 'id',
+          sortOrder: params?.sortOrder || 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取区域列表失败');
+      const response = await res.json();
+      return response.data;
+    },
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+  });
+};
+
+// 获取区域子树的hook
+export const useAreaSubtree = (parentId: number) => {
+  return useQuery({
+    queryKey: ['areas-subtree', parentId],
+    queryFn: async () => {
+      const res = await areaClient.index.$get({
+        query: {
+          page: 1,
+          pageSize: 100,
+          filters: JSON.stringify({ parentId }),
+          sortBy: 'id',
+          sortOrder: 'ASC'
+        }
+      });
+      if (res.status !== 200) throw new Error('获取区域子树失败');
+      const response = await res.json();
+      return response.data;
+    },
+    enabled: !!parentId,
+    staleTime: 5 * 60 * 1000,
+    gcTime: 10 * 60 * 1000,
+  });
+};
+
+// 创建区域的hook
+export const useCreateArea = () => {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async (data: CreateAreaRequest) => {
+      const res = await areaClient.index.$post({ json: data });
+      if (res.status !== 201) throw new Error('创建区域失败');
+    },
+    onSuccess: (_, variables) => {
+      // 更新根级缓存
+      queryClient.invalidateQueries({ queryKey: ['areas'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+
+      // 如果创建的是子节点,更新父节点的子树缓存
+      if (variables.parentId) {
+        queryClient.invalidateQueries({ queryKey: ['areas-subtree', variables.parentId] });
+      }
+
+      toast.success('区域创建成功');
+    },
+    onError: () => {
+      toast.error('创建失败,请重试');
+    }
+  });
+};
+
+// 更新区域的hook
+export const useUpdateArea = () => {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async ({ id, data }: { id: number; data: UpdateAreaRequest }) => {
+      const res = await areaClient[':id'].$put({
+        param: { id },
+        json: data
+      });
+      if (res.status !== 200) throw new Error('更新区域失败');
+    },
+    onSuccess: () => {
+      // 更新所有相关缓存
+      queryClient.invalidateQueries({ queryKey: ['areas'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-subtree'] });
+
+      toast.success('区域更新成功');
+    },
+    onError: () => {
+      toast.error('更新失败,请重试');
+    }
+  });
+};
+
+// 删除区域的hook
+export const useDeleteArea = () => {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async (id: number) => {
+      const res = await areaClient[':id'].$delete({
+        param: { id }
+      });
+      if (res.status !== 204) throw new Error('删除区域失败');
+    },
+    onSuccess: () => {
+      // 更新所有相关缓存
+      queryClient.invalidateQueries({ queryKey: ['areas'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-subtree'] });
+
+      toast.success('区域删除成功');
+    },
+    onError: () => {
+      toast.error('删除失败,请重试');
+    }
+  });
+};
+
+// 切换区域状态的hook
+export const useToggleAreaStatus = () => {
+  const queryClient = useQueryClient();
+
+  return useMutation({
+    mutationFn: async ({ id, isDisabled }: { id: number; isDisabled: number }) => {
+      const res = await areaClient[':id'].$put({
+        param: { id },
+        json: { isDisabled }
+      });
+      if (res.status !== 200) throw new Error('更新区域状态失败');
+    },
+    onSuccess: () => {
+      // 更新所有相关缓存
+      queryClient.invalidateQueries({ queryKey: ['areas'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-tree-province'] });
+      queryClient.invalidateQueries({ queryKey: ['areas-subtree'] });
+
+      toast.success('区域状态更新成功');
+    },
+    onError: () => {
+      toast.error('状态更新失败,请重试');
+    }
+  });
+};

+ 18 - 0
packages/area-management-ui-mt/src/index.ts

@@ -0,0 +1,18 @@
+// Export components
+export * from './components';
+
+// Export hooks
+export * from './hooks';
+
+// Export API
+export * from './api';
+
+// Export types
+export type {
+  AreaResponse,
+  CreateAreaRequest,
+  UpdateAreaRequest,
+  AreaNode,
+  AreaFormData,
+  AreaQueryParams
+} from './types/area';

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio