前言

中午十二点,办公室里总会响起那句魔咒:“今天吃什么?”外卖刷了二十分钟,收藏的店吃了个遍,最后还是点了楼下的黄焖鸡。这种选择困难,本质上不是没得吃,而是选项太多,大脑宕机。有天我实在不想再纠结了,打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上写了个抽签工具。它把午餐拆成主食、肉类、蔬菜三大类,每类里面放着常吃的食物,点一下按钮,随机给你搭出一套组合,还能手动排除不想要的食材——比如我今天就是不想吃香菜。这篇文章就是那次“自救”的产物,里面会聊到随机数怎么保证公平、排除逻辑怎么写才不会让程序崩掉,以及怎么用最简单的 ArkUI 组件搭出一个能用的午饭决策器。代码也给全,你拷进模拟器,明天中午就不用再纠结了。

一、把午饭决策交给随机数——伪随机的那点事儿

抽签这件事,说到底就是从一个名单里随机挑一个。计算机里的“随机”和抛硬币不一样,它其实是靠一个数学公式算出来的伪随机数。这个公式从一个叫做“种子”的初始值开始,反复迭代出一个看似没有规律的序列。种子通常取自系统时间、硬件噪声这些难以预测的东西,所以对选午餐来说,它已经足够公平了。

HarmonyOS 的 ArkTS 里,Math.random() 就是那个伪随机数生成器。它返回一个 [0, 1) 之间的浮点数,比如 0.37421。要从一个数组里随机取一个元素,思路是把数组长度乘上这个随机数,再用 Math.floor 向下取整,得到一个索引。比如:

let items = ['米饭', '馒头', '面条'];
let index = Math.floor(Math.random() * items.length);
let choice = items[index];

这个操作重复三次——分别从主食、肉类、蔬菜里各抽一个——一顿饭就搭配好了。要保证组合不重复,只需要每次独立随机,两次抽到同样的东西很正常,因为我们用的 Math.random 在不同时间戳下种子不同。

但如果今天特别不想吃某样东西呢?这就是“排除项”的由来。我们在抽取之前,先把用户排除掉的食物从对应类别的数组里过滤掉,生成一个新的候选数组,然后再抽。这样既保留了随机性,又照顾了口味的个性化。

二、食物库怎么设计——分门别类,留好扩展口

我们的食物数据不需要联网,直接写死在代码里。为了方便管理和扩展,我把数据按类别分开,每一类用一个字符串数组存储。主食包括米饭、馒头、面条、饺子、包子等;肉类包括猪肉、鸡肉、牛肉、鱼、虾;蔬菜包括白菜、菠菜、西红柿、黄瓜、豆芽、香菜等等。这些食物都是日常外卖里常见的,用户一看就有亲切感。

排除项不能太多,否则某个类别可能被用户全排除了,导致无法抽取。为了避免程序崩溃,我在抽取前会检查候选数组是否为空。如果某类别的食物全部被排除,就显示一个提示,比如“主食全部被排除了,请至少保留一项”,并让程序优雅降级,不抽那个类别。这次搭配视为不完整,但不会闪退。

数据结构用 @State 修饰的数组:staplesmeatsvegetables,另外还有三个布尔数组来记录每个食物是否被排除,比如 excludeStaples: boolean[],长度和对应食物数组一致。为了界面简单,我用 Toggle 组件切换每一项的排除状态。

排除逻辑在抽取时执行:根据 excludeStaples 数组,过滤 staples 得到一个新的可选数组,然后对新数组执行随机抽取。如果可选数组长度为 0,就直接把结果设为“已全部排除”,并在颜色上标红提示。

三、界面设计——三列排开,一目了然

我想让这个工具界面像一张菜单卡,左边是主食,中间是肉,右边是蔬菜。每个类别下面是食物列表,每项前面有个 Toggle,默认打开。底部一个大的“抽签”按钮,点一下,三个类别的结果就显示在各自列表的顶部,用大字体展示。这样用户可以同时调整排除项和查看结果。

