소스 검색

✨ feat(mini-shared-ui-components): 新增Taro共享UI组件包

- 创建完整的Taro共享UI组件包,包含Button、AvatarUpload、Card、Dialog、Form、Image、Input、Label、Navbar、PageContainer、TabBar、UserStatusBar等核心组件
- 添加Jest测试配置,包含完整的测试环境设置、mock文件和工具函数
- 配置TypeScript编译和构建脚本,支持模块化导出
- 实现跨平台工具函数,支持小程序和H5环境检测
- 添加Tailwind CSS工具类合并工具,支持小程序环境下的CSS转义
- 配置package.json依赖,包含Taro相关依赖和测试开发依赖
- 创建测试目录结构,包含setup、mocks、helpers和config文件
- 实现组件索引文件,统一导出所有UI组件和测试工具
yourname 1 개월 전
부모
커밋
cab31688e2
30개의 변경된 파일2235개의 추가작업 그리고 7개의 파일을 삭제
  1. 38 0
      mini-ui-packages/mini-shared-ui-components/jest.config.js
  2. 55 0
      mini-ui-packages/mini-shared-ui-components/package.json
  3. 93 0
      mini-ui-packages/mini-shared-ui-components/src/avatar-upload.tsx
  4. 46 0
      mini-ui-packages/mini-shared-ui-components/src/button.tsx
  5. 54 0
      mini-ui-packages/mini-shared-ui-components/src/card.tsx
  6. 95 0
      mini-ui-packages/mini-shared-ui-components/src/dialog.tsx
  7. 168 0
      mini-ui-packages/mini-shared-ui-components/src/form.tsx
  8. 127 0
      mini-ui-packages/mini-shared-ui-components/src/image.tsx
  9. 13 0
      mini-ui-packages/mini-shared-ui-components/src/index.ts
  10. 102 0
      mini-ui-packages/mini-shared-ui-components/src/input.tsx
  11. 55 0
      mini-ui-packages/mini-shared-ui-components/src/label.tsx
  12. 230 0
      mini-ui-packages/mini-shared-ui-components/src/navbar.tsx
  13. 37 0
      mini-ui-packages/mini-shared-ui-components/src/page-container.tsx
  14. 155 0
      mini-ui-packages/mini-shared-ui-components/src/tab-bar.tsx
  15. 58 0
      mini-ui-packages/mini-shared-ui-components/src/user-status-bar.tsx
  16. 15 0
      mini-ui-packages/mini-shared-ui-components/src/utils/cn.ts
  17. 16 0
      mini-ui-packages/mini-shared-ui-components/src/utils/platform.ts
  18. 8 0
      mini-ui-packages/mini-shared-ui-components/testing/index.ts
  19. 39 0
      mini-ui-packages/mini-shared-ui-components/tests/__config__/jest-preset.js
  20. 9 0
      mini-ui-packages/mini-shared-ui-components/tests/__config__/tsconfig.test.json
  21. 10 0
      mini-ui-packages/mini-shared-ui-components/tests/__helpers__/env-setup.ts
  22. 8 0
      mini-ui-packages/mini-shared-ui-components/tests/__helpers__/taro-mocks.ts
  23. 8 0
      mini-ui-packages/mini-shared-ui-components/tests/__helpers__/test-utils.ts
  24. 1 0
      mini-ui-packages/mini-shared-ui-components/tests/__mocks__/fileMock.js
  25. 1 0
      mini-ui-packages/mini-shared-ui-components/tests/__mocks__/styleMock.js
  26. 100 0
      mini-ui-packages/mini-shared-ui-components/tests/__mocks__/taroMock.ts
  27. 428 0
      mini-ui-packages/mini-shared-ui-components/tests/setup.ts
  28. 24 0
      mini-ui-packages/mini-shared-ui-components/tsconfig.json
  29. 240 6
      pnpm-lock.yaml
  30. 2 1
      pnpm-workspace.yaml

+ 38 - 0
mini-ui-packages/mini-shared-ui-components/jest.config.js

@@ -0,0 +1,38 @@
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'jsdom',
+  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1',
+    '^~/(.*)$': '<rootDir>/tests/$1',
+    '^@tarojs/taro$': '<rootDir>/tests/__mocks__/taroMock.ts',
+    '\\.(css|less|scss|sass)$': '<rootDir>/tests/__mocks__/styleMock.js',
+    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
+      '<rootDir>/tests/__mocks__/fileMock.js'
+  },
+  testMatch: [
+    '<rootDir>/tests/**/*.spec.{ts,tsx}',
+    '<rootDir>/tests/**/*.test.{ts,tsx}'
+  ],
+  collectCoverageFrom: [
+    'src/**/*.{ts,tsx}',
+    '!src/**/*.d.ts',
+    '!src/**/index.{ts,tsx}',
+    '!src/**/*.stories.{ts,tsx}'
+  ],
+  coverageDirectory: 'coverage',
+  coverageReporters: ['text', 'lcov', 'html'],
+  testPathIgnorePatterns: [
+    '/node_modules/',
+    '/dist/',
+    '/coverage/'
+  ],
+  transform: {
+    '^.+\\.(ts|tsx)$': 'ts-jest',
+    '^.+\\.(js|jsx)$': 'babel-jest'
+  },
+  transformIgnorePatterns: [
+    '/node_modules/(?!(swiper|@tarojs)/)'
+  ],
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
+}

+ 55 - 0
mini-ui-packages/mini-shared-ui-components/package.json

@@ -0,0 +1,55 @@
+{
+  "name": "@d8d/mini-shared-ui-components",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "Taro共享UI组件包",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    },
+    "./testing": {
+      "types": "./testing/index.ts",
+      "import": "./testing/index.ts",
+      "require": "./testing/index.ts"
+    }
+  },
+  "scripts": {
+    "build": "tsc",
+    "dev": "tsc --watch",
+    "typecheck": "tsc --noEmit",
+    "test": "jest",
+    "test:watch": "jest --watch",
+    "test:coverage": "jest --coverage",
+    "test:components": "jest tests/components"
+  },
+  "dependencies": {
+    "@tarojs/components": "4.1.4",
+    "@tarojs/react": "4.1.4",
+    "@tarojs/taro": "4.1.4",
+    "@radix-ui/react-slot": "^1.2.3",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "react": "^18.0.0",
+    "react-hook-form": "^7.62.0",
+    "@weapp-tailwindcss/merge": "^1.2.3"
+  },
+  "devDependencies": {
+    "@testing-library/jest-dom": "^6.8.0",
+    "@testing-library/react": "^16.3.0",
+    "@testing-library/user-event": "^14.6.1",
+    "@types/jest": "^29.5.14",
+    "@types/react": "^18.0.0",
+    "jest": "^30.2.0",
+    "jest-environment-jsdom": "^29.7.0",
+    "ts-jest": "^29.4.5",
+    "typescript": "^5.4.5"
+  },
+  "files": [
+    "src",
+    "testing"
+  ]
+}

+ 93 - 0
mini-ui-packages/mini-shared-ui-components/src/avatar-upload.tsx

@@ -0,0 +1,93 @@
+import { useState } from 'react'
+import { View, Image } from '@tarojs/components'
+import Taro from '@tarojs/taro'
+import { cn } from './utils/cn'
+
+interface AvatarUploadProps {
+  currentAvatar?: string
+  onUploadSuccess?: (result: any) => void
+  onUploadError?: (error: Error) => void
+  size?: number
+  editable?: boolean
+}
+
+export function AvatarUpload({
+  currentAvatar,
+  onUploadSuccess,
+  onUploadError,
+  size = 96,
+  editable = true
+}: AvatarUploadProps) {
+  const [uploading, setUploading] = useState(false)
+  const [progress, setProgress] = useState(0)
+
+  const handleChooseImage = async () => {
+    if (!editable || uploading) return
+
+    // 简化版本:只显示提示,不实际处理上传
+    Taro.showToast({
+      title: '头像上传功能需要在项目中实现',
+      icon: 'none'
+    })
+
+    // 调用错误回调,提示需要实现
+    onUploadError?.(new Error('头像上传功能需要在项目中实现,请参考原mini项目中的minio工具'))
+  }
+
+  const avatarSize = size
+
+  return (
+    <View
+      className="relative inline-block"
+      onClick={handleChooseImage}
+    >
+      <View
+        className={cn(
+          "relative overflow-hidden rounded-full",
+          "border-4 border-white shadow-lg",
+          editable && "cursor-pointer active:scale-95 transition-transform duration-150",
+          uploading && "opacity-75"
+        )}
+        style={{ width: avatarSize, height: avatarSize }}
+      >
+        <Image
+          src={currentAvatar || 'https://images.unsplash.com/photo-1494790108755-2616b612b786?w=160&h=160&fit=crop&crop=face'}
+          mode="aspectFill"
+          className="w-full h-full"
+        />
+
+        {uploading && (
+          <View className="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center">
+            <View className="text-white text-xs">{progress}%</View>
+          </View>
+        )}
+      </View>
+
+      {editable && !uploading && (
+        <View
+          className={cn(
+            "absolute -bottom-1 -right-1",
+            "w-8 h-8 bg-blue-500 rounded-full",
+            "flex items-center justify-center shadow-md",
+            "border-2 border-white"
+          )}
+        >
+          <View className="i-heroicons-camera-20-solid w-4 h-4 text-white" />
+        </View>
+      )}
+
+      {uploading && (
+        <View
+          className={cn(
+            "absolute -bottom-1 -right-1",
+            "w-8 h-8 bg-gray-500 rounded-full",
+            "flex items-center justify-center shadow-md",
+            "border-2 border-white"
+          )}
+        >
+          <View className="i-heroicons-arrow-path-20-solid w-4 h-4 text-white animate-spin" />
+        </View>
+      )}
+    </View>
+  )
+}

