AvueAvue
首页
  • 开发指南
  • Skill开发
  • 在线测试工具
  • Form组件
  • Crud组件
  • Default组件
  • Data组件
  • Component组件
产品
工作台
授权
联系
2.x文档
个人支付接口
首页
  • 开发指南
  • Skill开发
  • 在线测试工具
  • Form组件
  • Crud组件
  • Default组件
  • Data组件
  • Component组件
产品
工作台
授权
联系
2.x文档
个人支付接口
  • Form属性文档
  • Object对象用法
  • Input 输入框
  • InputOtp 一次性密码输入
  • Number 数字输入框
  • Select选择框
  • Cascader级联选择器
  • Checkbox多选框
  • Radio单选框
  • Date日期
  • Time 时间
  • Switch开关
  • Upload附件上传
  • Title标题
  • Array数组框
  • Dynamic子表单
  • Tree树型选择框
  • Icon图标选择器
  • Table表格选择器
  • Map坐标选择器
  • Color颜色选择器
  • Input Cron Cron表达式编辑器
  • InputTag 输标签输入框
  • Mention 提及框
  • Rate评价
  • Slider滑块
  • 表单布局
  • 表单验证
  • 表单默认值
  • 表单操作按钮
  • 表单自定义
  • 表单数据字典
  • 表单多级联动
  • 表单数据格式
  • 表单组件事件
  • 表单高级用法

表单数据字典

数据字典将业务值映射成可读文本,供 Select、Radio、Checkbox、Tree 和 Cascader 等字段使用。先确定字典结构与值类型,再选择本地数据或接口加载。

配置用途示例
dicData直接提供字典数组,也支持函数或 Promise[{ label: '启用', value: 1 }]
dicUrl请求字典接口'/api/status'
props.label / props.value指定文字和值对应的键{ label: 'name', value: 'code' }
props.res从响应体中提取字典数组'data.list'
dicFormatter将响应体转换为字典数组data => data.items.map(...)
dataType统一字段的数据类型'string' 或 'number'

提示

字典项的 value 和表单绑定值要使用一致的类型,例如都为字符串 '1',或都为数字 1。数字 0 是有效的字典值,不要用 if (value) 判断是否有选择。

本地字典

效果预览可直接操作下方示例

3.5.0 起,dicData 支持数组、返回数组的函数,以及 Promise 或返回 Promise 的函数。依次切换下面的选项,观察绑定值;异步函数最终也必须返回数组。

正在加载示例…
<template>
  <avue-form v-model="form" :option="option" />
  <form-demo-result :value="result" hint="0 是有效选项;三个示例的字典值和绑定值都使用数字。" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';

const options = [{ label: '待处理', value: 0 }, { label: '处理中', value: 1 }, { label: '已完成', value: 2 }];
const form = ref({ arrayValue: 0, functionValue: 1, promiseValue: 2 });
const result = computed(() => ({
  arrayValue: form.value.arrayValue,
  functionValue: form.value.functionValue,
  promiseValue: form.value.promiseValue,
}));
const option = {
  menuBtn: false,
  labelWidth: 110,
  column: [
    { label: '数组', prop: 'arrayValue', type: 'select', span: 24, dicData: options },
    { label: '函数', prop: 'functionValue', type: 'select', span: 24, dicData: () => options },
    { label: '异步函数', prop: 'promiseValue', type: 'select', span: 24,
      dicData: () => Promise.resolve(options) },
  ],
};
</script>

字段类型

效果预览可直接操作下方示例

对比相同含义的字符串值与数字值,结果区同时展示值和 typeof。按后端接口约定设置 dataType,避免回显时只出现编码。

正在加载示例…
<template>
  <avue-form v-model="form" :option="option" />
  <form-demo-result :value="result" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';

const form = ref({ stringCode: '1', numberCode: 1 });
const result = computed(() => ({
  stringCode: { value: form.value.stringCode, type: typeof form.value.stringCode },
  numberCode: { value: form.value.numberCode, type: typeof form.value.numberCode },
}));
const option = {
  menuBtn: false,
  labelWidth: 120,
  column: [
    { label: '字符串编码', prop: 'stringCode', type: 'select', dataType: 'string', span: 24,
      dicData: [{ label: '启用', value: '1' }, { label: '停用', value: '0' }] },
    { label: '数字编码', prop: 'numberCode', type: 'select', dataType: 'number', span: 24,
      dicData: [{ label: '启用', value: 1 }, { label: '停用', value: 0 }] },
  ],
};
</script>

字段配置

效果预览可直接操作下方示例

