FlutterFlame游戏实践#08 | 打砖块 -关卡设计


theme: cyanosis

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter\&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]\ 第二季:从休闲游戏实践,进阶 Flutter\&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》 或 【github 项目首页】 查看。


一、关卡数据的设计

上一篇我们实现了打砖块游戏的基本玩法,目前砖块只是简单的堆砌,本章我们将着专注于游戏关卡内容的设计:

image.png


1. 砖块的显隐

首先看一下,如何决定砖块的显示和隐藏,如下所示,想要通过数据,将想要去除的砖块移除。从而达到关卡的可设计性:

image.png

我们可以通过一个二维的点阵,来决定砖块展示的样式。如下所示, 1 代表展示砖块, 0 代表不展示砖块:

dart List<List<int>> tiles = [ [1,1,1,1,1,1,1,1,1], [1,1,1,1,1,1,1,1,1], [0,1,0,0,1,0,0,1,0], [0,1,0,0,1,0,0,1,0], [1,1,1,1,1,1,1,1,1], [1,1,1,1,1,1,1,1,1], ];

这样,我们就不需要在 BrickManager 通过行列来添加砖块,可以解析 tiles 数据来完成:

dart ---->[lib/bricks/05/heroes/bricks.dart]---- List<Brick> _createBricks() { List<Brick> bricks = []; for (int i = 0; i < tiles.length; i++) { List<int> rows = tiles[i]; for (int j = 0; j < rows.length; j++) { if (rows[j] == 1) { Brick brick = Brick(j + tiles.length * i); brick.x = 64.0 * j; brick.y = 32.0 * i; bricks.add(brick); } } } return bricks; }


2. 关卡数据的初步设计

对于关卡功能来说,目前希望每关卡可以指定砖块的排列方式、小球运行的速度两个参数。如下所示,定义一个 Level 类用于维护一个关卡中的数据:

```dart class Level { final int id; final List > tiles; final double ballSpeed;

const Level({ required this.id, required this.tiles, required this.ballSpeed, }); } ```


这样我们只要准备关卡的相关的数据,就可以实现不同的关卡。选关时只有加载对应关卡的数据,重新开始游戏即可。效果如下:

| 第一关 | 第二关 | | --- | --- | | image.png | image.png |

目前简单起见,先将关卡的数据通过产量维护在内存中。如下所示是两个关卡的数据:

dart ---->[lib/bricks/05/model/level.dart]---- const Map<int, Level> kLevels = { 1: Level( id: 1, tiles: [ [1, 0, 0, 1, 0, 1, 0, 0, 1], [0, 1, 0, 1, 1, 1, 0, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 1, 0, 0, 1, 0, 0, 1, 0], [0, 1, 0, 0, 1, 0, 0, 1, 0], [1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1], [0, 1, 0, 1, 1, 1, 0, 1, 0], [1, 0, 0, 1, 0, 1, 0, 0, 1], ], ballSpeed: 350, ), 2: Level( id: 2, tiles: [ [1, 1, 1, 1, 0, 1, 1, 1, 1], [1, 1, 1, 1, 0, 1, 1, 1, 1], [1, 1, 1, 1, 0, 1, 1, 1, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 0, 0, 1], [0, 1, 1, 0, 0, 1, 1, 1, 1], [0, 1, 1, 0, 0, 1, 1, 1, 1], [0, 1, 1, 0, 0, 1, 1, 1, 1], ], ballSpeed: 360, ), };


3. 代码中对关卡的维护

游戏界面中的关卡数时游戏过程中的状态数据,目前先在 BricksGame 中维护,方便其他地方进行访问。 levelNum 的 set 方法可以更新 _levelNum 的值,并重新开始游戏;nextLever 方法将当前关卡数 +1,方面外界调用,进入下一关:

```dart ---->[lib/bricks/05/bricks_game.dart]---- int _levelNum = 1;

Level get level => kLevels[_levelNum]!;

set levelNum(int level) { if (_levelNum != level) { _levelNum = level.clamp(1, kLevels.length); // 设置关卡时,重置砖块管理器 restart(); } }

void nextLever() => levelNum = _levelNum + 1; ```