+ 46 - 0
mini-ui-packages/mini-shared-ui-components/src/button.tsx

@@ -0,0 +1,46 @@
+import { Button as TaroButton, ButtonProps as TaroButtonProps } from '@tarojs/components'
+import { cn } from './utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+const buttonVariants = cva(
+  'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background',
+  {
+    variants: {
+      variant: {
+        default: 'bg-primary text-primary-foreground hover:bg-primary/90',
+        destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
+        outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
+        secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
+        ghost: 'hover:bg-accent hover:text-accent-foreground',
+        link: 'underline-offset-4 hover:underline text-primary',
+      },
+      size: {
+        default: 'h-10 py-2 px-4',
+        sm: 'h-9 px-3 rounded-md text-xs',
+        lg: 'h-11 px-8 rounded-md',
+        icon: 'h-10 w-10',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+interface ButtonProps extends Omit<TaroButtonProps, 'size'>, VariantProps<typeof buttonVariants> {
+  className?: string
+  children?: React.ReactNode
+}
+
+export function Button({ className, variant, size, ...props }: ButtonProps) {
+  return (
+    <TaroButton
+      className={cn(buttonVariants({ variant, size, className }))}
+      {...props}
+    />
+  )
+}
+
+// 预定义的按钮样式导出
+export { buttonVariants }

+ 54 - 0
mini-ui-packages/mini-shared-ui-components/src/card.tsx

@@ -0,0 +1,54 @@
+import { View } from '@tarojs/components'
+import { cn } from './utils/cn'
+
+interface CardProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function Card({ className, children }: CardProps) {
+  return (
+    <View className={cn("bg-white rounded-xl shadow-sm", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface CardHeaderProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardHeader({ className, children }: CardHeaderProps) {
+  return (
+    <View className={cn("p-4 border-b border-gray-100", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface CardContentProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardContent({ className, children }: CardContentProps) {
+  return (
+    <View className={cn("p-4", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface CardFooterProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function CardFooter({ className, children }: CardFooterProps) {
+  return (
+    <View className={cn("p-4 border-t border-gray-100", className)}>
+      {children}
+    </View>
+  )
+}

+ 95 - 0
mini-ui-packages/mini-shared-ui-components/src/dialog.tsx

@@ -0,0 +1,95 @@
+import { useEffect } from 'react'
+import { View, Text } from '@tarojs/components'
+import { cn } from './utils/cn'
+
+interface DialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  children: React.ReactNode
+}
+
+export function Dialog({ open, onOpenChange, children }: DialogProps) {
+  useEffect(() => {
+    if (open) {
+      // 在 Taro 中,我们可以使用模态框或者自定义弹窗
+      // 这里使用自定义实现
+    }
+  }, [open])
+
+  const handleBackdropClick = () => {
+    onOpenChange(false)
+  }
+
+  const handleContentClick = (e: any) => {
+    // 阻止事件冒泡,避免点击内容区域时关闭弹窗
+    e.stopPropagation()
+  }
+
+  if (!open) return null
+
+  return (
+    <View
+      className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
+      onClick={handleBackdropClick}
+    >
+      <View
+        className="relative bg-white rounded-lg shadow-lg max-w-md w-full mx-4"
+        onClick={handleContentClick}
+      >
+        {children}
+      </View>
+    </View>
+  )
+}
+
+interface DialogContentProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogContent({ className, children }: DialogContentProps) {
+  return (
+    <View className={cn("p-6", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface DialogHeaderProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogHeader({ className, children }: DialogHeaderProps) {
+  return (
+    <View className={cn("mb-4", className)}>
+      {children}
+    </View>
+  )
+}
+
+interface DialogTitleProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogTitle({ className, children }: DialogTitleProps) {
+  return (
+    <Text className={cn("text-lg font-semibold text-gray-900", className)}>
+      {children}
+    </Text>
+  )
+}
+
+interface DialogFooterProps {
+  className?: string
+  children: React.ReactNode
+}
+
+export function DialogFooter({ className, children }: DialogFooterProps) {
+  return (
+    <View className={cn("flex justify-end space-x-2", className)}>
+      {children}
+    </View>
+  )
+}

+ 168 - 0
mini-ui-packages/mini-shared-ui-components/src/form.tsx

@@ -0,0 +1,168 @@
+import * as React from "react"
+import { View, Text } from "@tarojs/components"
+import { Slot } from "@radix-ui/react-slot"
+import {
+  Controller,
+  FormProvider,
+  useFormContext,
+  useFormState,
+  type ControllerProps,
+  type FieldPath,
+  type FieldValues,
+} from "react-hook-form"
+
+import { cn } from './utils/cn'
+import { Label } from './label'
+
+const Form = FormProvider
+
+type FormFieldContextValue<
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+> = {
+  name: TName
+}
+
+const FormFieldContext = React.createContext<FormFieldContextValue>(
+  {} as FormFieldContextValue
+)
+
+const FormField = <
+  TFieldValues extends FieldValues = FieldValues,
+  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
+>({
+  ...props
+}: ControllerProps<TFieldValues, TName, TFieldValues>) => {
+  const ControllerWrapper = (props: any) => (
+    // @ts-ignore
+    <Controller {...props} />
+  )
+  return (
+    <FormFieldContext.Provider value={{ name: props.name }}>
+      <ControllerWrapper {...props} />
+    </FormFieldContext.Provider>
+  )
+}
+
+const useFormField = () => {
+  const fieldContext = React.useContext(FormFieldContext)
+  const itemContext = React.useContext(FormItemContext)
+  const { getFieldState } = useFormContext()
+  const formState = useFormState({ name: fieldContext.name })
+  const fieldState = getFieldState(fieldContext.name, formState)
+
+  if (!fieldContext) {
+    throw new Error("useFormField should be used within <FormField>")
+  }
+
+  const { id } = itemContext
+
+  return {
+    id,
+    name: fieldContext.name,
+    formItemId: `${id}-form-item`,
+    formDescriptionId: `${id}-form-item-description`,
+    formMessageId: `${id}-form-item-message`,
+    ...fieldState,
+  }
+}
+
+type FormItemContextValue = {
+  id: string
+}
+
+const FormItemContext = React.createContext<FormItemContextValue>(
+  {} as FormItemContextValue
+)
+
+function FormItem({ className, ...props }: React.ComponentProps<typeof View>) {
+  const id = React.useId()
+
+  return (
+    <FormItemContext.Provider value={{ id }}>
+      <View
+        className={cn("grid gap-2", className)}
+        {...props}
+      />
+    </FormItemContext.Provider>
+  )
+}
+
+function FormLabel({
+  className,
+  ...props
+}: React.ComponentProps<typeof Label>) {
+  const { error, formItemId } = useFormField()
+
+  return (
+    <Label
+      data-slot="form-label"
+      data-error={!!error}
+      className={cn("data-[error=true]:text-destructive", className)}
+      htmlFor={formItemId}
+      {...props}
+    />
+  )
+}
+
+function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
+  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
+
+  return (
+    <Slot
+      data-slot="form-control"
+      id={formItemId}
+      aria-describedby={
+        !error
+          ? `${formDescriptionId}`
+          : `${formDescriptionId} ${formMessageId}`
+      }
+      aria-invalid={!!error}
+      {...props}
+    />
+  )
+}
+
+function FormDescription({ className, ...props }: React.ComponentProps<typeof Text>) {
+  const { formDescriptionId } = useFormField()
+
+  return (
+    <Text
+      data-slot="form-description"
+      id={formDescriptionId}
+      className={cn("text-muted-foreground text-sm", className)}
+      {...props}
+    />
+  )
+}
+
+function FormMessage({ className, ...props }: React.ComponentProps<typeof Text>) {
+  const { error, formMessageId } = useFormField()
+  const body = error ? String(error?.message ?? "") : props.children
+
+  if (!body) {
+    return null
+  }
+
+  return (
+    <Text
+      data-slot="form-message"
+      id={formMessageId}
+      className={cn("text-destructive text-sm", className)}
+      {...props}
+    >
+      {body}
+    </Text>
+  )
+}
+
+export {
+  useFormField,
+  Form,
+  FormItem,
+  FormLabel,
+  FormControl,
+  FormDescription,
+  FormMessage,
+  FormField,
+}

+ 127 - 0
mini-ui-packages/mini-shared-ui-components/src/image.tsx

@@ -0,0 +1,127 @@
+import { View, Image as TaroImage, ImageProps as TaroImageProps } from '@tarojs/components'
+import { cn } from './utils/cn'
+import { useState } from 'react'
+
+export interface ImageProps extends Omit<TaroImageProps, 'onError'> {
+  /**
+   * 图片地址
+   */
+  src: string
+  /**
+   * 替代文本
+   */
+  alt?: string
+  /**
+   * 图片模式
+   * @default "aspectFill"
+   */
+  mode?: TaroImageProps['mode']
+  /**
+   * 是否懒加载
+   * @default true
+   */
+  lazyLoad?: boolean
+  /**
+   * 是否显示加载占位
+   * @default true
+   */
+  showLoading?: boolean
+  /**
+   * 是否显示错误占位
+   * @default true
+   */
+  showError?: boolean
+  /**
+   * 圆角大小
+   */
+  rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
+  /**
+   * 自定义样式类
+   */
+  className?: string
+  /**
+   * 图片加载失败时的回调
+   */
+  onError?: () => void
+  /**
+   * 图片加载成功的回调
+   */
+  onLoad?: () => void
+}
+
+const roundedMap = {
+  none: '',
+  sm: 'rounded-sm',
+  md: 'rounded-md',
+  lg: 'rounded-lg',
+  xl: 'rounded-xl',
+  full: 'rounded-full'
+}
+
+export function Image({
+  src,
+  alt = '图片',
+  mode = 'aspectFill',
+  lazyLoad = true,
+  showLoading = true,
+  showError = true,
+  rounded = 'none',
+  className,
+  onError,
+  onLoad,
+  ...props
+}: ImageProps) {
+  const [loading, setLoading] = useState(true)
+  const [error, setError] = useState(false)
+
+  const handleLoad = () => {
+    setLoading(false)
+    setError(false)
+    onLoad?.()
+  }
+
+  const handleError = () => {
+    setLoading(false)
+    setError(true)
+    onError?.()
+  }
+
+  const renderPlaceholder = () => {
+    if (loading && showLoading) {
+      return (
+        <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
+          <View className="i-heroicons-photo-20-solid w-8 h-8 text-gray-400 animate-pulse" />
+        </View>
+      )
+    }
+
+    if (error && showError) {
+      return (
+        <View className="absolute inset-0 flex items-center justify-center bg-gray-100">
+          <View className="i-heroicons-exclamation-triangle-20-solid w-8 h-8 text-gray-400" />
+        </View>
+      )
+    }
+
+    return null
+  }
+
+  return (
+    <View className={cn('relative overflow-hidden', roundedMap[rounded], className)}>
+      <TaroImage
+        src={src}
+        mode={mode}
+        lazyLoad={lazyLoad}
+        onLoad={handleLoad}
+        onError={handleError}
+        className={cn(
+          'w-full h-full',
+          loading && 'opacity-0',
+          !loading && !error && 'opacity-100 transition-opacity duration-300'
+        )}
+        {...props}
+      />
+      {renderPlaceholder()}
+    </View>
+  )
+}

+ 13 - 0
mini-ui-packages/mini-shared-ui-components/src/index.ts

@@ -0,0 +1,13 @@
+// 导出所有UI组件
+export { Button, buttonVariants } from './button'
+export { AvatarUpload } from './avatar-upload'
+export { Card, CardHeader, CardContent, CardFooter } from './card'
+export { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from './dialog'
+export { Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField, useFormField } from './form'
+export { Image } from './image'
+export { Input, inputVariants } from './input'
+export { Label, labelVariants } from './label'
+export { Navbar, NavbarPresets, createNavbar } from './navbar'
+export { PageContainer } from './page-container'
+export { TabBar } from './tab-bar'
+export { UserStatusBar } from './user-status-bar'

+ 102 - 0
mini-ui-packages/mini-shared-ui-components/src/input.tsx

@@ -0,0 +1,102 @@
+import { Input as TaroInput, InputProps as TaroInputProps, View, Text } from '@tarojs/components'
+import { cn } from './utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+import { forwardRef } from 'react'
+
+const inputVariants = cva(
+  'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
+  {
+    variants: {
+      variant: {
+        default: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
+        outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
+        filled: 'border-none bg-gray-50 hover:bg-gray-100',
+      },
+      size: {
+        default: 'h-10 px-3 py-2',
+        sm: 'h-9 px-2 text-sm',
+        lg: 'h-11 px-4 text-lg',
+        icon: 'h-10 w-10',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+export interface InputProps extends Omit<TaroInputProps, 'className' | 'onChange'>, VariantProps<typeof inputVariants> {
+  className?: string
+  leftIcon?: string
+  rightIcon?: string
+  error?: boolean
+  errorMessage?: string
+  onLeftIconClick?: () => void
+  onRightIconClick?: () => void
+  onChange?: (value: string, event: any) => void
+}
+
+const Input = forwardRef<any, InputProps>(
+  ({ className, variant, size, leftIcon, rightIcon, error, errorMessage, onLeftIconClick, onRightIconClick, onChange, ...props }, ref) => {
+    const handleInput = (event: any) => {
+      const value = event.detail.value
+      onChange?.(value, event)
+
+      // 同时调用原始的onInput(如果提供了)
+      if (props.onInput) {
+        props.onInput(event)
+      }
+    }
+
+    return (
+      <View className="w-full">
+        <View className="relative">
+          {leftIcon && (
+            <View
+              className={cn(
+                "absolute left-3 top-1/2 -translate-y-1/2",
+                onLeftIconClick ? "cursor-pointer" : "pointer-events-none"
+              )}
+              onClick={onLeftIconClick}
+            >
+              <View className={cn('w-5 h-5 text-gray-400', leftIcon)} />
+            </View>
+          )}
+
+          <TaroInput
+            ref={ref}
+            className={cn(
+              inputVariants({ variant, size, className }),
+              error && 'border-red-500 focus:border-red-500 focus:ring-red-500',
+              leftIcon && 'pl-10',
+              rightIcon && 'pr-10',
+            )}
+            onInput={handleInput}
+            {...props}
+          />
+
+          {rightIcon && (
+            <View
+              className={cn(
+                "absolute right-3 top-1/2 -translate-y-1/2",
+                onRightIconClick ? "cursor-pointer" : "pointer-events-none"
+              )}
+              onClick={onRightIconClick}
+            >
+              <View className={cn('w-5 h-5 text-gray-400', rightIcon)} />
+            </View>
+          )}
+        </View>
+
+        {error && errorMessage && (
+          <Text className="mt-1 text-sm text-red-600">{errorMessage}</Text>
+        )}
+      </View>
+    )
+  }
+)
+
+Input.displayName = 'Input'
+
+export { Input, inputVariants }

+ 55 - 0
mini-ui-packages/mini-shared-ui-components/src/label.tsx

@@ -0,0 +1,55 @@
+import { View, Text } from '@tarojs/components'
+import { cn } from './utils/cn'
+import { cva, type VariantProps } from 'class-variance-authority'
+import { forwardRef } from 'react'
+
+const labelVariants = cva(
+  'text-sm font-medium',
+  {
+    variants: {
+      variant: {
+        default: 'text-gray-900',
+        secondary: 'text-gray-600',
+        destructive: 'text-red-600',
+      },
+      size: {
+        default: 'text-sm',
+        sm: 'text-xs',
+        lg: 'text-base',
+      },
+    },
+    defaultVariants: {
+      variant: 'default',
+      size: 'default',
+    },
+  }
+)
+
+export interface LabelProps {
+  className?: string
+  variant?: VariantProps<typeof labelVariants>['variant']
+  size?: VariantProps<typeof labelVariants>['size']
+  children: React.ReactNode
+  required?: boolean
+  htmlFor?: string
+}
+
+const Label = forwardRef<HTMLLabelElement, LabelProps>(
+  ({ className, variant, size, children, required, htmlFor, ...props }, _ref) => {
+    return (
+      <View className="mb-2">
+        <Text
+          className={cn(labelVariants({ variant, size, className }))}
+          {...props}
+        >
+          {children}
+          {required && <Text className="text-red-500 ml-1">*</Text>}
+        </Text>
+      </View>
+    )
+  }
+)
+
+Label.displayName = 'Label'
+
+export { Label, labelVariants }

+ 230 - 0
mini-ui-packages/mini-shared-ui-components/src/navbar.tsx

@@ -0,0 +1,230 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import { cn } from './utils/cn'
+import Taro from '@tarojs/taro'
+import { isWeapp } from './utils/platform'
+
+export interface NavbarProps {
+  title?: string
+  leftText?: string
+  leftIcon?: string
+  rightText?: string
+  rightIcon?: string
+  backgroundColor?: string
+  textColor?: string
+  border?: boolean
+  fixed?: boolean
+  placeholder?: boolean
+  onClickLeft?: () => void
+  onClickRight?: () => void
+  children?: React.ReactNode
+  className?: string
+  /** 是否在小程序环境下隐藏右侧按钮(默认false,会自动避让) */
+  hideRightInWeapp?: boolean
+}
+
+const systemInfo = Taro.getSystemInfoSync()
+const menuButtonInfo = isWeapp() ? Taro.getMenuButtonBoundingClientRect() : undefined
+
+// 计算导航栏高度
+const NAVBAR_HEIGHT = 44
+const STATUS_BAR_HEIGHT = systemInfo.statusBarHeight || 0
+const TOTAL_HEIGHT = STATUS_BAR_HEIGHT + NAVBAR_HEIGHT
+
+export const Navbar: React.FC<NavbarProps> = ({
+  title,
+  leftText,
+  leftIcon = 'i-heroicons-chevron-left-20-solid',
+  rightText,
+  rightIcon,
+  backgroundColor = 'bg-white',
+  textColor = 'text-gray-900',
+  border = true,
+  fixed = true,
+  placeholder = true,
+  onClickLeft,
+  onClickRight,
+  children,
+  className,
+  hideRightInWeapp,
+}) => {
+  // 处理左侧点击
+  const handleLeftClick = () => {
+    if (onClickLeft) {
+      onClickLeft()
+    } else {
+      // 默认返回上一页
+      Taro.navigateBack()
+    }
+  }
+
+  // 渲染左侧内容
+  const renderLeft = () => {
+    if (children) return null
+
+    return (
+      <View
+        className="absolute left-3 top-0 bottom-0 flex items-center z-10"
+        style={{ height: NAVBAR_HEIGHT }}
+        onClick={handleLeftClick}
+      >
+        <View className="flex items-center">
+          {leftIcon && (
+            <View className={cn(leftIcon, 'w-5 h-5', textColor)} />
+          )}
+          {leftText && (
+            <Text className={cn('ml-1 text-sm', textColor)}>{leftText}</Text>
+          )}
+        </View>
+      </View>
+    )
+  }
+
+  // 渲染右侧内容
+  const renderRight = () => {
+    if (!rightText && !rightIcon || (hideRightInWeapp && isWeapp())) return null
+
+    if (isWeapp() && menuButtonInfo) {
+      // 小程序环境下,调整右侧按钮位置
+      return (
+        <View
+          className="absolute top-0 bottom-0 flex items-center z-10"
+          style={{
+            height: NAVBAR_HEIGHT,
+            right: `${systemInfo.screenWidth - menuButtonInfo.left + 10}px`,
+          }}
+          onClick={onClickRight}
+        >
+          <View className="flex items-center">
+            {rightText && (
+              <Text className={cn('mr-1 text-sm', textColor)}>{rightText}</Text>
+            )}
+            {rightIcon && (
+              <View className={cn(rightIcon, 'w-5 h-5', textColor)} />
+            )}
+          </View>
+        </View>
+      )
+    }
+
+    // H5或其他平台,保持原有样式
+    return (
+      <View
+        className="absolute right-3 top-0 bottom-0 flex items-center z-10"
+        style={{ height: NAVBAR_HEIGHT }}
+        onClick={onClickRight}
+      >
+        <View className="flex items-center">
+          {rightText && (
+            <Text className={cn('mr-1 text-sm', textColor)}>{rightText}</Text>
+          )}
+          {rightIcon && (
+            <View className={cn(rightIcon, 'w-5 h-5', textColor)} />
+          )}
+        </View>
+      </View>
+    )
+  }
+
+  // 渲染标题
+  const renderTitle = () => {
+    if (children) return children
+
+    if (isWeapp() && menuButtonInfo) {
+      // 小程序环境下,调整标题位置
+      return (
+        <View className="flex-1 flex items-center justify-center">
+          <Text
+            className={cn('text-base font-semibold truncate', textColor)}
+            style={{
+              maxWidth: `calc(100% - ${systemInfo.screenWidth - menuButtonInfo.right + 10}px - 60px - 60px)`
+            }}
+          >
+            {title}
+          </Text>
+        </View>
+      )
+    }
+
+    // H5或其他平台,保持原有样式
+    return (
+      <Text className={cn('text-base font-semibold', textColor)}>
+        {title}
+      </Text>
+    )
+  }
+
+  // 导航栏样式
+  const navbarStyle = {
+    height: TOTAL_HEIGHT,
+    paddingTop: STATUS_BAR_HEIGHT,
+  }
+
+  return (
+    <>
+      <View
+        className={cn(
+          'relative w-full',
+          backgroundColor,
+          border && 'border-b border-gray-200',
+          fixed && 'fixed top-0 left-0 right-0 z-50',
+          className
+        )}
+        style={navbarStyle}
+      >
+        {/* 导航栏内容 */}
+        <View
+          className="relative flex items-center justify-center"
+          style={{ height: NAVBAR_HEIGHT }}
+        >
+          {renderLeft()}
+          {renderTitle()}
+          {renderRight()}
+        </View>
+      </View>
+
+      {/* 占位元素 */}
+      {fixed && placeholder && (
+        <View style={{ height: TOTAL_HEIGHT }} />
+      )}
+    </>
+  )
+}
+
+// 预设样式
+export const NavbarPresets = {
+  // 默认白色导航栏
+  default: {
+    backgroundColor: 'bg-white',
+    textColor: 'text-gray-900',
+    border: true,
+  },
+
+  // 深色导航栏
+  dark: {
+    backgroundColor: 'bg-gray-900',
+    textColor: 'text-white',
+    border: true,
+  },
+
+  // 透明导航栏
+  transparent: {
+    backgroundColor: 'bg-transparent',
+    textColor: 'text-white',
+    border: false,
+  },
+
+  // 主色调导航栏
+  primary: {
+    backgroundColor: 'bg-blue-500',
+    textColor: 'text-white',
+    border: false,
+  },
+}
+
+// 快捷创建函数
+export const createNavbar = (preset: keyof typeof NavbarPresets) => {
+  return NavbarPresets[preset]
+}
+
+export default Navbar

+ 37 - 0
mini-ui-packages/mini-shared-ui-components/src/page-container.tsx

@@ -0,0 +1,37 @@
+import React, { ReactNode } from 'react'
+import { View } from '@tarojs/components'
+import { cn } from './utils/cn'
+
+export interface PageContainerProps {
+  children: ReactNode
+  className?: string
+  padding?: boolean
+  background?: string
+  safeArea?: boolean
+}
+
+export const PageContainer: React.FC<PageContainerProps> = ({
+  children,
+  className,
+  padding = true,
+  background = 'bg-gray-50',
+  safeArea = true,
+}) => {
+  return (
+    <View className={cn(
+      'min-h-screen w-full',
+      background,
+      safeArea && 'pb-safe',
+      className
+    )}>
+      <View className={cn(
+        padding && 'px-4 py-4',
+        'max-w-screen-md mx-auto'
+      )}>
+        {children}
+      </View>
+    </View>
+  )
+}
+
+export default PageContainer

+ 155 - 0
mini-ui-packages/mini-shared-ui-components/src/tab-bar.tsx

@@ -0,0 +1,155 @@
+import React from 'react'
+import { View, Text } from '@tarojs/components'
+import clsx from 'clsx'
+
+export interface TabBarItem {
+  key: string
+  title: string
+  icon?: string
+  selectedIcon?: string
+  iconClass?: string
+  selectedIconClass?: string
+  badge?: number | string
+  dot?: boolean
+}
+
+export interface TabBarProps {
+  items: TabBarItem[]
+  activeKey?: string
+  onChange?: (key: string) => void
+  className?: string
+  style?: React.CSSProperties
+  fixed?: boolean
+  safeArea?: boolean
+  color?: string
+  selectedColor?: string
+  backgroundColor?: string
+}
+
+const TabBar = React.forwardRef<HTMLDivElement, TabBarProps>(({
+  items,
+  activeKey,
+  onChange,
+  className,
+  style,
+  fixed = true,
+  safeArea = true,
+  color = '#7f7f7f',
+  selectedColor = '#1890ff',
+  backgroundColor = '#ffffff',
+}, ref) => {
+
+  const currentActiveKey = activeKey || items[0]?.key
+
+  const handleTabChange = (key: string) => {
+    if (key !== currentActiveKey) {
+      onChange?.(key)
+    }
+  }
+
+  return (
+    <View
+      ref={ref}
+      className={clsx(
+        'tab-bar',
+        fixed && 'fixed bottom-0 left-0 right-0',
+        safeArea && 'pb-safe',
+        'z-50',
+        className
+      )}
+      style={{
+        backgroundColor,
+        ...style,
+      }}
+    >
+      <View className="flex h-16 border-t border-gray-200">
+        {items.map((item) => {
+          const isActive = item.key === currentActiveKey
+
+          return (
+            <View
+              key={item.key}
+              className={clsx(
+                'flex-1 flex flex-col items-center justify-center',
+                'px-2 py-1',
+                'cursor-pointer',
+                'transition-colors duration-200',
+                'hover:opacity-80'
+              )}
+              onClick={() => handleTabChange(item.key)}
+            >
+              <View className="relative">
+                {(item.iconClass || item.icon) && (
+                  <View
+                    className={clsx(
+                      'mb-1',
+                      'flex items-center justify-center',
+                      item.iconClass ? 'w-6 h-6' : 'text-2xl',
+                      isActive ? 'text-blue-500' : 'text-gray-500'
+                    )}
+                    style={{
+                      color: isActive ? selectedColor : color,
+                    }}
+                  >
+                    {item.iconClass ? (
+                      <View
+                        className={clsx(
+                          isActive && item.selectedIconClass
+                            ? item.selectedIconClass
+                            : item.iconClass,
+                          'w-full h-full'
+                        )}
+                      />
+                    ) : (
+                      isActive && item.selectedIcon ? item.selectedIcon : item.icon
+                    )}
+                  </View>
+                )}
+
+                {item.badge && (
+                  <View
+                    className={clsx(
+                      'absolute -top-1 -right-2',
+                      'bg-red-500 text-white text-xs',
+                      'rounded-full px-1.5 py-0.5',
+                      'min-w-4 h-4 flex items-center justify-center'
+                    )}
+                  >
+                    {typeof item.badge === 'number' && item.badge > 99 ? '99+' : item.badge}
+                  </View>
+                )}
+
+                {item.dot && (
+                  <View
+                    className={clsx(
+                      'absolute -top-1 -right-1',
+                      'w-2 h-2 bg-red-500 rounded-full'
+                    )}
+                  />
+                )}
+              </View>
+
+              <Text
+                className={clsx(
+                  'text-xs',
+                  'leading-tight',
+                  isActive ? 'font-medium' : 'font-normal'
+                )}
+                style={{
+                  color: isActive ? selectedColor : color,
+                }}
+                numberOfLines={1}
+              >
+                {item.title}
+              </Text>
+            </View>
+          )
+        })}
+      </View>
+    </View>
+  )
+})
+
+TabBar.displayName = 'TabBar'
+
+export { TabBar }

+ 58 - 0
mini-ui-packages/mini-shared-ui-components/src/user-status-bar.tsx

@@ -0,0 +1,58 @@
+import React from 'react'
+import { View, Text, Image } from '@tarojs/components'
+import { cn } from './utils/cn'
+
+export interface UserStatusBarProps {
+  userName?: string
+  avatarUrl?: string
+  companyName?: string
+  notificationCount?: number
+  className?: string
+}
+
+export const UserStatusBar: React.FC<UserStatusBarProps> = ({
+  userName = '企业用户',
+  avatarUrl,
+  companyName = '企业名称',
+  notificationCount = 0,
+  className,
+}) => {
+  return (
+    <View className={cn(
+      'flex items-center justify-between px-4 py-3 bg-white border-b border-gray-200',
+      className
+    )}>
+      <View className="flex items-center">
+        {avatarUrl ? (
+          <Image
+            src={avatarUrl}
+            className="w-10 h-10 rounded-full mr-3"
+            mode="aspectFill"
+          />
+        ) : (
+          <View className="w-10 h-10 rounded-full bg-blue-500 flex items-center justify-center mr-3">
+            <Text className="text-white font-bold text-lg">
+              {userName.charAt(0).toUpperCase()}
+            </Text>
+          </View>
+        )}
+        <View>
+          <Text className="font-semibold text-gray-900">{userName}</Text>
+          <Text className="text-sm text-gray-600">{companyName}</Text>
+        </View>
+      </View>
+      <View className="relative">
+        <View className="i-heroicons-bell-20-solid w-6 h-6 text-gray-600" />
+        {notificationCount > 0 && (
+          <View className="absolute -top-1 -right-1 w-4 h-4 bg-red-500 rounded-full flex items-center justify-center">
+            <Text className="text-white text-xs font-bold">
+              {notificationCount > 99 ? '99+' : notificationCount}
+            </Text>
+          </View>
+        )}
+      </View>
+    </View>
+  )
+}
+
+export default UserStatusBar

+ 15 - 0
mini-ui-packages/mini-shared-ui-components/src/utils/cn.ts

@@ -0,0 +1,15 @@
+import { type ClassValue } from 'clsx'
+import { create } from '@weapp-tailwindcss/merge';
+import Taro from '@tarojs/taro';
+
+// 根据当前环境判断是否需要转义
+const isWeapp = Taro.getEnv() === Taro.ENV_TYPE.WEAPP;
+
+const { twMerge } = create({
+  // 仅在小程序环境下启用转义,H5环境禁用
+  disableEscape: !isWeapp
+});
+
+export function cn(...inputs: ClassValue[]) {
+  return twMerge(inputs)
+}

+ 16 - 0
mini-ui-packages/mini-shared-ui-components/src/utils/platform.ts

@@ -0,0 +1,16 @@
+import Taro from '@tarojs/taro'
+
+// 获取当前平台
+export const getPlatform = () => {
+  return Taro.getEnv()
+}
+
+// 是否为小程序
+export const isWeapp = (): boolean => {
+  return getPlatform() === Taro.ENV_TYPE.WEAPP
+}
+
+// 是否为H5
+export const isH5 = (): boolean => {
+  return getPlatform() === Taro.ENV_TYPE.WEB
+}

+ 8 - 0
mini-ui-packages/mini-shared-ui-components/testing/index.ts

@@ -0,0 +1,8 @@
+// 测试工具导出
+export * from '../tests/__helpers__/taro-mocks'
+export * from '../tests/__helpers__/test-utils'
+export * from '../tests/__helpers__/env-setup'
+
+// 配置导出
+export { default as jestPreset } from '../tests/__config__/jest-preset'
+export { default as tsconfigTest } from '../tests/__config__/tsconfig.test.json'

+ 39 - 0
mini-ui-packages/mini-shared-ui-components/tests/__config__/jest-preset.js

@@ -0,0 +1,39 @@
+// Jest预设配置,供其他mini UI包复用
+module.exports = {
+  preset: 'ts-jest',
+  testEnvironment: 'jsdom',
+  setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
+  moduleNameMapper: {
+    '^@/(.*)$': '<rootDir>/src/$1',
+    '^~/(.*)$': '<rootDir>/tests/$1',
+    '^@tarojs/taro$': '<rootDir>/tests/__mocks__/taroMock.ts',
+    '\\.(css|less|scss|sass)$': '<rootDir>/tests/__mocks__/styleMock.js',
+    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
+      '<rootDir>/tests/__mocks__/fileMock.js'
+  },
+  testMatch: [
+    '<rootDir>/tests/**/*.spec.{ts,tsx}',
+    '<rootDir>/tests/**/*.test.{ts,tsx}'
+  ],
+  collectCoverageFrom: [
+    'src/**/*.{ts,tsx}',
+    '!src/**/*.d.ts',
+    '!src/**/index.{ts,tsx}',
+    '!src/**/*.stories.{ts,tsx}'
+  ],
+  coverageDirectory: 'coverage',
+  coverageReporters: ['text', 'lcov', 'html'],
+  testPathIgnorePatterns: [
+    '/node_modules/',
+    '/dist/',
+    '/coverage/'
+  ],
+  transform: {
+    '^.+\\.(ts|tsx)$': 'ts-jest',
+    '^.+\\.(js|jsx)$': 'babel-jest'
+  },
+  transformIgnorePatterns: [
+    '/node_modules/(?!(swiper|@tarojs)/)'
+  ],
+  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json']
+}

+ 9 - 0
mini-ui-packages/mini-shared-ui-components/tests/__config__/tsconfig.test.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "types": ["jest", "@testing-library/jest-dom", "node"]
+  },
+  "include": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx", "setup.ts"],
+  "exclude": ["node_modules", "dist"]
+}

+ 10 - 0
mini-ui-packages/mini-shared-ui-components/tests/__helpers__/env-setup.ts

@@ -0,0 +1,10 @@
+// 测试环境设置
+export const setupTestEnv = () => {
+  // 设置环境变量
+  process.env.TARO_ENV = 'h5'
+  process.env.TARO_PLATFORM = 'web'
+  process.env.SUPPORT_TARO_POLYFILL = 'disabled'
+
+  // 定义 defineAppConfig 全局函数用于测试 Taro 配置文件
+  ;(global as any).defineAppConfig = (config: any) => config
+}

+ 8 - 0
mini-ui-packages/mini-shared-ui-components/tests/__helpers__/taro-mocks.ts

@@ -0,0 +1,8 @@
+// Taro组件mock配置
+// 从setup.ts中提取的mock逻辑可以放在这里
+// 目前直接使用setup.ts中的完整mock
+
+export const setupTaroMocks = () => {
+  // 如果需要额外的mock设置,可以在这里添加
+  // 目前setup.ts已经处理了所有mock
+}

+ 8 - 0
mini-ui-packages/mini-shared-ui-components/tests/__helpers__/test-utils.ts

@@ -0,0 +1,8 @@
+// 测试工具函数
+import { render } from '@testing-library/react'
+
+export const renderTaroComponent = (component: React.ReactElement, options?: any) => {
+  return render(component, options)
+}
+
+// 其他通用的测试工具函数可以在这里添加

+ 1 - 0
mini-ui-packages/mini-shared-ui-components/tests/__mocks__/fileMock.js

@@ -0,0 +1 @@
+module.exports = 'test-file-stub'

+ 1 - 0
mini-ui-packages/mini-shared-ui-components/tests/__mocks__/styleMock.js

@@ -0,0 +1 @@
+module.exports = {}

+ 100 - 0
mini-ui-packages/mini-shared-ui-components/tests/__mocks__/taroMock.ts

@@ -0,0 +1,100 @@
+/**
+ * Taro API Mock 文件
+ * 通过 jest.config.js 的 moduleNameMapper 重定向 @tarojs/taro 到这里
+ */
+
+// 创建所有 Taro API 的 mock 函数
+export const mockShowToast = jest.fn()
+export const mockShowLoading = jest.fn()
+export const mockHideLoading = jest.fn()
+export const mockNavigateTo = jest.fn()
+export const mockNavigateBack = jest.fn()
+export const mockSwitchTab = jest.fn()
+export const mockShowModal = jest.fn()
+export const mockReLaunch = jest.fn()
+export const mockOpenCustomerServiceChat = jest.fn()
+export const mockUseRouter = jest.fn()
+export const mockRequestPayment = jest.fn()
+export const mockGetEnv = jest.fn()
+export const mockUseLoad = jest.fn()
+export const mockUseShareAppMessage = jest.fn()
+export const mockUseShareTimeline = jest.fn()
+export const mockGetCurrentInstance = jest.fn()
+
+// 环境类型常量
+export const ENV_TYPE = {
+  WEAPP: 'WEAPP',
+  WEB: 'WEB',
+  RN: 'RN',
+  SWAN: 'SWAN',
+  ALIPAY: 'ALIPAY',
+  TT: 'TT',
+  QQ: 'QQ',
+  JD: 'JD',
+  HARMONY: 'HARMONY'
+}
+
+// 导出所有 mock 函数,便于在测试中访问
+export default {
+  // UI 相关
+  showToast: mockShowToast,
+  showLoading: mockShowLoading,
+  hideLoading: mockHideLoading,
+  showModal: mockShowModal,
+
+  // 导航相关
+  navigateTo: mockNavigateTo,
+  navigateBack: mockNavigateBack,
+  switchTab: mockSwitchTab,
+  reLaunch: mockReLaunch,
+  useRouter: () => mockUseRouter(),
+  useLoad: (callback: any) => mockUseLoad(callback),
+
+  // 微信相关
+  openCustomerServiceChat: mockOpenCustomerServiceChat,
+  requestPayment: mockRequestPayment,
+
+  // 系统信息
+  getSystemInfoSync: () => ({
+    statusBarHeight: 20
+  }),
+  getMenuButtonBoundingClientRect: () => ({
+    width: 87,
+    height: 32,
+    top: 48,
+    right: 314,
+    bottom: 80,
+    left: 227
+  }),
+  getEnv: mockGetEnv,
+
+  // 分享相关
+  useShareAppMessage: mockUseShareAppMessage,
+  useShareTimeline: mockUseShareTimeline,
+
+  // 实例相关
+  getCurrentInstance: mockGetCurrentInstance,
+
+  // 环境类型常量
+  ENV_TYPE
+}
+
+// 为命名导入导出所有函数
+export {
+  mockShowToast as showToast,
+  mockShowLoading as showLoading,
+  mockHideLoading as hideLoading,
+  mockShowModal as showModal,
+  mockNavigateTo as navigateTo,
+  mockNavigateBack as navigateBack,
+  mockSwitchTab as switchTab,
+  mockReLaunch as reLaunch,
+  mockUseRouter as useRouter,
+  mockUseLoad as useLoad,
+  mockOpenCustomerServiceChat as openCustomerServiceChat,
+  mockRequestPayment as requestPayment,
+  mockGetEnv as getEnv,
+  mockUseShareAppMessage as useShareAppMessage,
+  mockUseShareTimeline as useShareTimeline,
+  mockGetCurrentInstance as getCurrentInstance
+}

+ 428 - 0
mini-ui-packages/mini-shared-ui-components/tests/setup.ts

@@ -0,0 +1,428 @@
+import '@testing-library/jest-dom'
+
+// 扩展全局类型以支持 Taro 配置测试
+declare var defineAppConfig: (config: any) => any
+
+/* eslint-disable react/display-name */
+
+// 设置环境变量
+process.env.TARO_ENV = 'h5'
+process.env.TARO_PLATFORM = 'web'
+process.env.SUPPORT_TARO_POLYFILL = 'disabled'
+
+// 定义 defineAppConfig 全局函数用于测试 Taro 配置文件
+;(global as any).defineAppConfig = (config: any) => config
+
+// Mock Taro 组件
+// eslint-disable-next-line react/display-name
+jest.mock('@tarojs/components', () => {
+  const React = require('react')
+  const MockView = React.forwardRef((props: any, ref: any) => {
+    const { children, ...restProps } = props
+    return React.createElement('div', { ...restProps, ref }, children)
+  })
+  MockView.displayName = 'MockView'
+
+  const MockScrollView = React.forwardRef((props: any, ref: any) => {
+    const {
+      children,
+      onScroll,
+      onTouchStart,
+      onScrollEnd,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollY,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      showScrollbar,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollTop,
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      scrollWithAnimation,
+      ...restProps
+    } = props
+    return React.createElement('div', {
+      ...restProps,
+      ref,
+      onScroll: (e: any) => {
+        if (onScroll) onScroll(e)
+      },
+      onTouchStart: (e: any) => {
+        if (onTouchStart) onTouchStart(e)
+      },
+      onTouchEnd: () => {
+        if (onScrollEnd) onScrollEnd()
+      },
+      style: {
+        overflow: 'auto',
+        height: '200px',
+        ...restProps.style
+      }
+    }, children)
+  })
+  MockScrollView.displayName = 'MockScrollView'
+
+  return {
+    View: MockView,
+    ScrollView: MockScrollView,
+    Text: (() => {
+      const MockText = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('span', { ...restProps, ref }, children)
+      })
+      MockText.displayName = 'MockText'
+      return MockText
+    })(),
+    Button: (() => {
+      const MockButton = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('button', { ...restProps, ref }, children)
+      })
+      MockButton.displayName = 'MockButton'
+      return MockButton
+    })(),
+    Input: (() => {
+      const MockInput = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { ...restProps, ref })
+      })
+      MockInput.displayName = 'MockInput'
+      return MockInput
+    })(),
+    Textarea: (() => {
+      const MockTextarea = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('textarea', { ...restProps, ref }, children)
+      })
+      MockTextarea.displayName = 'MockTextarea'
+      return MockTextarea
+    })(),
+    Image: (() => {
+      const MockImage = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('img', { ...restProps, ref })
+      })
+      MockImage.displayName = 'MockImage'
+      return MockImage
+    })(),
+    Form: (() => {
+      const MockForm = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('form', { ...restProps, ref }, children)
+      })
+      MockForm.displayName = 'MockForm'
+      return MockForm
+    })(),
+    Label: (() => {
+      const MockLabel = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('label', { ...restProps, ref }, children)
+      })
+      MockLabel.displayName = 'MockLabel'
+      return MockLabel
+    })(),
+    Picker: (() => {
+      const MockPicker = React.forwardRef((props: any, ref: any) => {
+        const { children, ...restProps } = props
+        return React.createElement('div', { ...restProps, ref }, children)
+      })
+      MockPicker.displayName = 'MockPicker'
+      return MockPicker
+    })(),
+    Switch: (() => {
+      const MockSwitch = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { type: 'checkbox', ...restProps, ref })
+      })
+      MockSwitch.displayName = 'MockSwitch'
+      return MockSwitch
+    })(),
+    Slider: (() => {
+      const MockSlider = React.forwardRef((props: any, ref: any) => {
+        const { ...restProps } = props
+        return React.createElement('input', { type: 'range', ...restProps, ref })
+      })
+      MockSlider.displayName = 'MockSlider'
+      return MockSlider
+    })(),
+    Radio: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('input', { type: 'radio', ...restProps, ref }, children)
+    }),
+    RadioGroup: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Checkbox: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('input', { type: 'checkbox', ...restProps, ref }, children)
+    }),
+    CheckboxGroup: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Progress: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('progress', { ...restProps, ref })
+    }),
+    RichText: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MovableArea: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MovableView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Swiper: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    SwiperItem: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Navigator: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('a', { ...restProps, ref }, children)
+    }),
+    Audio: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('audio', { ...restProps, ref })
+    }),
+    Video: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('video', { ...restProps, ref }, children)
+    }),
+    Camera: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    LivePlayer: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    LivePusher: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Map: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Canvas: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('canvas', { ...restProps, ref }, children)
+    }),
+    OpenData: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    WebView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('iframe', { ...restProps, ref }, children)
+    }),
+    Ad: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    OfficialAccount: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CoverView: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CoverImage: React.forwardRef((props: any, ref: any) => {
+      const { ...restProps } = props
+      return React.createElement('img', { ...restProps, ref })
+    }),
+    FunctionalPageNavigator: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    AdContent: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    MatchMedia: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    PageContainer: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    ShareElement: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    KeyboardAccessory: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    RootPortal: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    PageMeta: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    NavigationBar: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Block: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Import: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Include: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Template: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Slot: React.forwardRef((props:any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    NativeSlot: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    CustomWrapper: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    Editor: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    VoipRoom: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    }),
+    AdCustom: React.forwardRef((props: any, ref: any) => {
+      const { children, ...restProps } = props
+      return React.createElement('div', { ...restProps, ref }, children)
+    })
+  }
+})
+
+// 模拟 MutationObserver
+// @ts-ignore
+global.MutationObserver = class {
+  disconnect() {}
+  observe(_element: any, _initObject: any) {}
+  takeRecords() { return [] }
+}
+
+// 模拟 IntersectionObserver
+// @ts-ignore
+global.IntersectionObserver = class {
+  constructor(fn: (args: any[]) => void) {
+    setTimeout(() => {
+      fn([{ isIntersecting: true }])
+    }, 1000)
+  }
+
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+  takeRecords() { return [] }
+  root: null = null
+  rootMargin: string = ''
+  thresholds: number[] = []
+}
+
+// 模拟 ResizeObserver
+// @ts-ignore
+global.ResizeObserver = class {
+  observe() {}
+  unobserve() {}
+  disconnect() {}
+}
+
+// 模拟 matchMedia
+Object.defineProperty(window, 'matchMedia', {
+  writable: true,
+  value: jest.fn().mockImplementation(query => ({
+    matches: false,
+    media: query,
+    onchange: null,
+    addListener: jest.fn(), // deprecated
+    removeListener: jest.fn(), // deprecated
+    addEventListener: jest.fn(),
+    removeEventListener: jest.fn(),
+    dispatchEvent: jest.fn(),
+  })),
+})
+
+// 模拟 getComputedStyle
+Object.defineProperty(window, 'getComputedStyle', {
+  value: () => ({
+    getPropertyValue: (prop: string) => {
+      return {
+        'font-size': '16px',
+        'font-family': 'Arial',
+        color: 'rgb(0, 0, 0)',
+        'background-color': 'rgb(255, 255, 255)',
+        width: '100px',
+        height: '100px',
+        top: '0px',
+        left: '0px',
+        right: '0px',
+        bottom: '0px',
+        x: '0px',
+        y: '0px'
+      }[prop] || ''
+    }
+  })
+})
+
+// 模拟 Element.prototype.getBoundingClientRect
+Element.prototype.getBoundingClientRect = jest.fn(() => ({
+  width: 100,
+  height: 100,
+  top: 0,
+  left: 0,
+  bottom: 100,
+  right: 100,
+  x: 0,
+  y: 0,
+  toJSON: () => ({
+    width: 100,
+    height: 100,
+    top: 0,
+    left: 0,
+    bottom: 100,
+    right: 100,
+    x: 0,
+    y: 0
+  })
+}))
+
+// 静默 console.error 在测试中
+const originalConsoleError = console.error
+console.error = (...args: any[]) => {
+  // 检查是否在测试环境中(通过 Jest 环境变量判断)
+  const isTestEnv = process.env.JEST_WORKER_ID !== undefined ||
+                    typeof jest !== 'undefined'
+
+  // 在测试环境中静默错误输出,除非是重要错误
+  if (isTestEnv && !args[0]?.includes?.('重要错误')) {
+    return
+  }
+  originalConsoleError(...args)
+}
+
+export {}