第一项通过 props.label 和 props.value 使用 name/code 字段,第二项通过 props.res 从响应体的 data.list 读取数组。props.res 从 axios 响应的 data 开始,不需要再额外增加一层 data。

正在加载示例…
<template>
  <avue-form v-model="form" :option="option" />
  <form-demo-result :value="result" hint="接口响应为 { data: { list: [...] } },props.res 从响应体开始解析。" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';
import { regionOptions, useDictionaryMock } from '../_shared/useDictionaryMock';

const form = ref({ permission: 0, region: 'north' });
const { baseUrl } = useDictionaryMock(() => ({ data: { list: regionOptions } }));
const result = computed(() => ({ permission: form.value.permission, region: form.value.region }));
const option = {
  menuBtn: false,
  column: [
    { label: '字段映射', prop: 'permission', type: 'select', span: 24,
      props: { label: 'name', value: 'code' },
      dicData: [{ name: '可查看', code: 0 }, { name: '可编辑', code: 1 }] },
    { label: '嵌套响应', prop: 'region', type: 'select', span: 24,
      props: { label: 'name', value: 'code', res: 'data.list' },
      dicUrl: baseUrl + '/regions' },
  ],
};
</script>

网络字典

在入口处将请求实例传给 Avue:

import axios from 'axios';
app.use(Avue, { axios });

dicMethod 默认为 get,此时 dicQuery 放在查询参数中;设置为 post 等方法时放在请求体中。网络字典不需要同时设置 dicData。

效果预览可直接操作下方示例

两个下拉框分别发送 GET 和 POST 请求。下方显示组件实际生成的方法与参数。此处使用页面内的模拟接口,复制到业务项目时将 dicUrl 改成自己的接口,并移除模拟接口相关代码。

正在加载示例…
<template>
  <avue-form v-model="form" :option="option" />
  <form-demo-result :value="result" title="当前值与最近请求" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';
import { useDictionaryMock } from '../_shared/useDictionaryMock';

const form = ref({ status: 'enabled', channel: 'online' });
const { baseUrl, requests } = useDictionaryMock(({ path, query }) => {
  if (path === '/statuses' && query.enabled === true) {
    return [{ label: '启用', value: 'enabled' }, { label: '停用', value: 'disabled' }];
  }
  if (path === '/channels' && query.business === 'service') {
    return [{ label: '线上', value: 'online' }, { label: '线下', value: 'offline' }];
  }
  return [];
});
const result = computed(() => ({ status: form.value.status, channel: form.value.channel, requests: requests.value }));
const option = {
  menuBtn: false,
  column: [
    { label: 'GET 请求', prop: 'status', type: 'select', span: 24,
      dicUrl: baseUrl + '/statuses', dicQuery: { enabled: true } },
    { label: 'POST 请求', prop: 'channel', type: 'select', span: 24,
      dicUrl: baseUrl + '/channels', dicMethod: 'post', dicQuery: { business: 'service' } },
  ],
};
</script>

字典格式化

效果预览可直接操作下方示例

dicFormatter 接收 axios 响应的 data,返回最终数组。下面从 payload.items 中提取数据并追加“其他”;格式化时使用 map 返回新对象,避免修改共享的接口数据。

正在加载示例…
<template>
  <avue-form v-model="form" :option="option" />
  <form-demo-result :value="result" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';
import { useDictionaryMock } from '../_shared/useDictionaryMock';

const form = ref({ service: 'setup' });
const { baseUrl } = useDictionaryMock(() => ({ payload: { items: [
  { code: 'setup', name: '安装', enabled: true },
  { code: 'repair', name: '维修', enabled: true },
  { code: 'legacy', name: '旧服务', enabled: false },
] } }));
const result = computed(() => ({ service: form.value.service }));
const option = {
  menuBtn: false,
  column: [{
    label: '服务', prop: 'service', type: 'select', span: 24,
    dicUrl: baseUrl + '/services',
    dicFormatter(data) {
      return [
        ...data.payload.items.filter((item) => item.enabled)
          .map((item) => ({ label: item.name + '服务', value: item.code })),
        { label: '其他', value: 'other' },
      ];
    },
  }],
};
</script>

自行实现 dicFormatter 时,也应确保输出的值类型与表单一致。已在格式化函数中提取数组时,不需要再配置 props.res。

禁止字典某项

效果预览可直接操作下方示例

为字典项设置 disabled: true 后,该项仍可展示,但用户无法选择;需要禁止整个字段时,应在 column 上设置 disabled。

正在加载示例…
<template>
  <avue-form v-model="form"
             :option="option"></avue-form>
</template>

