表单数据字典
数据字典将业务值映射成可读文本,供 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 的函数。依次切换下面的选项,观察绑定值;异步函数最终也必须返回数组。
字段类型
对比相同含义的字符串值与数字值,结果区同时展示值和 typeof。按后端接口约定设置 dataType,避免回显时只出现编码。
字段配置
第一项通过 props.label 和 props.value 使用 name/code 字段,第二项通过 props.res 从响应体的 data.list 读取数组。props.res 从 axios 响应的 data 开始,不需要再额外增加一层 data。
网络字典
在入口处将请求实例传给 Avue:
import axios from 'axios';
app.use(Avue, { axios });dicMethod 默认为 get,此时 dicQuery 放在查询参数中;设置为 post 等方法时放在请求体中。网络字典不需要同时设置 dicData。
两个下拉框分别发送 GET 和 POST 请求。下方显示组件实际生成的方法与参数。此处使用页面内的模拟接口,复制到业务项目时将 dicUrl 改成自己的接口,并移除模拟接口相关代码。
字典格式化
dicFormatter 接收 axios 响应的 data,返回最终数组。下面从 payload.items 中提取数据并追加“其他”;格式化时使用 map 返回新对象,避免修改共享的接口数据。
自行实现 dicFormatter 时,也应确保输出的值类型与表单一致。已在格式化函数中提取数组时,不需要再配置 props.res。
禁止字典某项
为字典项设置 disabled: true 后,该项仍可展示,但用户无法选择;需要禁止整个字段时,应在 column 上设置 disabled。
字典联动
在上级字段上用 cascader 声明下级的 prop,下级的 dicUrl 或 dicQuery 中用 {{key}} 接收上级值。它不是固定表示当前字段,也不是字典对象的 label。
切换大区,两个站点字段会一起重新加载。“首选站点”设置 cascaderIndex: 0,自动选择返回字典的第一项;“备选站点”由用户手动选择。清空大区后两个下级也会清空。
cascaderIndex 配在需要默认选中的下级字段上,从 0 开始,索引不能超出返回字典的范围。多级链路、其他表单参数及 Input/Radio 联动见 表单多级联动。
修改数据
点击“使用临时字典”执行 updateDic(prop, list),直接替换当前选项;点击“重新请求字典”执行 updateDic(prop),重新读取 dicUrl。两个操作分别展示完成状态和当前值。
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 }));
}