+ 24 - 0
mini-ui-packages/mini-shared-ui-components/tsconfig.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "module": "ESNext",
+    "lib": ["ES2020", "DOM"],
+    "moduleResolution": "node",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "jsx": "react-jsx",
+    "resolveJsonModule": true,
+    "allowSyntheticDefaultImports": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist", "tests"]
+}

+ 240 - 6
pnpm-lock.yaml

@@ -1293,6 +1293,64 @@ importers:
         specifier: ^0.0.10
         version: 0.0.10(webpack@5.91.0(@swc/core@1.3.96))
 
+  mini-ui-packages/mini-shared-ui-components:
+    dependencies:
+      '@radix-ui/react-slot':
+        specifier: ^1.2.3
+        version: 1.2.3(@types/react@18.3.26)(react@18.3.1)
+      '@tarojs/components':
+        specifier: 4.1.4
+        version: 4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0)
+      '@tarojs/react':
+        specifier: 4.1.4
+        version: 4.1.4(react@18.3.1)
+      '@tarojs/taro':
+        specifier: 4.1.4
+        version: 4.1.4(@tarojs/components@4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0))(@tarojs/helper@4.1.4)(@tarojs/shared@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0)
+      '@weapp-tailwindcss/merge':
+        specifier: ^1.2.3
+        version: 1.3.0(tailwindcss@4.1.15)
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      react:
+        specifier: ^18.0.0
+        version: 18.3.1
+      react-hook-form:
+        specifier: ^7.62.0
+        version: 7.65.0(react@18.3.1)
+    devDependencies:
+      '@testing-library/jest-dom':
+        specifier: ^6.8.0
+        version: 6.9.1
+      '@testing-library/react':
+        specifier: ^16.3.0
+        version: 16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@19.2.0(react@18.3.1))(react@18.3.1)
+      '@testing-library/user-event':
+        specifier: ^14.6.1
+        version: 14.6.1(@testing-library/dom@10.4.1)
+      '@types/jest':
+        specifier: ^29.5.14
+        version: 29.5.14
+      '@types/react':
+        specifier: ^18.0.0
+        version: 18.3.26
+      jest:
+        specifier: ^30.2.0
+        version: 30.2.0(@types/node@18.19.130)
+      jest-environment-jsdom:
+        specifier: ^29.7.0
+        version: 29.7.0
+      ts-jest:
+        specifier: ^29.4.5
+        version: 29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0)(typescript@5.9.3)
+      typescript:
+        specifier: ^5.4.5
+        version: 5.9.3
+
   packages/advertisement-management-ui:
     dependencies:
       '@d8d/advertisement-type-management-ui':
