前后端为个下拉框又吵起来了?一招优雅的 Java 枚举解法,让团队和谐开发!

上周五快下班,前后端又吵起来了。

前端小李:

"哥,能不能别改接口?下拉框又空了!"

后端老王:

"我明明加了审核驳回,你前端没同步!"

我夹在中间,只想回家躺着。 就这么个小小的下拉框状态,前后端来回改了三版,还是错。

1. 问题到底出在哪?

两边各写各的。

  • 后端数据库:0=待审核,1=通过,2=驳回
  • 前端下拉框:0=待审核,1=已通过,2=拒绝

差一个汉字,线上就白屏。

2. 土办法:把字典写死

我一开始也干过:

java 复制代码
// 后端
if (status == 2) return "驳回";

// 前端
if (value == 2) option.text = "拒绝";

写起来确实快,可要是需求一变,两边都得改,非常的麻烦。

3. 枚举

后来我把字典收进一个枚举里,前后端都吃同一份,世界瞬间清净。

新建一个枚举 StatusEnum.java

java 复制代码
public enum StatusEnum {

    PENDING(0, "待审核"),
    PASS(1, "通过"),
    REJECT(2, "驳回");

    private final int code;
    private final String desc;

    StatusEnum(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    // 根据 code 拿描述
    public static String descOf(Integer code) {
        for (StatusEnum e : values()) {
            if (e.code == code) {
                return e.desc;
            }
        }
        return "未知";
    }

    // 直接扔给前端当下拉框
    public static List<Map<String, Object>> options() {
        return Arrays.stream(values())
                     .map(e -> Map.of("value", e.code, "text", e.desc))
                     .collect(Collectors.toList());
    }
}

后端接口直接复用

java 复制代码
@GetMapping("/status/options")
public List<Map<String, Object>> statusOptions() {
    return StatusEnum.options();   // 一行搞定
}

前端拿到直接用

javascript 复制代码
axios.get('/status/options').then(res => {
  this.statusOptions = res.data; // 再也不用手写
});

这时产品想加"已撤销",枚举里直接加一行CANCEL(3, "已撤销")就可以了,前后端零改动。 运营要把"驳回"改成"拒绝",只改枚举的desc,重新部署,完事。

4. 进阶用法

一个系统里,如果很多这种固定选项,比如:

  • 订单类型:普通单、秒杀单、预售单
  • 支付方式:微信、支付宝、银联
  • 审核状态:待提交、审核中、已通过、已驳回
  • 会员等级:青铜、白银、黄金、钻石
  • 甚至:性别、来源渠道、设备类型......

每个都要下拉框,且都要加接口让前后端对数据,这反而很繁琐。咋整呢?我们往下看。

枚举不止是类型,它也可以是一种数据契约

我们搞了个EnumRepository,专门统一管理所有业务枚举。

java 复制代码
@Component
public class EnumRepository {

    // 所有枚举都注册到这里
    private final Map<String, List<Map<String, Object>>> enumMap = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        // 注册所有枚举列表
        enumMap.put("user_status", UserStatusEnum.list());
        enumMap.put("order_type", OrderTypeEnum.list());
        enumMap.put("pay_channel", PayChannelEnum.list());
        // ... 更多
    }

    // 提供给前端的统一接口
    public List<Map<String, Object>> getEnum(String type) {
        return enumMap.getOrDefault(type, Collections.emptyList());
    }

    public Map<String, List<Map<String, Object>>> getAllEnums() {
        return enumMap;
    }
}

然后暴露一个接口:

java 复制代码
@GetMapping("/enums")
public Result<Map<String, List<Map<String, Object>>>> getAllEnums() {
    return Result.success(enumRepository.getAllEnums());
}

前端一进来,调一次/enums,拿到所有下拉框数据:

json 复制代码
{
  "user_status": [
    { "value": 1, "label": "正常" },
    { "value": 2, "label": "禁用" }
  ],
  "order_type": [
    { "value": 1, "label": "普通单" },
    { "value": 2, "label": "秒杀单" }
  ],
  "pay_channel": [
    { "value": 1, "label": "微信" },
    { "value": 2, "label": "支付宝" }
  ]
}

