11-在线使用
1. 需求分析
在线使用代码生成器,在表单页面输入数据模型的值,就能直接下载生成的代码
2. 核心设计
1. 业务流程
- 用户打开某个生成器的使用页面,从后端请求需要用户填写的数据模型
- 用户填写表单并提交,向后端发送请求
- 后端从数据库中查询生成器信息,得到生成器产物文件路径
- 后端从对象存储中下载生成器产物文件到本地
- 后端操作代码生成器,输入用户填写的数据,得到生成的代码
- 后端将生成的代码返回给用户,前端下载
2. 问题分析
- 生成器使用页面需要展示哪些表单项?数据模型信息从哪里来?
- web 后端怎么操作代码生成器文件去生成代码?
1. 数据模型从哪来?
最原始的数据模型信息肯定是由用户创建生成器时填写的,所以 完善创建生成器页面 的 “模型配置” 表单。有了模型配置,生成器使用页面就可以渲染出对应的表单项,供用户填写
2. 如何操作生成器?
- 通过执行脚本文件、传入指定的参数、交互式输入、最终得到生成的代码
- 支持通过读取 JSON 文件获取数据模型,并生成代码
- web 后端项目将用户输入的数据模型值 JSON 保存为本地文件,然后将文件路径作为输入参数去执行生成器脚本了
3. 后端开发
后端开发工作分为:
- 单个代码生成器改造,支持 JSON 输入
- 修改制作工具,生成支持 JSON 输入的代码生成器
- 使用生成器接口开发
0. 示例数据
- 插入一条代码生成器数据,便于后续测试
- 在代码生成器详情页查看该测试数据
INSERT INTO my_db.generator (id, name, description, basePackage, version, author, tags, picture, fileConfig, modelConfig, distPath, status, userId, createTime, updateTime, isDelete) VALUES (18, 'acm-template-pro-generator', 'ACM 示例模板生成器', 'com.yupi', '1.0', 'yupi', '["Java"]', 'https://yuzi-1256524210.cos.ap-shanghai.myqcloud.com/generator_picture/1738875515482562562/U7uDBXC3-test (1).jpg', '{
"files": [
{
"groupKey": "git",
"groupName": "开源",
"type": "group",
"condition": "needGit",
"files": [
{
"inputPath": ".gitignore",
"outputPath": ".gitignore",
"type": "file",
"generateType": "static"
},
{
"inputPath": "README.md",
"outputPath": "README.md",
"type": "file",
"generateType": "static"
}
]
},
{
"inputPath": "src/com/yupi/acm/MainTemplate.java.ftl",
"outputPath": "src/com/yupi/acm/MainTemplate.java",
"type": "file",
"generateType": "dynamic"
}
]
}', '{"models":[{"fieldName":"needGit","type":"boolean","description":"是否生成 .gitignore 文件","defaultValue":true},{"fieldName":"loop","type":"boolean","description":"是否生成循环","defaultValue":false,"abbr":"l"},{"type":"MainTemplate","description":"用于生成核心模板文件","groupKey":"mainTemplate","groupName":"核心模板","models":[{"fieldName":"author","type":"String","description":"作者注释","defaultValue":"yupi","abbr":"a"},{"fieldName":"outputText","type":"String","description":"输出信息","defaultValue":"sum = ","abbr":"o"}],"condition":"loop"}]}', '/generator_dist/1738875515482562562/kLbG2yGh-acm-template-pro-generator.zip', 0, 1738875515482562562, '2024-01-06 23:00:17', '2024-01-08 18:50:12', 0);
1. 单个代码生成器改造
在修改 maker 制作工具项目前,我们先从之前已经生成的单个代码生成器 acm-template-pro-generator
下手,让它能支持 JSON 输入并生成代码。
- 打开 ACM 模板代码生成器项目,在
cli.command
包下新增一个 JSON 生成命令类JsonGenerateCommand
- 定义一个文件路径(filePath)属性来接受 JSON 文件路径,在执行时读取该文件并转换为
DataModel
, 调用MainGenerator.doGenerate
生成代码即可
- 定义一个文件路径(filePath)属性来接受 JSON 文件路径,在执行时读取该文件并转换为
@Command(name = "json-generate", description = "读取 json 文件生成代码", mixinStandardHelpOptions = true)
@Data
public class JsonGenerateCommand implements Callable<Integer> {
@Option(names = {"-f", "--file"}, arity = "0..1", description = "json 文件路径", interactive = true, echo = true)
private String filePath;
public Integer call() throws Exception {
String jsonStr = FileUtil.readUtf8String(filePath);
DataModel dataModel = JSONUtil.toBean(jsonStr, DataModel.class);
MainGenerator.doGenerate(dataModel);
return 0;
}
}
- 修改
CommandExecutor
类,补充刚创建的子命令
@Command(name = "acm-template-pro-generator", mixinStandardHelpOptions = true)
public class CommandExecutor implements Runnable {
private final CommandLine commandLine;
{
commandLine = new CommandLine(this)
.addSubcommand(new GenerateCommand())
.addSubcommand(new ConfigCommand())
.addSubcommand(new ListCommand())
.addSubcommand(new JsonGenerateCommand());
}
}
- 新建测试文件
test.json
{
"needGit": false,
"loop": true,
"mainTemplate": {
"author": "laoYuPi",
"outputText": "i said = "
}
}
package com.yupi;
import com.yupi.cli.CommandExecutor;
public class Main {
public static void main(String[] args) {
CommandExecutor commandExecutor = new CommandExecutor();
args = new String[]{"json-generate", "--file=/Users/yupi/Code/yuzi-generator/yuzi-generator-maker/generated/acm-template-pro-generator/test.json"};
commandExecutor.doExecute(args);
}
}
2. 修改制作工具
- 在资源文件的模板目录下新建 JSON 生成命令类对应的 FTL 文件
JsonGenerateCommand.java.ftl
package ${basePackage}.cli.command;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONUtil;
import ${basePackage}.generator.MainGenerator;
import ${basePackage}.model.DataModel;
import lombok.Data;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;
@Command(name = "json-generate", description = "读取 json 文件生成代码", mixinStandardHelpOptions = true)
@Data
public class JsonGenerateCommand implements Callable<Integer> {
@Option(names = {"-f", "--file"}, arity = "0..1", description = "json 文件路径", interactive = true, echo = true)
private String filePath;
public Integer call() throws Exception {
String jsonStr = FileUtil.readUtf8String(filePath);
DataModel dataModel = JSONUtil.toBean(jsonStr, DataModel.class);
MainGenerator.doGenerate(dataModel);
return 0;
}
}
- 修改
CommandExecutor.java.ftl
文件
import ${basePackage}.cli.command.JsonGenerateCommand;
{
commandLine = new CommandLine(this)
.addSubcommand(new GenerateCommand())
.addSubcommand(new ConfigCommand())
.addSubcommand(new ListCommand())
.addSubcommand(new JsonGenerateCommand());
}
- 修改生成器制作类
GenerateTemplate
的generateCode
方法,补充对新命令文件的生成
// cli.command.JsonGenerateCommand
inputFilePath = inputResourcePath + File.separator + "templates/java/cli/command/JsonGenerateCommand.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/cli/command/JsonGenerateCommand.java";
DynamicFileGenerator.doGenerate(inputFilePath , outputFilePath, meta);
- 进行测试,能够顺利生成符合要求的代码生成器
3. 使用生成器接口
- 定义接口:接受用户输入的模型参数,返回生成的文件
package com.yupi.web.model.dto.generator;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* 使用代码生成器请求
*/
@Data
public class GeneratorUseRequest implements Serializable {
/**
* 生成器的 id
*/
private Long id;
/**
* 数据模型
*/
Map<String, Object> dataModel;
private static final long serialVersionUID = 1L;
}
/**
* 使用代码生成器
*/
@PostMapping("/use")
public void useGenerator(
@RequestBody GeneratorUseRequest generatorUseRequest,
HttpServletRequest request,
HttpServletResponse response) {
}
- 从对象存储下载生成器压缩包
- 先获取到代码生成器存储路径
- 定义一个独立的工作空间,用来存放下载的生成器压缩包等临时文件
- 压缩包的下载
- 解压文件,得到用户上传的生成器文件
- 将用户输入的参数写入到 JSON 文件中
- 执行脚本
- 找到代码生成器文件中的脚本路径,通过递归目录找到第一个名称为
generator
文件 - 如果是非 windows 系统,还要添加可执行权限
- windows 系统和其他操作系统执行脚本的规则不同,需要对路径进行转义
- 找到代码生成器文件中的脚本路径,通过递归目录找到第一个名称为
- 返回生成的代码结果压缩包
- 清理文件
- 以异步直接清理整个工作空间
/**
* 使用代码生成器
*
* @param generatorUseRequest
* @param request
* @param response
* @return
*/
@PostMapping("/use")
public void useGenerator(@RequestBody GeneratorUseRequest generatorUseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException {
// 获取用户输入的请求参数
Long id = generatorUseRequest.getId();
Map<String, Object> dataModel = generatorUseRequest.getDataModel();
// 需要用户登录
User loginUser = userService.getLoginUser(request);
log.info("userId = {} 使用了生成器 id = {}", loginUser.getId(), id);
Generator generator = generatorService.getById(id);
if (generator == null) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR);
}
// 生成器的存储路径
String distPath = generator.getDistPath();
if (StrUtil.isBlank(distPath)) {
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "产物包不存在");
}
// 从对象存储下载生成器的压缩包
// 定义独立的工作空间
String projectPath = System.getProperty("user.dir");
String tempDirPath = String.format("%s/.temp/use/%s", projectPath, id);
String zipFilePath = tempDirPath + "/dist.zip";
if (!FileUtil.exist(zipFilePath)) {
FileUtil.touch(zipFilePath);
}
try {
cosManager.download(distPath, zipFilePath);
} catch (Exception e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成器下载失败");
}
// 解压压缩包,得到脚本文件
File unzipDistDir = ZipUtil.unzip(zipFilePath);
// 将用户输入的参数写到 json 文件中
String dataModelFilePath = tempDirPath + "/dataModel.json";
String jsonStr = JSONUtil.toJsonStr(dataModel);
FileUtil.writeUtf8String(jsonStr, dataModelFilePath);
// 执行脚本
// 找到脚本文件所在路径
// 要注意,如果不是 windows 系统,找 generator 文件而不是 bat
File scriptFile = FileUtil.loopFiles(unzipDistDir, 2, null)
.stream()
.filter(file -> file.isFile()
&& "generator.bat".equals(file.getName()))
.findFirst()
.orElseThrow(RuntimeException::new);
// 添加可执行权限
try {
Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rwxrwxrwx");
Files.setPosixFilePermissions(scriptFile.toPath(), permissions);
} catch (Exception e) {
}
// 构造命令
File scriptDir = scriptFile.getParentFile();
// 注意,如果是 mac / linux 系统,要用 "./generator"
String scriptAbsolutePath = scriptFile.getAbsolutePath().replace("\\", "/");
String[] commands = new String[] {scriptAbsolutePath, "json-generate", "--file=" + dataModelFilePath};
// 这里一定要拆分!
ProcessBuilder processBuilder = new ProcessBuilder(commands);
processBuilder.directory(scriptDir);
try {
Process process = processBuilder.start();
// 读取命令的输出
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 等待命令执行完成
int exitCode = process.waitFor();
System.out.println("命令执行结束,退出码:" + exitCode);
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "执行生成器脚本错误");
}
// 压缩得到的生成结果,返回给前端
String generatedPath = scriptDir.getAbsolutePath() + "/generated";
String resultPath = tempDirPath + "/result.zip";
File resultFile = ZipUtil.zip(generatedPath, resultPath);
// 设置响应头
response.setContentType("application/octet-stream;charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=" + resultFile.getName());
Files.copy(resultFile.toPath(), response.getOutputStream());
// 清理文件
CompletableFuture.runAsync(() -> {
FileUtil.del(tempDirPath);
});
}
需要下载对象存储的文件到服务器,而不是前端,需要新写一个对象存储的通用下载方法。官方文档
- 在
CosManager
类补充下载方法,使用线程池提高下载效率 @PostConstruct
注解,用于等 Bean 加载完成后,初始化对象存储下载对象
// 复用对象
private TransferManager transferManager;
// bean 加载完成后执行
@PostConstruct
public void init() {
// 执行初始化逻辑
System.out.println("Bean initialized!");
// 多线程并发上传下载
ExecutorService threadPool = Executors.newFixedThreadPool(32);
transferManager = new TransferManager(cosClient, threadPool);
}
/**
* 下载对象到本地文件
*
* @param key
* @param localFilePath
* @return
* @throws InterruptedException
*/
public Download download(String key, String localFilePath) throws InterruptedException {
File downloadFile = new File(localFilePath);
GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);
Download download = transferManager.download(getObjectRequest, downloadFile);
// 同步等待下载完成
download.waitForCompletion();
return download;
}
4. 测试
通过 Swagger 测试整个流程能否正确跑通
4. 前端页面开发
1. 创建生成器的模型配置
1. 支持创建
模型配置的填写规则相对复杂,有 3 个要求:官方文档
- 能够动态添加或减少模型
- 支持选择添加模型或模型组
- 如果是模型组,组内支持嵌套多个模型
- 在
Generator/Add/components
目录下新建ModelConfigForm.tsx
模型配置表单组件- 在创建页面中引用该组件
- 为了便于测试,先
onFinish
输出表单填写结果,并返回false
防止跳转到下一步
<StepsForm.StepForm name="modelConfig" title="模型配置" onFinish={async (values) => {
console.log(values);
return false;
}}>
<ModelConfigForm formRef={formRef} />
</StepsForm.StepForm>
- 开发表单组件
- 首先展示所有模型配置字段表单项,用
Space
套起各表单项,使它们展示在一行,便于用户填写 - 无论字段是否属于某个分组,需要填写的表单项一致,可以抽象出一个单字段填写视图组件,便于复用
- 注意:如果字段属于某个分组,要额外展示一个删除按钮
- 首先展示所有模型配置字段表单项,用
/**
* 单个字段表单视图
* @param field
* @param remove
*/
const singleFieldFormView = (
field: FormListFieldData,
remove?: (index: number | number[]) => void,
) => {
return (
<Space key={field.key}>
<Form.Item label="字段名称" name={[field.name, 'fieldName']}>
<Input />
</Form.Item>
<Form.Item label="描述" name={[field.name, 'description']}>
<Input />
</Form.Item>
<Form.Item label="类型" name={[field.name, 'type']}>
<Input />
</Form.Item>
<Form.Item label="默认值" name={[field.name, 'defaultValue']}>
<Input />
</Form.Item>
<Form.Item label="缩写" name={[field.name, 'abbr']}>
<Input />
</Form.Item>
{remove && (
<Button type="text" danger onClick={() => remove(field.name)}>
删除
</Button>
)}
</Space>
);
};
- 修改分组 / 未分组字段的层级关系,对应的 name 层级等
- 对于最外层的表单列表项
- 如果是组内的表单列表,name 层级要加上父字段(分组)的 name
<Form.List name={['modelConfig', 'models']}>
...
</Form.List>
<Form.List name={[field.name, 'models']}>
...
</Form.List>
- 区分单个字段和分组
- 提供 “添加字段” 和 “添加分组” 按钮供用户选择。如果选择添加分组,需要设置 groupName 和 groupKey 默认值
<Button type="dashed" onClick={() => add()}>
添加字段
</Button>
<Button
type="dashed"
onClick={() =>
add({
groupName: '分组',
groupKey: 'group',
})
}
>
添加分组
</Button>
同样,可以用 groupKey 是否存在来判断是分组还是单个字段。先从当前已填写的表单信息中获取 groupKey
const groupKey =
formRef?.current?.getFieldsValue()?.modelConfig?.models?.[field.name]?.groupKey;
分组和单个字段需要填写的表单项是不同的,所以要根据 groupKey 展示不同的内容
{groupKey ? (
<Space>
<Form.Item label="分组key" name={[field.name, 'groupKey']}>
<Input />
</Form.Item>
<Form.Item label="组名" name={[field.name, 'groupName']}>
<Input />
</Form.Item>
<Form.Item label="类型" name={[field.name, 'type']}>
<Input />
</Form.Item>
<Form.Item label="条件" name={[field.name, 'condition']}>
<Input />
</Form.Item>
</Space>
) : (
singleFieldFormView(field)
)}
- 测试创建
- 填写表单后查看控制台的输出值,验证字段是否符合输入
2. 支持修改
测试修改已有代码生成器的模型配置,发现并没有渲染已填写的分组。
- 通过 groupKey 来判断是否渲染分组,而表单刚加载完成。还没有到模型配置填写时,通过表单的值是读取不到已有的 modelConfig
注意,Ant Design Procomponents 分步表单组件中,通过 formRef.current.getFieldsValue 得到的值始终只有当前步骤的,不包括之前已填写的
可以增加一个逻辑,如果存在之前的数据,并且通过表单读取不到模型配置,就读取之前数据内的模型配置,实现降级。
- 给组件增加
oldData
属性,用于传递修改前的数据、并驱动视图更新- 组件属性定义
- 创建页面增加属性传递
interface Props {
formRef: any;
oldData: any;
}
<ModelConfigForm formRef={formRef} oldData={oldData} />
- 修改获取 groupKey 的逻辑
{fields.map((field) => {
const modelConfig =
formRef?.current?.getFieldsValue()?.modelConfig ?? oldData?.modelConfig;
const groupKey = modelConfig.models?.[field.name]?.groupKey;
...
})}
3. 完整代码
import { CloseOutlined } from '@ant-design/icons';
import { Button, Card, Form, FormListFieldData, Input, Space } from 'antd';
interface Props {
formRef: any;
oldData: any;
}
export default (props: Props) => {
const { formRef, oldData } = props;
/**
* 单个字段表单视图
* @param field
* @param remove
*/
const singleFieldFormView = (
field: FormListFieldData,
remove?: (index: number | number[]) => void,
) => {
return (
<Space key={field.key}>
<Form.Item label="字段名称" name={[field.name, 'fieldName']}>
<Input />
</Form.Item>
<Form.Item label="描述" name={[field.name, 'description']}>
<Input />
</Form.Item>
<Form.Item label="类型" name={[field.name, 'type']}>
<Input />
</Form.Item>
<Form.Item label="默认值" name={[field.name, 'defaultValue']}>
<Input />
</Form.Item>
<Form.Item label="缩写" name={[field.name, 'abbr']}>
<Input />
</Form.Item>
{remove && (
<Button type="text" danger onClick={() => remove(field.name)}>
删除
</Button>
)}
</Space>
);
};
return (
<Form.List name={['modelConfig', 'models']}>
{(fields, { add, remove }) => {
return (
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
{fields.map((field) => {
const modelConfig =
formRef?.current?.getFieldsValue()?.modelConfig ?? oldData?.modelConfig;
const groupKey = modelConfig.models?.[field.name]?.groupKey;
return (
<Card
size="small"
title={groupKey ? '分组' : '未分组字段'}
key={field.key}
extra={
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
}
>
{groupKey ? (
<Space>
<Form.Item label="分组key" name={[field.name, 'groupKey']}>
<Input />
</Form.Item>
<Form.Item label="组名" name={[field.name, 'groupName']}>
<Input />
</Form.Item>
<Form.Item label="类型" name={[field.name, 'type']}>
<Input />
</Form.Item>
<Form.Item label="条件" name={[field.name, 'condition']}>
<Input />
</Form.Item>
</Space>
) : (
singleFieldFormView(field)
)}
{/* 组内字段 */}
{groupKey && (
<Form.Item label="组内字段">
<Form.List name={[field.name, 'models']}>
{(subFields, subOpt) => (
<div
style={{
display: 'flex',
flexDirection: 'column',
rowGap: 16,
}}
>
{subFields.map((subField) =>
singleFieldFormView(subField, subOpt.remove),
)}
<Button type="dashed" onClick={() => subOpt.add()} block>
添加组内字段
</Button>
</div>
)}
</Form.List>
</Form.Item>
)}
</Card>
);
})}
<Button type="dashed" onClick={() => add()}>
添加字段
</Button>
<Button
type="dashed"
onClick={() =>
add({
groupName: '分组',
groupKey: 'group',
})
}
>
添加分组
</Button>
<div style={{ marginBottom: 16 }} />
</div>
);
}}
</Form.List>
);
};
2. 使用生成器页面
不一定需要单独的页面,可以直接作为代码生成器详情页的一个 Tab 栏目,让用户能够在同一个页面查看详情并使用。为了大家学习方便、页面更精简,才选择将该功能放到新页面开发
1. 开发
- 新增页面和路由
{
path: '/generator/use/:id',
icon: 'home',
component: './Generator/Use',
name: '使用生成器',
hideInMenu: true,
},
使用生成器页面的核心布局和详情页基本一致
- 页面开发
- 使用页面主要是引导用户填写模型参数表单,需要注意区分模型是否为分组
<Form form={form}>
{models.map((model, index) => {
// 是分组
if (model.groupKey) {
if (!model.models) {
return <></>;
}
return (
<Collapse
key={index}
style={{
marginBottom: 24,
}}
items={[
{
key: index,
label: model.groupName + '(分组)',
children: model.models.map((subModel, index) => {
return (
<Form.Item
key={index}
label={subModel.fieldName}
// @ts-ignore
name={[model.groupKey, subModel.fieldName]}
>
<Input placeholder={subModel.description} />
</Form.Item>
);
}),
},
]}
bordered={false}
defaultActiveKey={[index]}
/>
);
}
return (
<Form.Item key={index} label={model.fieldName} name={model.fieldName}>
<Input placeholder={model.description} />
</Form.Item>
);
})}
</Form>
修改下载按钮请求的接口为 “使用生成器”,从用户填写的表单中获取参数并调用。由于下载时间可能比较长,用 loading 状态变量表示下载中
/**
* 下载按钮
*/
const downloadButton = data.distPath && currentUser && (
<Button
type="primary"
icon={<DownloadOutlined />}
loading={downloading}
onClick={async () => {
setDownloading(true);
const values = form.getFieldsValue();
// eslint-disable-next-line react-hooks/rules-of-hooks
const blob = await useGeneratorUsingPost(
{
id: data.id,
dataModel: values,
},
{
responseType: 'blob',
},
);
// 使用 file-saver 来保存文件
const fullPath = data.distPath || '';
saveAs(blob, fullPath.substring(fullPath.lastIndexOf('/') + 1));
setDownloading(false);
}}
>
生成代码
</Button>
);
由于这个接口返回的也是 blob,缺少一般接口响应的 code 值。所以要修改全局响应拦截器,直接通过响应类型判断
// 文件下载时,直接返回
if (response.data instanceof Blob) {
return response;
}
- 补充详情页和使用页的互相跳转逻辑
<Link to={`/generator/use/${id}`}>
<Button type="primary">立即使用</Button>
</Link>
<Link to={`/generator/detail/${id}`}>
<Button>查看详情</Button>
</Link>
- 测试能否生成符合预期的文件
2. 完整代码
import {
getGeneratorVoByIdUsingGet,
useGeneratorUsingPost,
} from '@/services/backend/generatorController';
import { useModel, useParams } from '@@/exports';
import { DownloadOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import {
Button,
Card,
Col,
Collapse,
Divider,
Form,
Image,
Input,
message,
Row,
Space,
Typography,
} from 'antd';
import { saveAs } from 'file-saver';
import React, { useEffect, useState } from 'react';
import { Link } from 'umi';
/**
* 生成器使用页面
* @constructor
*/
const GeneratorUsePage: React.FC = () => {
const { id } = useParams();
const [form] = Form.useForm();
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<API.GeneratorVO>({});
const { initialState } = useModel('@@initialState');
const { currentUser } = initialState ?? {};
const [downloading, setDownloading] = useState<boolean>(false);
const models = data?.modelConfig?.models ?? [];
/**
* 加载数据
*/
const loadData = async () => {
if (!id) {
return;
}
setLoading(true);
try {
const res = await getGeneratorVoByIdUsingGet({
id,
});
setData(res.data || {});
} catch (error: any) {
message.error('获取数据失败,' + error.message);
}
setLoading(false);
};
useEffect(() => {
loadData();
}, [id]);
/**
* 下载按钮
*/
const downloadButton = data.distPath && currentUser && (
<Button
type="primary"
icon={<DownloadOutlined />}
loading={downloading}
onClick={async () => {
setDownloading(true);
const values = form.getFieldsValue();
// eslint-disable-next-line react-hooks/rules-of-hooks
const blob = await useGeneratorUsingPost(
{
id: data.id,
dataModel: values,
},
{
responseType: 'blob',
},
);
// 使用 file-saver 来保存文件
const fullPath = data.distPath || '';
saveAs(blob, fullPath.substring(fullPath.lastIndexOf('/') + 1));
setDownloading(false);
}}
>
生成代码
</Button>
);
return (
<PageContainer title={<></>} loading={loading}>
<Card>
<Row justify="space-between" gutter={[32, 32]}>
<Col flex="auto">
<Space size="large" align="center">
<Typography.Title level={4}>{data.name}</Typography.Title>
</Space>
<Typography.Paragraph>{data.description}</Typography.Paragraph>
<Divider />
<Form form={form}>
{models.map((model, index) => {
// 是分组
if (model.groupKey) {
if (!model.models) {
return <></>;
}
return (
<Collapse
key={index}
style={{
marginBottom: 24,
}}
items={[
{
key: index,
label: model.groupName + '(分组)',
children: model.models.map((subModel, index) => {
return (
<Form.Item
key={index}
label={subModel.fieldName}
// @ts-ignore
name={[model.groupKey, subModel.fieldName]}
>
<Input placeholder={subModel.description} />
</Form.Item>
);
}),
},
]}
bordered={false}
defaultActiveKey={[index]}
/>
);
}
return (
<Form.Item key={index} label={model.fieldName} name={model.fieldName}>
<Input placeholder={model.description} />
</Form.Item>
);
})}
</Form>
<div style={{ marginBottom: 24 }} />
<Space size="middle">
{downloadButton}
<Link to={`/generator/detail/${id}`}>
<Button>查看详情</Button>
</Link>
</Space>
</Col>
<Col flex="320px">
<Image src={data.picture} />
</Col>
</Row>
</Card>
</PageContainer>
);
};
export default GeneratorUsePage;
5. 扩展
- 使用页面优化模型参数的填写顺序和依赖关系,优先填写单个字段,再根据单个字段的值判断是否要填写模型组
- 根据字段的类型,区分填写值的表单组件,比如布尔类型的字段使用 Radio 单选组件
- 表单项自动填充模型配置中指定的默认值