具体布局:用三个 Column 并排放在 Row 里,每个 Column 包含类别标题、抽签结果展示、食物列表(带排除开关)。结果用 Text 显示,如果该类被全排除,显示红色“无可用”。

颜色上,主食淡黄底、肉类淡红底、蔬菜淡绿底,区分明显。按钮用蓝色圆角胶囊。整体色调柔和,不刺眼,适合午间刷手机用。

为了代码清晰,我把抽取逻辑写成一个 randomPick 函数,传入原数组和排除数组,返回结果字符串。三个类别分别调用三次,一次性更新三个结果变量,实现同步刷新。

四、完整代码——所有食材和逻辑都在一个文件里

以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,替换 entry/src/main/ets/pages/Index.ets。无需任何权限,纯本地逻辑。

/*
 * 营养搭配抽签 —— 午餐搭配随机生成
 * 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
 */

@Entry
@Component
struct Index {
  // 主食数据
  @State staples: string[] = ['米饭', '馒头', '面条', '饺子', '包子', '烧饼'];
  @State excludeStaples: boolean[] = [false, false, false, false, false, false];
  @State stapleResult: string = '?';

  // 肉类数据
  @State meats: string[] = ['猪肉', '鸡肉', '牛肉', '鱼肉', '虾仁', '排骨'];
  @State excludeMeats: boolean[] = [false, false, false, false, false, false];
  @State meatResult: string = '?';

  // 蔬菜数据
  @State vegetables: string[] = ['白菜', '菠菜', '西红柿', '黄瓜', '豆芽', '香菜', '西兰花'];
  @State excludeVegs: boolean[] = [false, false, false, false, false, false, false];
  @State vegResult: string = '?';

  // 随机抽取函数
  private randomPick(items: string[], excludes: boolean[]): string {
    let available: string[] = [];
    for (let i = 0; i < items.length; i++) {
      if (!excludes[i]) {
        available.push(items[i]);
      }
    }
    if (available.length === 0) {
      return '已全部排除';
    }
    let idx = Math.floor(Math.random() * available.length);
    return available[idx];
  }

  // 执行抽签
  private draw(): void {
    this.stapleResult = this.randomPick(this.staples, this.excludeStaples);
    this.meatResult = this.randomPick(this.meats, this.excludeMeats);
    this.vegResult = this.randomPick(this.vegetables, this.excludeVegs);
  }

  // 重置所有排除
  private resetExcludes(): void {
    this.excludeStaples = new Array(this.staples.length).fill(false);
    this.excludeMeats = new Array(this.meats.length).fill(false);
    this.excludeVegs = new Array(this.vegetables.length).fill(false);
    this.stapleResult = '?';
    this.meatResult = '?';
    this.vegResult = '?';
  }

  build() {
    Column() {
      Text('午餐搭配抽签')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 8 })

      Text('排除不想吃的,再点抽签')
        .fontSize(15)
        .fontColor('#888')
        .margin({ bottom: 15 })