<script setup>
import { ref } from 'vue';

const form = ref({
  grade: 1
});

const option = ref({
  column: [
    {
      label: '权限',
      prop: 'grade',
      type: 'select',
      dicData: [
        {
          label: '有权限',
          value: 1
        },
        {
          label: '无权限',
          value: 0
        },
        {
          label: '禁止项',
          disabled: true,
          value: -1
        }
      ]
    }
  ]
});
</script>

字典联动

在上级字段上用 cascader 声明下级的 prop,下级的 dicUrl 或 dicQuery 中用 {{key}} 接收上级值。它不是固定表示当前字段,也不是字典对象的 label。

效果预览可直接操作下方示例

切换大区,两个站点字段会一起重新加载。“首选站点”设置 cascaderIndex: 0,自动选择返回字典的第一项;“备选站点”由用户手动选择。清空大区后两个下级也会清空。

正在加载示例…
<template>
  <avue-form v-model="form" :option="option" />
  <form-demo-result :value="result" title="联动值与最近请求" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';
import { getRegionChildren, regionOptions, useDictionaryMock } from '../_shared/useDictionaryMock';

const form = ref({ region: 'north', primarySite: 'beijing', backupSite: 'tianjin' });
const { baseUrl, requests } = useDictionaryMock(({ query }) => getRegionChildren(query.region));
const result = computed(() => ({
  region: form.value.region, primarySite: form.value.primarySite,
  backupSite: form.value.backupSite, requests: requests.value,
}));
const option = {
  menuBtn: false,
  labelWidth: 110,
  column: [
    { label: '大区', prop: 'region', type: 'select', span: 24,
      props: { label: 'name', value: 'code' }, dicData: regionOptions,
      cascader: ['primarySite', 'backupSite'] },
    { label: '首选站点', prop: 'primarySite', type: 'select', span: 24, cascaderIndex: 0,
      props: { label: 'name', value: 'code' }, dicUrl: baseUrl + '/primary-sites',
      dicQuery: { region: '{{key}}' } },
    { label: '备选站点', prop: 'backupSite', type: 'select', span: 24,
      props: { label: 'name', value: 'code' }, dicUrl: baseUrl + '/backup-sites',
      dicQuery: { region: '{{key}}' } },
  ],
};
</script>

cascaderIndex 配在需要默认选中的下级字段上,从 0 开始,索引不能超出返回字典的范围。多级链路、其他表单参数及 Input/Radio 联动见 表单多级联动。

修改数据

效果预览可直接操作下方示例

点击“使用临时字典”执行 updateDic(prop, list),直接替换当前选项;点击“重新请求字典”执行 updateDic(prop),重新读取 dicUrl。两个操作分别展示完成状态和当前值。

正在加载示例…
<template>
  <el-space wrap>
    <el-button :disabled="loading" @click="replaceDictionary">使用临时字典</el-button>
    <el-button type="primary" :loading="loading" @click="reloadDictionary">重新请求字典</el-button>
    <span>{{ status }}</span>
  </el-space>
  <avue-form ref="formRef" v-model="form" :option="option" />
  <form-demo-result :value="result" hint="更新选项不会自动清空已选择的值,本例两套字典都保留相同编码。" />
</template>

<script setup>
import { computed, ref } from 'vue';
import FormDemoResult from '../_shared/FormDemoResult.vue';
import { useDictionaryMock } from '../_shared/useDictionaryMock';

const formRef = ref();
const form = ref({ service: 'setup' });
const loading = ref(false);
const status = ref('初始字典');
const { baseUrl, requests } = useDictionaryMock(() => [
  { label: '标准安装', value: 'setup' }, { label: '标准维护', value: 'repair' },
]);
const option = {
  menuBtn: false,
  column: [{ label: '服务', prop: 'service', type: 'select', span: 24, dicUrl: baseUrl + '/services' }],
};
const result = computed(() => ({ service: form.value.service, source: status.value, requests: requests.value }));

async function replaceDictionary() {
  await formRef.value.updateDic('service', [
    { label: '加急安装', value: 'setup' }, { label: '上门维护', value: 'repair' },
  ]);
  status.value = '临时字典已生效';
}
async function reloadDictionary() {
  loading.value = true;
  try {
    const list = await formRef.value.updateDic('service');
    status.value = list ? '接口字典已恢复' : '加载失败,可重试';
  } finally {
    loading.value = false;
  }
}
</script>

updateDic 返回 Promise,可以等待字典更新完成后再继续业务逻辑。替换字典不等于清空字段值;若新字典已不包含旧值,应按业务需要同步清空或重新赋值。带上级参数的联动字段应通过上级变更触发请求,不要把无参刷新当作自动填充级联参数。