@@ -11156,9 +11214,6 @@ packages:
     resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==}
     engines: {node: '>=18'}
 
-  csstype@3.1.3:
-    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
-
   csstype@3.2.3:
     resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
 
@@ -20642,6 +20697,30 @@ snapshots:
       - webpack-chain
       - webpack-dev-server
 
+  '@tarojs/components@4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0)':
+    dependencies:
+      '@stencil/core': 2.22.3
+      '@tarojs/runtime': 4.1.4
+      '@tarojs/shared': 4.1.4
+      '@tarojs/taro': 4.1.4(@tarojs/components@4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0))(@tarojs/helper@4.1.4)(@tarojs/shared@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0)
+      classnames: 2.5.1
+      hammerjs: 2.0.8
+      hls.js: 1.6.13
+      resolve-pathname: 3.0.0
+      swiper: 11.1.15
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 18.3.26
+      vue: 3.5.22(typescript@5.9.3)
+    transitivePeerDependencies:
+      - '@tarojs/helper'
+      - html-webpack-plugin
+      - postcss
+      - rollup
+      - webpack
+      - webpack-chain
+      - webpack-dev-server
+
   '@tarojs/helper@4.1.4':
     dependencies:
       '@babel/core': 7.28.4
@@ -20942,6 +21021,24 @@ snapshots:
       webpack-chain: 6.5.1
       webpack-dev-server: 4.15.2(webpack@5.91.0(@swc/core@1.3.96))
 