砖块管理器根据 game 中的激活关卡 level 对象中的数据,构建砖块即可:

image.png

游戏界面中的关卡文字也是同理。可以看出,对于 Flame 而言,游戏状态数据的管理,最简单直接的方式就是放入到游戏主类中。在下层的构件中,可以通过 game 实例来访问或修改数据:

image.png


此时当某一关卡的砖块被全部击碎,可以通过按钮触发 下一关卡

| 关卡通过 | 进入下一关 | | --- | --- | | image.png | image.png |

在代码中,点击事件只需要移除 GameSuccessMenu 弹出菜单,并触发 game.nextLever 即可切换到下一关卡。

dart void toNextLevel(){ widget.game.am.play(SoundEffect.uiClose); widget.game.overlays.remove('GameSuccessMenu'); widget.game.nextLever(); }


二、实现游戏选关

既然支持有关卡的,那么选择关卡的功能自然要安排上。上一篇在主页中添加了 选择关卡 的按钮,接下来实现一下右图所示的关卡选择功能。其中未通关的关卡需要锁定,禁止进入:

| 游戏主界面 | | | --- | --- | | image.png | image.png |


1. 关卡界面 LevelPage

这里关卡界面也是作为一个浮层进行维护的,其中界面布局是 Flutter 原生组件。通过 LevelPage 进行展示:

image.png

关卡选择界面是一个可滑动的网格布局,可以使用 GridView 组件来实现。其中封装 LevelItem 组件,负责展示单体的关卡按钮。组件需要的数据有 关卡名是否锁定 以及点击事件回调函数:

image.png

```dart --->[lib/bricks/05/overlays/leverpage/levelitem.dart]---- class LevelItem extends StatelessWidget { final bool locked; final ValueChanged onTapItem; final int level;

const LevelItem({ super.key, this.locked = true, required this.level, required this.onTapItem, }); ```

在构建逻辑中,可以根据 locked 数据决定不同的界面表现。比如在构造时传入 locked 为 true 时,使用小锁的图标,否则使用文字:

image.png

另外,锁定的关卡呈灰色,这里并没有单独使用一张灰色图片。而是使用 ColorFiltered 组件添加灰色的滤色效果。如果锁定的话直接返回,否则才嵌套 GestureDetector 处理点击事件:

image.png

```dart const ColorFilter greyscale = ColorFilter.matrix( [ 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0.2126, 0.7152, 0.0722, 0, 0, 0, 0, 0, 1, 0, ]);

```

然后通过 GridView 构建滑动的网格布局,这里先放置 30 个关卡,激活前 10 个来测试展示效果。后面会结合实际数据来处理:

image.png

点击时,触发 onSelectLevel 选择关卡进入。只要为 game.levelNum 设置值即可:

dart void onSelectLevel(int level) { game.overlays.remove("LevelPage"); game.levelNum = level; game.overlays.remove("HomePage"); }


2.关卡数据的随机生成

每一个关卡理论上来说应该精心设计砖块的排布数据,但这里为了节约时间,通过写一个数据解析器,通过随机数来自动生成砖块排布的数据。每个关卡中:

  • [1]. 砖块宽高 9*10
  • [2]. 随机生成的砖块排布左右堆成。
  • [3]. 随机生成的最终结果以 json 的形式存储为关卡数据文件。在游戏初始化时加载数据。

: 关卡生成器代码是独立于游戏之外的存在,只是在开发过程中辅助创建数据。


如下所示,每一行通过 formLine 方法封装,由于左右对称。可以先生成一半的数据,然后把另一半反向放入列表尾部。

image.png

```dart Random random = Random();

List formLine(int count) { List line = []; int half = count ~/ 2; for (int i = 0; i <= half; i++) { line.add(random.nextInt(2)); } line.addAll(line.sublist(0,half).reversed); return line; } ```

