微搭低代码AI组件单词消消乐从0到1实践

目录

  • [1 为什么要开发单词消消乐](#1 为什么要开发单词消消乐)
  • [2 需要具备什么功能](#2 需要具备什么功能)
  • [3 采用什么技术方案实现](#3 采用什么技术方案实现)
  • [4 逻辑设计](#4 逻辑设计)
    • [4.1 数据结构设计](#4.1 数据结构设计)
    • [4.2 游戏的核心逻辑](#4.2 游戏的核心逻辑)
    • [4.3 数据设计](#4.3 数据设计)
  • [5 代码详解](#5 代码详解)
    • [5.1 导入依赖](#5.1 导入依赖)
    • [5.2 定义函数组件](#5.2 定义函数组件)
    • [5.3 数据初始化](#5.3 数据初始化)
    • [5.4 状态定义](#5.4 状态定义)
    • [5.5 打乱解释的逻辑](#5.5 打乱解释的逻辑)
    • [5.6 定义选择单词的函数](#5.6 定义选择单词的函数)
    • [5.7 定义选择解释的函数](#5.7 定义选择解释的函数)
    • [5.8 界面渲染](#5.8 界面渲染)
  • [6 如何使用这个代码](#6 如何使用这个代码)
  • 总结

周末线上观看了腾讯TVP技术分享交流,产品经理现场演示了使用新的云开发Copilot写一个抽奖的功能。看完后大受启发,于是我也现学现卖,做一个类似的组件。

单词消消乐小游戏教程

1 为什么要开发单词消消乐

在在线英语单词的APP或者小程序中,有很多学习单词的场景。教培机构为了让学员体会到学习的乐趣,通常会引入游戏的元素。那么单词消消乐就是一种趣味性的工具。

低龄孩子在消消乐的过程中,既记住了单词又有耐心把学习进行完毕。

2 需要具备什么功能

  • 屏幕左侧显示一组英文单词,右侧显示一组中文解释。
  • 玩家需要点击英文单词和其对应的中文解释,如果配对正确,单词和解释会从屏幕中消失。
  • 当所有单词和解释都匹配完成后,显示"游戏结束",并提供重新开始的选项。

3 采用什么技术方案实现

单词消消乐总体算是复杂逻辑了,如果我们使用基本的组件,比如普通容器、文本、按钮这些搭建。不是说不可以,但是问题是要考虑副作用的问题。

微搭总体是使用react组件设计的,简单场景没问题,但是复杂场景,尤其在用户快速点击的时候,就会有奇奇怪怪的问题。比如我的单词明明选中了,要消掉,但是还留着。这种就大大影响用户的体验了。

过去这种我们只能是将就用,但是新版本提供了AI代码块的组件,支持你用React的语法实现自己想要的功能,就比较方便了。

4 逻辑设计

要想设计一款比较好玩的游戏,逻辑是非常重要的,逻辑想明白了,代码只是逻辑的实现罢了。有的人就感觉技术非常高级,技术本身是为业务服务的,企业最终是通过技术来实现自己的商业目标。

TVP的一位嘉宾的观点我非常认同,系统的价值在于给企业带没带来效率的提升,成本的降低,给没给用户的体验带来提升。那我们在做程序设计的时候也需要把用户体验放在首位进行考虑。

4.1 数据结构设计

游戏的核心是管理一组单词和对应的解释,因此我们用数组来存储这些数据。

每个单词和解释用一个对象表示,包含两个属性:

  • word:英文单词。
  • definition:对应的中文解释。

实际的数据结构是长这样的

bash 复制代码
[
  { word: "apple", definition: "苹果" },
  { word: "banana", definition: "香蕉" }
]

4.2 游戏的核心逻辑

1.初始化游戏:

  • 提供一组单词和解释。
  • 打乱右侧的中文解释顺序,增加挑战性。

2.点击事件:

  • 玩家点击一个英文单词后,再点击一个中文解释。
  • 判断二者是否匹配,匹配成功:将这对单词和解释从屏幕移除。匹配失败:重置选择,提示玩家重新尝试。

3.游戏结束:

  • 当所有单词都匹配完成,显示"重新开始"的按钮。

4.重新开始:

  • 恢复初始状态,重新打乱解释。

4.3 数据设计

我们的组件需要设计一些数据来跟踪用户的点击,我们考虑如下数据

  • pairs:剩余的单词和解释。
  • shuffledDefinitions:打乱后的解释。
  • selectedWord 和 selectedDefinition:当前被选中的单词和解释。
  • matchedPairs:已匹配成功的单词和解释。

5 代码详解

完整代码如下:

bash 复制代码
import React, { useState, useEffect } from 'react';
import { Button, CardContent, CardTitle } from '@/components/ui';

// 打乱数组顺序的函数
const shuffle = (array) => {
  return array.sort(() => Math.random() - 0.5);
};

export default function WordMatchGame() {
  const initialPairs = $w.page.dataset.state.words

  const [pairs, setPairs] = useState(initialPairs); // 存储单词和解释的数组
  const [shuffledDefinitions, setShuffledDefinitions] = useState([]); // 存储打乱后的中文解释
  const [selectedWord, setSelectedWord] = useState(null); // 当前选中的单词
  const [selectedDefinition, setSelectedDefinition] = useState(null); // 当前选中的解释
  const [matchedPairs, setMatchedPairs] = useState([]); // 匹配成功的单词和解释

  // 打乱解释的顺序
  useEffect(() => {
    const shuffled = shuffle(pairs.map(pair => pair.definition)); // 仅打乱中文解释
    setShuffledDefinitions(shuffled);
  }, [pairs]); // 当 pairs 改变时重新打乱解释

  // 选择单词
  const selectWord = (word) => {
    if (matchedPairs.some(pair => pair.word === word)) return; // 如果该单词已匹配成功,则不能再次选择
    console.log(`选择单词: ${word}`);  // 输出选中的单词
    setSelectedWord(word);
  };

  // 选择解释
  const selectDefinition = (definition) => {
    console.log(`选择解释: ${definition}`);  // 输出选中的解释
    setSelectedDefinition(definition);

    // 找到选中的单词的正确解释
    const correctDefinition = pairs.find(pair => pair.word === selectedWord)?.definition;

    if (correctDefinition && correctDefinition === definition) {
      // 如果匹配成功,消除配对的单词和解释
      console.log('匹配成功!'); // 输出匹配成功的日志
      setMatchedPairs(prev => [...prev, { word: selectedWord, definition }]);
      
      // 移除已匹配的单词和解释
      setPairs(prevPairs =>
        prevPairs.filter(pair => pair.word !== selectedWord && pair.definition !== definition)
      );

      // 重置选中的单词和解释
      setSelectedWord(null);
      setSelectedDefinition(null);
    } else {
      console.log('匹配失败!'); // 输出匹配失败的日志
      // 如果匹配失败,重置选中的单词和解释
      setTimeout(() => {
        setSelectedWord(null);
        setSelectedDefinition(null);
      }, 500); // 延迟恢复,避免闪烁
    }
  };

  // 渲染单词和解释列
  const renderColumn = (column, isWordColumn) => {
    return column.map((item, index) => {
      // 根据是否选中单词来改变样式
      const isSelected = isWordColumn ? selectedWord === item.word : selectedDefinition === item;
      const selectedClass = isSelected ? 'bg-green-300' : '';  // 选中时背景为绿色

      return (
        <div
          key={index}
          className={`cursor-pointer p-2 mb-2 bg-white rounded-lg shadow-md ${isWordColumn ? 'text-left' : 'text-right'} ${selectedClass}`}
          onClick={() => isWordColumn ? selectWord(item.word) : selectDefinition(item)}
        >
          {isWordColumn ? item.word : item}
        </div>
      );
    });
  };

  // 判断是否所有的单词和解释都已匹配成功
  const isGameOver = pairs.length === 0;

  // 重新开始游戏
  const restartGame = () => {
    setPairs(initialPairs); // 重置 pairs 数组
    setMatchedPairs([]); // 清空匹配记录
    setShuffledDefinitions(shuffle(initialPairs.map(pair => pair.definition))); // 重新打乱解释顺序
    setSelectedWord(null); // 清空选中的单词
    setSelectedDefinition(null); // 清空选中的解释
  };

  return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-r from-green-400 via-blue-500 to-purple-500 relative">
      <div className="text-white text-4xl font-extrabold mb-8">单词对对碰游戏</div>

      {/* 游戏列 */}
      <div className="flex justify-center space-x-12">
        <div className="flex flex-col space-y-2">
          <CardTitle className="text-white font-bold text-xl mb-4">英文单词</CardTitle>
          {pairs.map((pair, index) => (
            <div
              key={index}
              className={`cursor-pointer p-2 mb-2 bg-white rounded-lg shadow-md text-left ${selectedWord === pair.word ? 'bg-green-300' : ''}`}
              onClick={() => selectWord(pair.word)}
            >
              {pair.word}
            </div>
          ))}
        </div>
        <div className="flex flex-col space-y-2">
          <CardTitle className="text-white font-bold text-xl mb-4">中文解释</CardTitle>
          {renderColumn(shuffledDefinitions, false)} {/* 渲染打乱后的中文解释列 */}
        </div>
      </div>

      {/* 匹配成功的信息 */}
      {matchedPairs.length > 0 && (
        <div className="mt-6 text-white text-xl">
          <CardContent>
            <CardTitle>已匹配:</CardTitle>
            <ul>
              {matchedPairs.map((pair, index) => (
                <li key={index}>{pair.word} - {pair.definition}</li>
              ))}
            </ul>
          </CardContent>
        </div>
      )}

      {/* 重新开始按钮 */}
      {isGameOver && (
        <Button onClick={restartGame} className="mt-6 bg-green-500 text-white px-6 py-2 rounded-md shadow-lg">
          重新开始
        </Button>
      )}
    </div>
  );
}

5.1 导入依赖

bash 复制代码
import React, { useState, useEffect } from 'react';
import { Button, CardContent, CardTitle } from '@/components/ui';

一开头,有两条导入语句,这部分相当于是要引入已经封装好的第三方库。

先导入了React框架

  • React 是一个前端框架,用来构建用户界面。
  • 每个 React 组件需要 React 提供的一些功能,比如状态管理(useState)、生命周期管理(useEffect)等。

这个就相当于我们可以直接用方法名调用它封装好的方法,但是也必须要强制符合他的方法定义的要求,不能自由发挥。开源的东西看似好用,其实是引入了复杂性,遇到问题就比较费时间。

之后导入了导入 useState 和 useEffect

  • useState:用来定义和管理组件的状态。
  • useEffect:用来定义组件的副作用逻辑,比如初始化数据、监听事件等。

导入 UI 组件

  • Button、CardContent、CardTitle 是从一个自定义的 UI 库中引入的现成组件,用来实现按钮、卡片标题等界面元素。

5.2 定义函数组件

bash 复制代码
export default function WordMatchGame() {

这是一个 React 函数组件,在 React 中,所有的界面逻辑都定义在组件里。WordMatchGame 是这个组件的名字,表示这是一个"单词配对游戏"的组件。

export default 的作用:它表示将 WordMatchGame 组件作为模块导出,方便其他地方引入使用。

5.3 数据初始化

bash 复制代码
const initialPairs = $w.page.dataset.state.words;

这一行是做什么的?

定义了一个变量 initialPairs,它存储了游戏初始的单词和解释配对数据。

数据来源是 $w.page.dataset.state.words,表示从页面的数据集里获取单词和解释。

在JSX组件中是可以引用我们微搭中定义的各种自定义变量的,在代码区我们可以创建一个数组变量,用来初始化数据

默认值是

bash 复制代码
[
  {
    "word": "apple",
    "definition": "苹果"
  },
  {
    "word": "dog",
    "definition": "狗"
  },
  {
    "word": "car",
    "definition": "汽车"
  },
  {
    "word": "sun",
    "definition": "太阳"
  },
  {
    "word": "tree",
    "definition": "树"
  },
  {
    "word": "house",
    "definition": "房子"
  }
]

5.4 状态定义

bash 复制代码
const [pairs, setPairs] = useState(initialPairs);
const [shuffledDefinitions, setShuffledDefinitions] = useState([]);
const [selectedWord, setSelectedWord] = useState(null);
const [selectedDefinition, setSelectedDefinition] = useState(null);
const [matchedPairs, setMatchedPairs] = useState([]);

使用 React 的 useState 定义了 5 个状态,分别是:

  1. pairs:游戏的核心数据(单词和解释配对)。
  2. shuffledDefinitions:打乱后的中文解释,用来显示在界面上。
  3. selectedWord:当前选中的英文单词。
  4. selectedDefinition:当前选中的中文解释。
  5. matchedPairs:匹配成功的单词和解释。

每个状态都有两个变量,pairs 是状态的值;setPairs 是修改状态的函数。

用中括号定义是React强制的语法,React 提供了一个叫做 useState 的 Hook,用来定义和管理组件的状态。

语法说明:

bash 复制代码
const [状态变量, 修改状态的函数] = useState(初始值);
  • 状态变量:存储数据的地方(如 pairs)。
  • 修改状态的函数:用来更新状态,React 会自动重新渲染界面(如 setPairs)。
  • 初始值:状态的初始数据,比如 initialPairs。

5.5 打乱解释的逻辑

bash 复制代码
useEffect(() => {
  const shuffled = shuffle(pairs.map(pair => pair.definition));
  setShuffledDefinitions(shuffled);
}, [pairs]);

useEffect 的作用:这个函数在组件渲染后运行。它监听 pairs 的变化,只要 pairs 更新,就会重新执行里面的代码。

具体逻辑:pairs.map(pair => pair.definition) 提取所有中文解释。调用 shuffle 函数打乱解释的顺序。将打乱后的结果保存到状态 shuffledDefinitions。

设计这个的初衷是,如果不把解释打乱,用户就知道你的答案是一一对应的,也就失去了学习的意义。

5.6 定义选择单词的函数

bash 复制代码
const selectWord = (word) => {
  if (matchedPairs.some(pair => pair.word === word)) return;
  setSelectedWord(word);
};

这段代码是干什么的?

  1. 定义了一个函数 selectWord,用来处理"点击单词"的操作。
  2. 如果这个单词已经匹配成功(在 matchedPairs 里),直接返回。否则,将当前选中的单词存储到 selectedWord 状态。

5.7 定义选择解释的函数

bash 复制代码
const selectDefinition = (definition) => {
  setSelectedDefinition(definition);

  const correctDefinition = pairs.find(pair => pair.word === selectedWord)?.definition;

  if (correctDefinition && correctDefinition === definition) {
    setMatchedPairs(prev => [...prev, { word: selectedWord, definition }]);
    setPairs(prev => prev.filter(pair => pair.word !== selectedWord));
  } else {
    setTimeout(() => {
      setSelectedWord(null);
      setSelectedDefinition(null);
    }, 500);
  }
};

这段代码是干什么的?

  1. 定义了一个函数 selectDefinition,用来处理"点击解释"的操作。
  2. 将当前选中的解释存储到 selectedDefinition 状态。查找当前选中的单词的正确解释(correctDefinition)。判断是否匹配成功:成功:更新匹配记录,并从未匹配的单词中移除该配对。失败:延迟清空选中的单词和解释。

5.8 界面渲染

bash 复制代码
return (
    <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-r from-green-400 via-blue-500 to-purple-500 relative">
      <div className="text-white text-4xl font-extrabold mb-8">单词对对碰游戏</div>

      {/* 游戏列 */}
      <div className="flex justify-center space-x-12">
        <div className="flex flex-col space-y-2">
          <CardTitle className="text-white font-bold text-xl mb-4">英文单词</CardTitle>
          {pairs.map((pair, index) => (
            <div
              key={index}
              className={`cursor-pointer p-2 mb-2 bg-white rounded-lg shadow-md text-left ${selectedWord === pair.word ? 'bg-green-300' : ''}`}
              onClick={() => selectWord(pair.word)}
            >
              {pair.word}
            </div>
          ))}
        </div>
        <div className="flex flex-col space-y-2">
          <CardTitle className="text-white font-bold text-xl mb-4">中文解释</CardTitle>
          {renderColumn(shuffledDefinitions, false)} {/* 渲染打乱后的中文解释列 */}
        </div>
      </div>

      {/* 匹配成功的信息 */}
      {matchedPairs.length > 0 && (
        <div className="mt-6 text-white text-xl">
          <CardContent>
            <CardTitle>已匹配:</CardTitle>
            <ul>
              {matchedPairs.map((pair, index) => (
                <li key={index}>{pair.word} - {pair.definition}</li>
              ))}
            </ul>
          </CardContent>
        </div>
      )}

      {/* 重新开始按钮 */}
      {isGameOver && (
        <Button onClick={restartGame} className="mt-6 bg-green-500 text-white px-6 py-2 rounded-md shadow-lg">
          重新开始
        </Button>
      )}
    </div>
  );

这里其实就需要懂HTML和CSS了,这里的CSS是引入了Tailwind CSS,我发现微搭的技术栈里这个是可以使用的,用样式库自带的样式搭建界面还是非常快的

这里唯一需要注意的就是{}的使用,他的作用类似于表达式,凡是你要用表达式的就得用{}包裹起来

6 如何使用这个代码

上边代码要想看的懂,需要你学习React、HTML、CSS、JavaScript,学习过程还是比较艰辛的。但是代码的一个最大的特点是可以拷贝粘贴,我们使用我们的cv大法看如何使用。

在应用中创建一个页面

输入页面的名称

加入我们的AI代码块组件

点击编辑JSX代码

将教程的完整代码贴入编辑器中

在代码区创建一个数组变量words


然后设置默认值

bash 复制代码
[
  {
    "word": "apple",
    "definition": "苹果"
  },
  {
    "word": "dog",
    "definition": "狗"
  },
  {
    "word": "car",
    "definition": "汽车"
  },
  {
    "word": "sun",
    "definition": "太阳"
  },
  {
    "word": "tree",
    "definition": "树"
  },
  {
    "word": "house",
    "definition": "房子"
  }
]

我们的效果就有了

点击预览就大功告成啦

总结

我们本篇介绍了微搭的新能力,AI组件的具体用法。结合一个实战案例单词消消乐,详细讲解了游戏的功能设计、逻辑设计以及代码逻辑。对于复杂需求,借助纯代码也是一种可行解。但是纯代码技术难度高,调错复杂,未来也许AI的能力提高之后这部分就不是太复杂的一件事。

相关推荐
卷叶小树5 小时前
低代码接入外部数据能力:DataSource Runtime 的设计
低代码
Jeking2176 小时前
低代码平台表单设计器 unione form editor 布局组件 —— 向导布局
低代码·动态表单·表单设计·表单引擎·unione cloud
踩着两条虫9 小时前
VTJ.PRO 开源 AI 低代码引擎深度评测大纲
前端·低代码·开源软件
Jeking2171 天前
低代码平台表单设计器 unione form editor 组件 —— 富文本编辑器
低代码·动态表单·表单设计·表单引擎·unione cloud
多租户观察室2 天前
中小微企业适用低代码开发平台有哪些选型
低代码
数睿数据无代码开发2 天前
2026 无代码平台企业选型推荐
低代码·无代码
咬人喵喵2 天前
E2编辑器里的零高容器是什么?怎么用?
低代码·微信·编辑器·交互·svg
Jeking2173 天前
低代码平台表单设计器 unione form editor 布局组件 — 折叠面板
低代码·动态表单·表单设计·表单引擎·unione cloud
低代码行业资讯3 天前
五大实锤证据:AI不会终结低代码,只会倒逼技术进化
低代码·ai
Teable任意门互动3 天前
深度解析:AI 赋能开源多维表格,实现企业全场景数据整合与高效应用
数据库·人工智能·低代码·信息可视化·开源·数据库开发