+  '@tarojs/taro@4.1.4(@tarojs/components@4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0))(@tarojs/helper@4.1.4)(@tarojs/shared@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0)':
+    dependencies:
+      '@tarojs/api': 4.1.4(@tarojs/runtime@4.1.4)(@tarojs/shared@4.1.4)
+      '@tarojs/components': 4.1.4(@tarojs/helper@4.1.4)(@types/react@18.3.26)(html-webpack-plugin@5.6.4(webpack@5.91.0))(postcss@8.5.6)(rollup@3.29.5)(vue@3.5.22(typescript@5.9.3))(webpack-chain@6.5.1)(webpack-dev-server@4.15.2(webpack@5.91.0))(webpack@5.91.0)
+      '@tarojs/helper': 4.1.4
+      '@tarojs/runtime': 4.1.4
+      '@tarojs/shared': 4.1.4
+      '@types/postcss-url': 10.0.4
+      postcss: 8.5.6
+    optionalDependencies:
+      '@types/react': 18.3.26
+      html-webpack-plugin: 5.6.4(webpack@5.91.0)
+      rollup: 3.29.5
+      vue: 3.5.22(typescript@5.9.3)
+      webpack: 5.91.0
+      webpack-chain: 6.5.1
+      webpack-dev-server: 4.15.2(webpack@5.91.0)
+
   '@tarojs/webpack5-prebundle@4.1.4(webpack@5.91.0(@swc/core@1.3.96))':
     dependencies:
       '@tarojs/helper': 4.1.4