上面的数据就对应着游戏关卡中的一行:

image.png

一共有 10 行数据,只要遍历生成 10 次 formLine 即可。这样就生成了随机的二维点阵:

image.png

```dart void main() { List > tile = [];

for (int i = 0; i < 10; i++) { tile.add(formLine(9)); } print(tile.join('\n')); } ```

将其作用于界面上,效果如下。这样我们就随机生成了一个关卡中的砖块布局:

image.png

然后可以进一步封装一个 gen 方法生成关卡数据。比如现在关卡的小球速度依次 + 10 :

dart Level gen(int index) { List<List<int>> tiles = []; for (int i = 0; i < 10; i++) { tiles.add(formLine(9)); } return Level( id: index, tiles: tiles, ballSpeed: 340 + 10.0 * index, ); }


3.随机关卡数据的存储和加载

下面代码中随机 30 关的数据,并将其通过 json.encode 编码为字符串实现序列化,存储到资源文件中:

image.png

dart void main() { List<Level> levels = []; for (int i = 0; i < 10; i++) { levels.add(gen(i+1)); } String data = json.encode(levels); String filePath = path.join(Directory.current.path,'assets','data','bricks_levels.json'); File(filePath).writeAsString(data); }

对象想要序列化成 json ,需要实现 toJson 的方法,根据成员创建 map 对象:

```dart class Level { /// 略同...

Map toJson() => { 'id': id, 'tiles': tiles, 'ballSpeed': ballSpeed, }; } ```


现在游戏中就可以抛弃掉之前临时打工的 kLevels 关卡映射。现在所有的关卡数据都集中在 bricks_levels.json 中。所以我们只需要在游戏主类中读取问价,反序列化解析成 Level 列表即可。之前当前 level 获取方式,可以修改为 _levels 列表根据激活关卡的索引获取:

```dart ---->[lib/bricks/05/bricksgame.dart]---- List _levels = []; Level get level => _levels[ levelNum-1];

Future loadLevels() async { String path = 'assets/data/bricks_levels.json'; String data = await rootBundle.loadString(path); List list = json.decode(data) as List; _levels = list.map(Level.fromMap).toList(); } ```

现在就可以真正地进行选关挑战了。目前关卡的解锁功能还没有实现,接下来一起来完成吧 ~

| 第3关 | 第10关 | | --- | --- | | image.png | image.png |


三、游戏中持久化数据维护

为了不让游戏退出后,解锁的关卡被重置,需要将解锁的最大关卡数被持久化存储。之前在小恐龙跳跃中已经介绍过使用 shared_preferences 数据持久化存储的方案。另外,像金币数、钻石数、音效的开启等配置项,也需要进行数据的持久化。


1. 游戏配置参数 GameConfig

这里先将游戏的配置参数统一交由 GameConfig 类进行维护,其中承载着目前需要的数据信息:

```dart ---->[lib/bricks/05/config/game_config.dart]---- class GameConfig { /// 最大解锁关卡数 final int maxUnLockLevel; // 绿水晶个数 final int blueCrystal; // 金币个数 final int coin; // 是否开启音效 final bool enableSoundEffect; // 是否开启背景音乐 final bool enableBgMusic;

GameConfig({ required this.maxUnLockLevel, required this.blueCrystal, required this.coin, required this.enableSoundEffect, required this.enableBgMusic, }); } ```


为 GameConfig 提供三个辅助方法,便于调用:

  • fromMap 方法: 通过 json 解析成的 Map 创建 GameConfig 实例,
  • toJson 方法 : 便于将 GameConfig 编码成字符串,用于持久化存储。
  • copyWith 方法: 便于通过已有的 GameConfig 对象,修改若干属性,创建新的对象。