      // 三类食材
      Row() {
        // 主食
        Column() {
          Text('主食')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 8 })
          Text(this.stapleResult)
            .fontSize(22)
            .fontColor(this.stapleResult === '已全部排除' ? '#F44336' : '#333')
            .height(36)
            .margin({ bottom: 6 })
          ForEach(this.staples, (item: string, idx: number) => {
            Row() {
              Toggle({ type: ToggleType.Switch, isOn: !this.excludeStaples[idx] })
                .onChange((value: boolean) => {
                  this.excludeStaples[idx] = !value;
                })
                .width(40)
              Text(item).fontSize(14).margin({ left: 6 })
            }
            .margin({ bottom: 4 })
          })
        }
        .layoutWeight(1)
        .padding(10)
        .backgroundColor('#FFF8E1')
        .borderRadius(8)

        // 肉类
        Column() {
          Text('肉类')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 8 })
          Text(this.meatResult)
            .fontSize(22)
            .fontColor(this.meatResult === '已全部排除' ? '#F44336' : '#333')
            .height(36)
            .margin({ bottom: 6 })
          ForEach(this.meats, (item: string, idx: number) => {
            Row() {
              Toggle({ type: ToggleType.Switch, isOn: !this.excludeMeats[idx] })
                .onChange((value: boolean) => {
                  this.excludeMeats[idx] = !value;
                })
                .width(40)
              Text(item).fontSize(14).margin({ left: 6 })
            }
            .margin({ bottom: 4 })
          })
        }
        .layoutWeight(1)
        .padding(10)
        .backgroundColor('#FFEBEE')
        .borderRadius(8)
        .margin({ left: 6 })

        // 蔬菜
        Column() {
          Text('蔬菜')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .margin({ bottom: 8 })
          Text(this.vegResult)
            .fontSize(22)
            .fontColor(this.vegResult === '已全部排除' ? '#F44336' : '#333')
            .height(36)
            .margin({ bottom: 6 })
          ForEach(this.vegetables, (item: string, idx: number) => {
            Row() {
              Toggle({ type: ToggleType.Switch, isOn: !this.excludeVegs[idx] })
                .onChange((value: boolean) => {
                  this.excludeVegs[idx] = !value;
                })
                .width(40)
              Text(item).fontSize(14).margin({ left: 6 })
            }
            .margin({ bottom: 4 })
          })
        }
        .layoutWeight(1)
        .padding(10)
        .backgroundColor('#E8F5E9')
        .borderRadius(8)
        .margin({ left: 6 })
      }
      .width('96%')
      .margin({ bottom: 15 })

      // 操作按钮
      Row() {
        Button('抽签')
          .type(ButtonType.Capsule)
          .backgroundColor('#1976D2')
          .fontColor(Color.White)
          .fontSize(20)
          .layoutWeight(1)
          .onClick(() => { this.draw(); })

        Button('重置排除')
          .type(ButtonType.Capsule)
          .backgroundColor('#EEEEEE')
          .fontColor('#333')
          .fontSize(16)
          .layoutWeight(1)
          .margin({ left: 10 })
          .onClick(() => { this.resetExcludes(); })
      }
      .width('90%')
      .margin({ bottom: 15 })

      Text('💡 使用 Math.random 随机生成搭配,排除项即时生效')
        .fontSize(12)
        .fontColor('#AAA')
        .width('90%')
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FAFAFA')
  }
}

代码把食物数据、排除状态和抽取逻辑全部放在一个组件里。randomPick 函数处理过滤和随机挑选,如果某类全部被排除,就返回带颜色的提示文本。界面使用三列并排,每列内用 ForEach 渲染开关和食物名称。抽签和重置按钮放在底部,操作路径极短。

运行效果

代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。屏幕上方出现三列颜色不同的卡片:黄底主食、红底肉类、绿底蔬菜。每列顶部显示一个问号,下面是带开关的食物列表,全部默认开启。点“抽签”按钮,三个问号同时变成具体食物,比如“馒头”“鸡肉”“菠菜”。不想吃香菜?把蔬菜列里“香菜”的开关关掉,再抽签,香菜永远不会出现。如果不小心把所有蔬菜都排除了,蔬菜结果会显示红色“已全部排除”,提醒你至少留一样。点“重置排除”,所有开关恢复开启,问号复位。整个过程交互流畅,抽签瞬间完成,是那种“纠结了就点一下”的轻松工具。

总结

这个小小的午餐抽签器,里面其实揉进了几个写应用时经常要用到的技能:

  • 随机数与组合决策Math.random 配合数组索引实现等概率随机选择,是抽奖、摇号、推荐算法的基础。
  • 动态过滤与容错处理:通过排除数组动态生成候选列表,并在列表为空时给出优雅提示,避免程序报错。
  • ArkUI 的列表与开关组件Toggle 的开关状态与 @State 数组联动,ForEach 循环渲染可交互列表,实现了多选项的独立控制。
  • 组件化布局思维:把功能拆成可复用的 randomPick 函数,把 UI 拆成三列卡片,代码清晰易维护。

如果以后想升级,可以把食物库换成从 Preferences 读取,让用户自己添加爱吃的东西,或者给每道菜配上图片。但就解决“今天中午吃什么”这个难题来说,这个小工具已经足够了——点一下,命运帮你决定,你只管去吃。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