示例辅助文件

本页和相关表单示例中的 FormDemoResult 只负责展示当前值;useDictionaryMock 用局部模拟响应演示真实的 Avue 字典请求。它只匹配当前示例的专属地址,离开示例时会移除拦截器,不会请求外部服务。

复制完整示例时,将下面两个辅助文件保存到示例旁的 ../_shared/ 目录;也可以移除结果组件,并将模拟接口替换为业务接口。区域数据是用于演示的固定服务站点,不是完整行政区数据。

查看 FormDemoResult.vue:结果展示组件
<template>
  <section class="form-demo-result" aria-live="polite">
    <div class="form-demo-result__heading">{{ title }}</div>
    <p v-if="hint" class="form-demo-result__hint">{{ hint }}</p>
    <pre>{{ JSON.stringify(value, null, 2) }}</pre>
  </section>
</template>

<script setup>
defineProps({
  title: { type: String, default: '当前绑定值' },
  hint: { type: String, default: '' },
  value: { required: true },
});
</script>

<style scoped>
.form-demo-result {
  min-width: 0;
  margin-top: 16px;
  padding: 14px 16px;
  border: 1px solid var(--el-border-color-lighter);
  border-radius: 10px;
  background: var(--el-fill-color-light);
}
.form-demo-result__heading {
  color: var(--el-text-color-primary);
  font-size: 13px;
  font-weight: 600;
}
.form-demo-result__hint {
  margin: 6px 0 0;
  color: var(--el-text-color-secondary);
  font-size: 13px;
  line-height: 1.6;
}
.form-demo-result pre {
  max-height: 260px;
  margin: 10px 0 0;
  padding: 0;
  overflow: auto;
  color: var(--el-text-color-regular);
  background: transparent;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 12px;
  line-height: 1.7;
}
</style>
查看 useDictionaryMock.js:模拟接口与演示数据
import axios from 'axios';
import { getCurrentInstance, onScopeDispose, ref } from 'vue';

// Only this demo instance's URLs are intercepted. Real application requests are untouched.
export function useDictionaryMock(resolveData) {
  const baseUrl = `/__avue_form_demo__/${getCurrentInstance().uid}`;
  const requests = ref([]);
  let active = true;
  let sequence = 0;

  // Avue loads dictionaries after mounting, so SSR needs no request interceptor.
  if (typeof window !== 'undefined') {
    const interceptor = axios.interceptors.request.use((config) => {
      if (!config.url?.startsWith(`${baseUrl}/`)) return config;
      config.adapter = async (request) => {
        const body = typeof request.data === 'string'
          ? JSON.parse(request.data)
          : request.data;
        const record = {
          id: ++sequence,
          method: (request.method || 'get').toUpperCase(),
          path: request.url.slice(baseUrl.length),
          query: request.params || body || {},
        };
        const data = await resolveData(record);
        if (active) requests.value = [record, ...requests.value].slice(0, 6);
        return {
          data: JSON.parse(JSON.stringify(data)),
          status: 200,
          statusText: 'OK',
          headers: {},
          config: request,
        };
      };
      return config;
    });
    onScopeDispose(() => {
      active = false;
      axios.interceptors.request.eject(interceptor);
    });
  }

  return { baseUrl, requests };
}

// A small fixed dataset is enough to demonstrate parent/child relationships.
export const regions = [
  {
    name: '北方大区', code: 'north', children: [
      { name: '北京站', code: 'beijing', children: [
        { name: '朝阳服务点', code: 'chaoyang' },
        { name: '海淀服务点', code: 'haidian' },
      ] },
      { name: '天津站', code: 'tianjin', children: [
        { name: '和平服务点', code: 'heping' },
      ] },
    ],
  },
  {
    name: '南方大区', code: 'south', children: [
      { name: '杭州站', code: 'hangzhou', children: [
        { name: '西湖服务点', code: 'xihu' },
        { name: '滨江服务点', code: 'binjiang' },
      ] },
      { name: '广州站', code: 'guangzhou', children: [
        { name: '天河服务点', code: 'tianhe' },
      ] },
    ],
  },
];

export const regionOptions = regions.map(({ name, code }) => ({ name, code }));

export function getRegionChildren(code) {
  const nodes = [...regions, ...regions.flatMap((region) => region.children)];
  return (nodes.find((node) => node.code === code)?.children || [])
    .map(({ name, code }) => ({ name, code }));
}
最后更新:
贡献者: smallwei
Prev
表单自定义
Next
表单多级联动