``` factory GameConfig.fromMap(dynamic map) { return GameConfig( maxUnLockLevel: map['maxUnLockLevel'] ?? 1, blueCrystal: map['blueCrystal'] ?? 0, coin: map['coin'] ?? 0, enableSoundEffect: map['enableSoundEffect'] ?? true, enableBgMusic: map['enableBgMusic'] ?? true, ); }

Map toJson() => { 'maxUnLockLevel': maxUnLockLevel, 'blueCrystal': blueCrystal, 'coin': coin, 'enableSoundEffect': enableSoundEffect, 'enableBgMusic': enableBgMusic, };

GameConfig copyWith({ int? maxUnLockLevel, int? blueCrystal, int? coin, bool? enableSoundEffect, bool? enableBgMusic, }) => GameConfig( maxUnLockLevel: maxUnLockLevel ?? this.maxUnLockLevel, blueCrystal: blueCrystal ?? this.blueCrystal, coin: coin ?? this.coin, enableSoundEffect: enableSoundEffect ?? this.enableSoundEffect, enableBgMusic: enableBgMusic ?? this.enableBgMusic, ); ```


2.配置信息管理类

这里将配置信息的加载和存储通过 GameConfigManager 类进行维护,其中:

  • 持有 SharedPreferences 对象,并在构造方法中赋值。
  • 持有当前游戏的配置信息 config 对象,该对象通过 loadConfig 方法进行初始化。
  • 通过 saveConfig 方法,将 config 对象序列化成字符串,使用 sp 持久化存储.

```dart ---->[lib/bricks/05/config/game_config.dart]---- class GameConfigManager { static const _kConfigKey = 'bricks-game-config-key'; final SharedPreferences sp; late GameConfig config;

GameConfigManager(this.sp);

void loadConfig() { String data = sp.getString(_kConfigKey) ?? "{}"; config = GameConfig.fromMap(jsonDecode(data)); }

Future saveConfig() => sp.setString(_kConfigKey, jsonEncode(config)); } ```

另外基于 saveConfig 方法,可以提供一些修改某个配置项的方法,方便调用:

dart /// 解锁下一关 Future<void> unlockNextLevel() { config = config.copyWith(maxUnLockLevel: config.maxUnLockLevel + 1); return saveConfig(); } /// 增加绿水晶 Future<void> addBlueCrystal({int count = 1}) { config = config.copyWith(blueCrystal: config.blueCrystal + count); return saveConfig(); } /// 增加金币 Future<void> addCoin({int count = 1}) { config = config.copyWith(coin: config.coin + count); return saveConfig(); } /// 修改背景音乐是否激活 Future<void> changeEnableBgMusic(bool enable) { config = config.copyWith(enableBgMusic: enable); return saveConfig(); } /// 修改音效是否激活 Future<void> changeEnableSoundEffect(bool enable) { config = config.copyWith(enableSoundEffect: enable); return saveConfig(); }


3. 配置管理器的使用

这里让游戏主类持有 GameConfigManager 对象,方便访问,并在 onLoad 回调在进行创建。另外,声音的配置之前是在 AudioManager 中维护的,此时可以让其依赖 GameConfigManager ,获取配置信息:

image.png

```dart ---->[lib/bricks/05/bricks_game.dart]---- GameConfig get config => configManager.config;

late GameConfigManager configManager;

@override FutureOr onLoad() async { sp = await SharedPreferences.getInstance(); configManager = GameConfigManager(sp); configManager.loadConfig(sp); am = AudioManager(configManager); /// 略同... } ```

这样在相关的时机调用 GameConfigManager 方法,更新 config 配置对象即可,同时也会持久化存储到本地的 xml 中。比如当砖块全部被击碎时,可以触发 unlockNextLevel 解锁下一关,以及 addBlueCrystal 增加一个绿水晶。如下是存储的配置文件具体信息:

image.png

dart void checkSuccess() { if (brickManager.children.isEmpty) { game.am.play(SoundEffect.uiClose); game.overlays.add('GameSuccessMenu'); game.configManager.unlockNextLevel(); game.configManager.addBlueCrystal(); game.status = GameStatus.gameOver; } }


四。本章小结