@@ -21070,6 +21167,16 @@ snapshots:
       '@types/react': 18.3.26
       '@types/react-dom': 19.2.3(@types/react@18.3.26)
 
+  '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@18.3.26))(@types/react@18.3.26)(react-dom@19.2.0(react@18.3.1))(react@18.3.1)':
+    dependencies:
+      '@babel/runtime': 7.28.4
+      '@testing-library/dom': 10.4.1
+      react: 18.3.1
+      react-dom: 19.2.0(react@18.3.1)
+    optionalDependencies:
+      '@types/react': 18.3.26
+      '@types/react-dom': 19.2.3(@types/react@18.3.26)
+
   '@testing-library/react@16.3.0(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
     dependencies:
       '@babel/runtime': 7.28.4
@@ -21338,7 +21445,7 @@ snapshots:
   '@types/react@18.3.26':
     dependencies:
       '@types/prop-types': 15.7.15
-      csstype: 3.1.3
+      csstype: 3.2.3
 
   '@types/react@19.2.2':
     dependencies:
@@ -23232,8 +23339,6 @@ snapshots:
       '@asamuzakjp/css-color': 3.2.0
       rrweb-cssom: 0.8.0
 
-  csstype@3.1.3: {}
-
   csstype@3.2.3: {}
 
   cuint@0.2.2: {}