前端存进PiniaRedux,全局使用。不需要再关心这个字段对应啥选项,直接查enums.user_status 就行。 也封装个Dict工具,然后调用就可以了。

javascript 复制代码
// src/utils/dict.js

let dictData = {};

/**
 * 初始化字典数据(在登录后或应用启动时调用)
 * @param {Object} data 后端返回的全部枚举 { user_status: [...], pay_channel: [...] }
 */
export function initDict(data) {
  dictData = { ...data };
}

/**
 * 根据类型和值,获取 label
 * @param {String} type 枚举类型,如 'user_status'
 * @param {Number|String} value 枚举值
 * @returns {String} 对应的文本,找不到返回 '-'
 */
export function label(type, value) {
  const items = dictData[type];
  if (!items) return '-';
  const item = items.find(i => i.value == value); // == 避免类型问题
  return item ? item.label : '-';
}

/**
 * 获取某个枚举的全部选项
 * @param {String} type 枚举类型
 * @returns {Array} 选项列表 [{value, label}, ...]
 */
export function options(type) {
  return dictData[type] || [];
}

/**
 * 获取完整项(包含 tagType, disabled 等元信息)
 * @param {String} type 枚举类型
 * @param {Number|String} value 枚举值
 * @returns {Object|null} 完整对象
 */
export function get(type, value) {
  const items = dictData[type];
  if (!items) return null;
  return items.find(i => i.value == value) || null;
}

// 也可以挂到全局,比如 window.Dict
export const Dict = {
  label,
  options,
  get,
  init: initDict
};
js 复制代码
// 根据 type 和 value 查 label
Dict.label('user_status', 1) // 返回 '正常'

// 根据 type 查所有选项
Dict.options('pay_channel')

// 带样式的
Dict.get('order_status', 2) // 返回 { value: 2, label: '已发货', color: 'blue' }

表格列直接用:

js 复制代码
{
  label: '状态',
  formatter: row => Dict.label('user_status', row.status)
}

下拉框:

js 复制代码
<el-select :options="Dict.options('user_status')" />

前端开发再也不写死任何下拉数据。 你可能会说:这不就是个字典表吗? 不一样。 字典表是数据驱动 ,适合运营可配置的场景。 枚举是代码契约,适合"业务规则固定"的场景。 我们用枚举,解决的不是技术问题,而是:

  • 沟通成本
  • 数据一致性
  • 维护复杂度

当前端说:"这个字段的选项在哪?"

你能说:"去/enums拉一下,或者看UserStatusEnum。"

那一刻,你就知道:这枚举,值了。 搞定。

我是大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《Elasticsearch 太重?来看看这个轻量级的替代品 Manticore Search》

《只会写 Mapper 就敢说会 MyBatis?面试官:原理都没懂》

《别学23种了!Java项目中最常用的6个设计模式,附案例》

《写给小公司前端的 UI 规范:别让页面丑得自己都看不下去》

《Vue3+TS设计模式:5个真实场景让你代码更优雅》

相关推荐
Charlo3 小时前
是什么让一个AI系统成为智能体(Agent)?
前端·后端
哈基米喜欢哈哈哈3 小时前
MongoDB入门
数据库·后端·mongodb
石小石Orz3 小时前
来自面试官给我的建议,我备受启发
前端·后端·面试
珹洺3 小时前
Java-Spring入门指南(二)利用IDEA手把手教你如何创建第一个Spring系统
java·spring·intellij-idea
Java水解3 小时前
MySQL 高级查询:JOIN、子查询、窗口函数
后端·mysql
偏执网友3 小时前
idea上传本地项目代码到Gitee仓库教程
java·gitee·intellij-idea
东百牧码人4 小时前
.NET开发之不要在构造函数中进行 文件操作、数据操作、网络请求等受检操作
后端
海边捡石子4 小时前
openGauss 支持的四种兼容模式
后端
bobz9654 小时前
BGP 和 OSPF 的区别
后端