本章我们实现了打砖块游戏的选关功能,进一步拓展了游戏玩法。并通过随机生成的方式制作出 30 个关卡。另外,也完成了一些配置数据的持久化存储的功能,这样配置信息就不会随着游戏结束而重置。

image.png

接下来,我们将进一步拓展游戏玩法,设计一些道具进一步增加游戏的可玩性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/521566.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SQL注入---POST注入

文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 一. POST提交概述 在Webshell文章中介绍过post提交和get提交的区别&#xff0c;这里不再赘述 post提交和get提交的区别&#xff1a; get方式提交URL中的参数信息&#xff0c;post方式则是将信…

【学习分享】小白写算法之选择排序篇

【学习分享】小白写算法之选择排序篇 前言一、什么是选择排序算法二、选择排序算法如何实现三、C语言实现算法四、复杂度计算五、算法稳定性六、小结 前言 简单排序有三种&#xff0c;冒泡排序&#xff0c;插入排序和选择排序。这三种排序的算法算是入门级别的&#xff0c;打好…

UART设计

一、UART通信简介 通用异步收发器&#xff0c; 特点&#xff1a;串行、异步、全双工通信 优点&#xff1a;通信线路简单&#xff0c;传输距离远 缺点&#xff1a;传输速度慢 数据传输速率&#xff1a;波特率&#xff08;单位&#xff1a;baud&#xff0c;波特&#xff09; …

解决IDEA下载mysql驱动太慢

下载驱动 下载页 解压后&#xff0c;提取**.jar**文件&#xff0c;放到一个目录下(你自己决定这个目录) 打开IDEA项目&#xff0c;点击右侧的数据库选项卡 在打开的页面&#xff0c;点击号 依次选择&#xff1a;数据源->MySQL 在弹出的页面&#xff0c;依次选择&#…

注解式 WebSocket - 构建 群聊、单聊 系统

目录 前言 注解式 WebSocket 构建聊天系统 群聊系统&#xff08;基本框架&#xff09; 群聊系统&#xff08;添加昵称&#xff09; 单聊系统 WebSocket 作用域下无法注入 Spring Bean 对象&#xff1f; 考虑离线消息 前言 很久之前&#xff0c;咱们聊过 WebSocket 编程式…

华为ensp中高级acl (控制列表) 原理和配置命令 (详解)

作者主页&#xff1a;点击&#xff01; ENSP专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年4月6日23点18分 高级acl&#xff08;Access Control List&#xff09;是一种访问控制列表&#xff0c;可以根据数据包的源IP地址、目标IP地址、源端口、目标端口、协议…

【ARM 嵌入式 C 常用数据结构系列 25.1 -- linux 双向链表 list_head 使用详细介绍】

请阅读【嵌入式开发学习必备专栏 】 文章目录 内核双向链表双向链表的数据结构初始化双向链表在双向链表中添加元素遍历双向链表链表使用示例注意事项 内核双向链表 在Linux内核中&#xff0c;双向链表是一种广泛使用的数据结构&#xff0c;允许从任意节点高效地进行前向或后向…

STM32F407-SRAM

SRAM—> 内存 Flash–>硬盘 外置SRAM 可以存储1M数据 地址线&#xff1a;A0-A18&#xff1b;2^18次方&#xff1b;512K个数据块 每个数据块是2字节&#xff1b; 数据线&#xff1a;D0-D15 UB/LB 掩码&#xff1b;低电平有效 UB -》低电平-》数据高字节有效 LB-》低电平…

golang 选择排序

学习笔记&#xff5e; // Author sunwenbo // 2024/4/6 21:49 package mainimport "fmt"/* 选择排序基本介绍选择式排序也属于内部排序法&#xff0c;是从预排序的数据中按指定的规则选出某一元素&#xff0c;经过和其他元素重整&#xff0c;再依原则交换位置后达到…

轻量的 WebHook 工具:歪脖虎克