@@ -24794,6 +24899,17 @@ snapshots:
     optionalDependencies:
       webpack: 5.91.0(@swc/core@1.3.96)
 
+  html-webpack-plugin@5.6.4(webpack@5.91.0):
+    dependencies:
+      '@types/html-minifier-terser': 6.1.0
+      html-minifier-terser: 6.1.0
+      lodash: 4.17.21
+      pretty-error: 4.0.0
+      tapable: 2.3.0
+    optionalDependencies:
+      webpack: 5.91.0
+    optional: true
+
   htmlparser2@10.0.0:
     dependencies:
       domelementtype: 2.3.0
@@ -27586,6 +27702,11 @@ snapshots:
       react: 18.3.1
       scheduler: 0.23.2
 
+  react-dom@19.2.0(react@18.3.1):
+    dependencies:
+      react: 18.3.1
+      scheduler: 0.27.0
+
   react-dom@19.2.0(react@19.2.0):
     dependencies:
       react: 19.2.0
@@ -28679,6 +28800,16 @@ snapshots:
       '@swc/core': 1.3.96
       esbuild: 0.21.5
 
+  terser-webpack-plugin@5.3.14(webpack@5.91.0):
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.31
+      jest-worker: 27.5.1
+      schema-utils: 4.3.3
+      serialize-javascript: 6.0.2
+      terser: 5.44.0
+      webpack: 5.91.0
+    optional: true
+
   terser@5.44.0:
     dependencies:
       '@jridgewell/source-map': 0.3.11
