Skip to content

DictFields 字典表单字段

基于字典代码自动加载选项数据的表单字段组件族,包含 DictSelectFieldDictRadioFieldDictCheckboxField 三种组件。

特性

  • ✅ 基于 TanStack Query 实现全局缓存,相同字典代码只请求一次
  • ✅ 支持在 DictionaryProvider 中配置全局 queryFn
  • ✅ 支持字段级别覆盖 queryFn
  • ✅ 自动显示加载状态和空数据提示
  • ✅ 支持字段映射、过滤和排序
  • ✅ 支持禁用选项
  • ✅ 完整的 TypeScript 类型支持
  • ✅ 与 @tanstack/react-form 无缝集成

导入

tsx
import { DictionaryProvider, Form } from '@xcloud/ui-core'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

示例

基础用法

1. 配置 DictionaryProvider

首先,在表单外层包裹 QueryClientProviderDictionaryProvider 并提供全局 queryFn

tsx
import { DictionaryProvider } from '@xcloud/ui-core'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// 创建 QueryClient 实例
const queryClient = new QueryClient()

// 定义字典查询函数
const fetchDictionary = async (dictCode: string) => {
  const response = await fetch(`/api/dict/${dictCode}`)
  const data = await response.json()
  return {
    data, // 字典项数组
    total: data.length
  }
}

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DictionaryProvider value={{ queryFn: fetchDictionary }}>
        <MyForm />
      </DictionaryProvider>
    </QueryClientProvider>
  )
}

2. 使用字典表单字段

DictionaryProvider 内部使用字典表单字段组件:

tsx
import { DictionaryProvider, Form } from '@xcloud/ui-core'

function MyForm() {
  const form = Form.useAppForm({
    defaultValues: {
      gender: '',
      hobbies: []
    }
  })

  return (
    <form onSubmit={form.handleSubmit}>
      {/* 下拉选择 */}
      <form.AppField name="gender">
        {(field) => (
          <field.DictSelectField
            dictCode="gender"
            label="性别"
            required
          />
        )}
      </form.AppField>

      {/* 多选框 */}
      <form.AppField name="hobbies">
        {(field) => (
          <field.DictCheckboxField
            dictCode="hobby"
            label="兴趣爱好"
          />
        )}
      </form.AppField>
    </form>
  )
}

组件类型

DictSelectField - 下拉选择

基于字典的下拉选择框,适合选项较多的场景:

tsx
<form.AppField name="city">
  {(field) => (
    <field.DictSelectField
      dictCode="city"
      label="城市"
      placeholder="请选择城市"
      hint="选择您所在的城市"
      required
    />
  )}
</form.AppField>

DictRadioField - 单选框

基于字典的单选框组,适合选项较少且需要全部可见的场景:

tsx
<form.AppField name="gender">
  {(field) => (
    <field.DictRadioField
      dictCode="gender"
      label="性别"
      hint="请选择您的性别"
      required
    />
  )}
</form.AppField>

DictCheckboxField - 多选框

基于字典的复选框组,用于多选场景:

tsx
<form.AppField name="hobbies">
  {(field) => (
    <field.DictCheckboxField
      dictCode="hobby"
      label="兴趣爱好"
      hint="选择您的兴趣爱好(可多选)"
    />
  )}
</form.AppField>

与 @xcloud/request 集成

推荐使用 @xcloud/request 包来管理字典请求,它提供了统一的请求配置、拦截器和错误处理。

快速开始

1. 设置 RequestProvider

在应用根部提供 RequestProvider

tsx
// App.tsx
import { RequestProvider } from '@xcloud/request'
import { requestClient } from '@/config/request'

function App() {
  return (
    <RequestProvider client={requestClient}>
      <YourApp />
    </RequestProvider>
  )
}

2. 使用 Hook 版本适配器(推荐)

使用 useDictionaryAdapter 自动从 RequestProvider 获取 client:

tsx
import { useDictionaryAdapter, DictSelectField } from '@xcloud/ui-core'

function UserForm() {
  // 自动从 RequestProvider 获取 client
  const queryFn = useDictionaryAdapter({
    url: '/api/dict/{dictCode}',
  })

  return (
    <form.Provider>
      <form.AppField name="status">
        {(field) => (
          <field.DictSelectField
            dictCode="user_status"
            queryFn={queryFn}
            label="用户状态"
          />
        )}
      </form.AppField>
    </form.Provider>
  )
}

集成方式

方式 1: Hook 版本(推荐)

自动从 RequestProvider 获取 client,无需手动传递:

tsx
import { useDictionaryAdapter } from '@xcloud/ui-core'

function MyForm() {
  const queryFn = useDictionaryAdapter({
    url: '/api/dict/{dictCode}',
    method: 'GET', // 可选,默认 GET
  })

  return (
    <form.AppField name="role">
      {(field) => (
        <field.DictSelectField
          dictCode="user_role"
          queryFn={queryFn}
          label="角色"
        />
      )}
    </form.AppField>
  )
}