本篇文章聊聊轻量的网络钩子&#xff08;WebHook&#xff09;工具&#xff1a;歪脖虎克。 写在前面 这是一篇迟到很久的文章&#xff0c;在 21 年和 22 年的时候&#xff0c;我分享过两篇关于轻量的计划任务工具 Cronicle 的文章&#xff1a;《轻量的定时任务工具 Cronicle&a…

Linux(Ubuntu)中创建【samba】服务,用于和Windows系统之间共享文件

目录 1.先介绍一下什么是Samba 2.安装&#xff0c;配置服务 安装 配置&#xff08;smb.conf&#xff09; 配置用户 3.出现的问题&#xff08;Failed to add entry for user XXXX&#xff09; 4.创建文件夹 5.windows访问 1.先介绍一下什么是Samba Samba是一个开源的软…

2024.4.3-[作业记录]-day08-CSS 盒子模型(溢出显示、伪元素)

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;HTML5和CSS3悦读 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 作业 2024.4.3-学习笔记css溢出显示单行文本溢出显示省略号多行文本溢出显示省…

【Android】图解View的工作流程原理

文章目录 入口DecorView如何加载到Window中MeasureSpec MeasureView的测量ViewGroup的测量 LayoutView的layout() Draw1、绘制背景3、绘制View内容4、绘制子View6、绘制装饰 入口 DecorView如何加载到Window中 MeasureSpec 该类是View的内部类&#xff0c;封装View的规格尺寸…

C++资源重复释放问题

这不是自己释放了2次&#xff1b; 可能是类互相引用&#xff0c;有类似现象释放资源时引起&#xff1b;还不太了解&#xff1b; 类对象作为函数参数也会引起&#xff1b; 下面是一个简单示例&#xff1b; #include <iostream> #include <string.h> #include &l…

Spark-Scala语言实战(14)

在之前的文章中&#xff0c;我们学习了如何在spark中使用键值对中的fullOuterJoin&#xff0c;zip&#xff0c;combineByKey三种方法。想了解的朋友可以查看这篇文章。同时&#xff0c;希望我的文章能帮助到你&#xff0c;如果觉得我的文章写的不错&#xff0c;请留下你宝贵的点…

最优算法100例之36-扑克牌顺子

专栏主页:计算机专业基础知识总结(适用于期末复习考研刷题求职面试)系列文章https://blog.csdn.net/seeker1994/category_12585732.html 题目描述 LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了…

软考113-上午题-【计算机网络】-IPv6、无线网络、Windows命令

一、IPv6 IPv6 具有长达 128 位的地址空间&#xff0c;可以彻底解决 IPv4 地址不足的问题。由于 IPv4 地址是32 位二进制&#xff0c;所能表示的IP 地址个数为 2^32 4 294 967 29640 亿&#xff0c;因而在因特网上约有 40亿个P 地址。 由 32 位的IPv4 升级至 128 位的IPv6&am…

FaaF:利用事实作为评估RAG的函数方法

原文地址&#xff1a;faaf-facts-as-a-function-for-evaluating-rag 2024 年 4 月 5 日 在某些情况下&#xff0c;我们使用其他语言模型来验证RAG的输出结果&#xff0c;但这种方法并未能有效识别出数据生成过程中的错误和缺失。 论文解析 挑战 评估的可靠性和效率&#xff…

PyTorch之计算模型推理时间

一、参考资料 如何测试模型的推理速度 Pytorch 测试模型的推理速度 二、计算PyTorch模型推理时间 1. 计算CPU推理时间 import torch import torchvision import time import tqdm from torchsummary import summarydef calcCPUTime():model torchvision.models.resnet18()…

数据字典

文章目录 一、需求分析二、表设计&#xff08;两张表&#xff09;三、功能实现3.1 数据字典功能3.1.1 列表功能3.1.2 新增数据字典3.1.3 编辑数据字典 3.2 数据字典明细3.2.1 列表功能3.2.2 新增字典明细3.2.3 编辑字典明细 3.3 客户管理功能3.3.1 列表功能3.3.2 新增用户3.3.3…