@@ -28828,6 +28959,26 @@ snapshots:
       babel-jest: 30.2.0(@babel/core@7.28.4)
       jest-util: 30.2.0
 
+  ts-jest@29.4.5(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(jest-util@30.2.0)(jest@30.2.0)(typescript@5.9.3):
+    dependencies:
+      bs-logger: 0.2.6
+      fast-json-stable-stringify: 2.1.0
+      handlebars: 4.7.8
+      jest: 30.2.0(@types/node@18.19.130)
+      json5: 2.2.3
+      lodash.memoize: 4.1.2
+      make-error: 1.3.6
+      semver: 7.7.3
+      type-fest: 4.41.0
+      typescript: 5.9.3
+      yargs-parser: 21.1.1
+    optionalDependencies:
+      '@babel/core': 7.28.4
+      '@jest/transform': 30.2.0
+      '@jest/types': 30.2.0
+      babel-jest: 30.2.0(@babel/core@7.28.4)
+      jest-util: 30.2.0
+
   ts-morph@26.0.0:
     dependencies:
       '@ts-morph/common': 0.27.0
@@ -29555,6 +29706,16 @@ snapshots:
       schema-utils: 4.3.3
       webpack: 5.91.0(@swc/core@1.3.96)
 
+  webpack-dev-middleware@5.3.4(webpack@5.91.0):
+    dependencies:
+      colorette: 2.0.20
+      memfs: 3.5.3
+      mime-types: 2.1.35
+      range-parser: 1.2.1
+      schema-utils: 4.3.3
+      webpack: 5.91.0
+    optional: true
+
   webpack-dev-server@4.15.2(webpack@5.91.0(@swc/core@1.3.96)):
     dependencies:
       '@types/bonjour': 3.5.13
@@ -29595,6 +29756,47 @@ snapshots:
       - supports-color
       - utf-8-validate
 
+  webpack-dev-server@4.15.2(webpack@5.91.0):
+    dependencies:
+      '@types/bonjour': 3.5.13
+      '@types/connect-history-api-fallback': 1.5.4
+      '@types/express': 4.17.23
+      '@types/serve-index': 1.9.4
+      '@types/serve-static': 1.15.9
+      '@types/sockjs': 0.3.36
+      '@types/ws': 8.18.1
+      ansi-html-community: 0.0.8
+      bonjour-service: 1.3.0
+      chokidar: 3.6.0
+      colorette: 2.0.20
+      compression: 1.8.1
+      connect-history-api-fallback: 2.0.0
+      default-gateway: 6.0.3
+      express: 4.21.2
+      graceful-fs: 4.2.11
+      html-entities: 2.6.0
+      http-proxy-middleware: 2.0.9(@types/express@4.17.23)
+      ipaddr.js: 2.2.0
+      launch-editor: 2.11.1
+      open: 8.4.2
+      p-retry: 4.6.2
+      rimraf: 3.0.2
+      schema-utils: 4.3.3
+      selfsigned: 2.4.1
+      serve-index: 1.9.1
+      sockjs: 0.3.24
+      spdy: 4.0.2
+      webpack-dev-middleware: 5.3.4(webpack@5.91.0)
+      ws: 8.18.3
+    optionalDependencies:
+      webpack: 5.91.0
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - supports-color
+      - utf-8-validate
+    optional: true
+
   webpack-format-messages@3.0.1:
     dependencies:
       kleur: 4.1.5
@@ -29622,6 +29824,38 @@ snapshots:
 
   webpack-virtual-modules@0.6.2: {}
 
+  webpack@5.91.0:
+    dependencies:
+      '@types/eslint-scope': 3.7.7
+      '@types/estree': 1.0.8
+      '@webassemblyjs/ast': 1.14.1
+      '@webassemblyjs/wasm-edit': 1.14.1
+      '@webassemblyjs/wasm-parser': 1.14.1
+      acorn: 8.15.0
+      acorn-import-assertions: 1.9.0(acorn@8.15.0)
+      browserslist: 4.26.3
+      chrome-trace-event: 1.0.4
+      enhanced-resolve: 5.18.3
+      es-module-lexer: 1.7.0
+      eslint-scope: 5.1.1
+      events: 3.3.0
+      glob-to-regexp: 0.4.1
+      graceful-fs: 4.2.11
+      json-parse-even-better-errors: 2.3.1
+      loader-runner: 4.3.1
+      mime-types: 2.1.35
+      neo-async: 2.6.2
+      schema-utils: 3.3.0
+      tapable: 2.3.0
+      terser-webpack-plugin: 5.3.14(webpack@5.91.0)
+      watchpack: 2.4.4
+      webpack-sources: 3.3.3
+    transitivePeerDependencies:
+      - '@swc/core'
+      - esbuild
+      - uglify-js
+    optional: true
+
   webpack@5.91.0(@swc/core@1.3.96):
     dependencies:
       '@types/eslint-scope': 3.7.7

+ 2 - 1
pnpm-workspace.yaml

@@ -2,4 +2,5 @@ packages:
   - 'mini'
   - 'web'
   - 'packages/*'
-  - 'allin-packages/*'
+  - 'allin-packages/*'
+  - 'mini-ui-packages/*'