方式 2: 工厂函数版本

手动创建 client 和适配器:

tsx
import { createClient } from '@xcloud/request'
import { createDictionaryAdapter } from '@xcloud/ui-core'

const client = createClient({ baseURL: '/api' })

const queryFn = createDictionaryAdapter({
  url: '/dict/{dictCode}',
  client,
})

function MyForm() {
  return (
    <form.AppField name="status">
      {(field) => (
        <field.DictSelectField
          dictCode="user_status"
          queryFn={queryFn}
          label="状态"
        />
      )}
    </form.AppField>
  )
}

高级配置

自定义响应转换

如果后端返回格式与标准格式不同,可以自定义转换:

tsx
const queryFn = useDictionaryAdapter({
  url: '/api/dict/{dictCode}',
  transformResponse: (response) => ({
    data: response.items.map(item => ({
      value: item.id,
      label: item.name,
      disabled: !item.enabled,
    })),
    total: response.count,
  }),
})

POST 方法

tsx
const queryFn = useDictionaryAdapter({
  url: '/api/dict/query',
  method: 'POST',
  params: {
    category: 'system',
    status: 'active',
  },
})

批量获取字典

tsx
import { useBatchDictionaryAdapter } from '@xcloud/ui-core'

function MyForm() {
  const batchQueryFn = useBatchDictionaryAdapter({
    url: '/api/dict/batch',
  })

  React.useEffect(() => {
    async function loadDicts() {
      const dictionaries = await batchQueryFn(['user_status', 'user_role'])
      // dictionaries = {
      //   user_status: { data: [...], total: 10 },
      //   user_role: { data: [...], total: 5 }
      // }
    }
    loadDicts()
  }, [])
}

创建可复用的适配器

在 API 层统一配置字典适配器:

tsx
// api/dictionaries.ts
import { useDictionaryAdapter } from '@xcloud/ui-core'

export function useDictApi() {
  return useDictionaryAdapter({
    url: '/api/system/dict/data/type/{dictCode}',
    method: 'GET',
  })
}

// 在组件中使用
import { useDictApi } from '@/api/dictionaries'

function UserForm() {
  const queryFn = useDictApi()

  return (
    <form.AppField name="status">
      {(field) => (
        <field.DictSelectField
          dictCode="user_status"
          queryFn={queryFn}
          label="状态"
        />
      )}
    </form.AppField>
  )
}

支持的响应格式

适配器自动识别以下后端响应格式:

typescript
// 格式 1: 直接返回数组
[{ value: '1', label: '选项1' }]

// 格式 2: 包含 data 字段
{ data: [...], total: 10 }

// 格式 3: 包含 list 字段
{ list: [...], total: 10 }

// 格式 4: 嵌套 data.data
{ data: { data: [...], total: 10 } }

所有格式都会自动转换为标准的 DictResponse 格式。

高级用法

字段级别覆盖 queryFn

可以在字段级别覆盖全局 queryFn:

tsx
const fetchSpecialDictionary = async (dictCode: string) => {
  // 自定义查询逻辑
  return { data: [...], total: 0 }
}

<form.AppField name="special">
  {(field) => (
    <field.DictSelectField
      dictCode="special"
      queryFn={fetchSpecialDictionary}
      label="特殊字典"
    />
  )}
</form.AppField>

字段映射

当后端返回的字段名与组件期望的不同时,使用 fieldNames 进行映射:

tsx
<form.AppField name="status">
  {(field) => (
    <field.DictSelectField
      dictCode="status"
      fieldNames={{
        value: 'id',      // 将 id 映射为 value
        label: 'name',    // 将 name 映射为 label
        disabled: 'inactive' // 将 inactive 映射为 disabled
      }}
      label="状态"
    />
  )}
</form.AppField>

过滤和排序

使用 filtersort 对选项进行过滤和排序:

tsx
<form.AppField name="user">
  {(field) => (
    <field.DictSelectField
      dictCode="user"
      // 只显示激活的用户
      filter={(item) => item.active === true}
      // 按名称排序
      sort={(a, b) => a.name.localeCompare(b.name)}
      label="用户"
    />
  )}
</form.AppField>

自定义缓存时间

通过 staleTime 自定义缓存时间(毫秒):

tsx
<form.AppField name="config">
  {(field) => (
    <field.DictSelectField
      dictCode="config"
      staleTime={10 * 60 * 1000} // 10分钟
      label="配置项"
    />
  )}
</form.AppField>

自定义加载和空数据提示

tsx
<form.AppField name="options">
  {(field) => (
    <field.DictSelectField
      dictCode="options"
      showLoading={true}
      loadingText="数据加载中..."
      emptyText="暂无可选项"
      label="选项"
    />
  )}
</form.AppField>

字典数据格式

标准格式

字典查询函数应返回以下格式的数据:

typescript
interface DictResponse<T = any> {
  data: T[]      // 字典项数组
  total?: number // 总数(可选)
}

interface DictItem {
  value: string | number  // 选项值
  label: string          // 显示文本
  disabled?: boolean     // 是否禁用
  [key: string]: any    // 扩展字段
}

示例数据

typescript
{
  data: [
    { value: 'male', label: '男' },
    { value: 'female', label: '女' },
    { value: 'other', label: '其他', disabled: true }
  ],
  total: 3
}

组件 API

DictSelectField Props

属性类型默认值描述
dictCodestring-必填。字典代码
queryFn(dictCode: string) => Promise<DictResponse>-查询函数,未提供时使用 Provider 中的配置
labelstring-表单标签
hintstring-提示信息
hintIconReact.ComponentType-提示图标组件
errorIconReact.ComponentType-错误图标组件
requiredbooleanfalse是否必填
disabledbooleanfalse是否禁用
placeholderstring'请选择'占位符
classNamestring-自定义样式类名
showLoadingbooleantrue是否显示加载状态
loadingTextstring'加载中...'加载文本
emptyTextstring'暂无数据'空数据文本
fieldNamesFieldNames-字段名称映射
filter(item: T) => boolean-过滤函数
sort(a: T, b: T) => number-排序函数
enabledbooleantrue是否启用查询
staleTimenumber300000缓存时间(毫秒)

DictRadioField Props

继承 DictSelectField 的所有属性,但不包括 placeholder

DictCheckboxField Props

继承 DictSelectField 的所有属性,但不包括 placeholder

注意:DictCheckboxField 的字段值类型为 Array<string | number>

FieldNames

typescript
interface FieldNames {
  value?: string    // 值字段名,默认 'value'
  label?: string    // 标签字段名,默认 'label'
  disabled?: string // 禁用字段名,默认 'disabled'
}

DictionaryConfig

typescript
interface DictionaryConfig {
  queryFn?: (dictCode: string) => Promise<DictResponse>
  staleTime?: number // 默认 300000 (5分钟)
}

最佳实践

1. 配置 QueryClient 和 Provider

推荐在应用根部配置 QueryClientProviderDictionaryProvider,让所有字典字段共享配置:

tsx
// App.tsx
import { DictionaryProvider } from '@xcloud/ui-core'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fetchDictionary } from './api/dictionary'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5分钟
      refetchOnWindowFocus: false,
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <DictionaryProvider value={{ queryFn: fetchDictionary }}>
        <Router />
      </DictionaryProvider>
    </QueryClientProvider>
  )
}

2. 统一字典查询接口

将字典查询逻辑封装到统一的 API 函数中:

typescript
// api/dictionary.ts
export async function fetchDictionary(dictCode: string) {
  const response = await fetch(`/api/v1/dictionaries/${dictCode}`)
  if (!response.ok) {
    throw new Error('Failed to fetch dictionary')
  }
  const data = await response.json()
  return {
    data: data.items,
    total: data.total
  }
}

3. 类型安全

为字典项定义具体类型以获得更好的类型推断:

typescript
interface GenderDictItem {
  value: 'male' | 'female' | 'other'
  label: string
}

<form.AppField name="gender">
  {(field) => (
    <field.DictSelectField<GenderDictItem>
      dictCode="gender"
      label="性别"
    />
  )}
</form.AppField>

4. 错误处理

在 queryFn 中添加适当的错误处理:

typescript
const fetchDictionary = async (dictCode: string) => {
  try {
    const response = await fetch(`/api/dict/${dictCode}`)
    if (!response.ok) throw new Error('Network error')
    return await response.json()
  } catch (error) {
    console.error('Dictionary fetch failed:', error)
    return { data: [], total: 0 }
  }
}

5. 预加载常用字典

对于频繁使用的字典,可以在应用启动时预加载:

typescript
import { useQuery } from '@tanstack/react-query'

function App() {
  // 预加载常用字典
  useQuery({
    queryKey: ['dictionary', 'gender'],
    queryFn: () => fetchDictionary('gender'),
    staleTime: 5 * 60 * 1000
  })

  return <YourApp />
}

缓存机制

组件使用 TanStack Query 实现全局缓存:

  1. 自动去重:相同 dictCode 的多个字段只会发起一次请求
  2. 缓存时间:默认缓存 5 分钟,可通过 staleTime 自定义
  3. 缓存键['dictionary', dictCode]
  4. 自动更新:缓存过期后自动重新请求

常见问题

如何刷新字典数据?

使用 useDictionary hook 返回的 refetch 方法:

tsx
const { refetch } = useDictionary({
  dictCode: 'status',
  queryFn: fetchDictionary
})

<button onClick={() => refetch()}>刷新</button>

如何禁用某些选项?

在字典数据中设置 disabled: true

typescript
{
  data: [
    { value: '1', label: '选项1' },
    { value: '2', label: '选项2', disabled: true }
  ]
}

如何处理大量选项?

对于选项很多的场景,推荐使用 DictSelectField 配合搜索功能(需要自定义实现)。

字典数据为空时如何处理?

组件会自动显示空数据提示,可通过 emptyText 自定义提示文本。

相关链接

基于 MIT 许可发布