最近做后台项目,后台的很多页面都是类似的,都是一些表格、表单等。如图所示,为了提高效率,我们可以使用命令行一键创建后台页面。
页面的目录结构
这边项目,每个页面都是一个文件夹,里面的文件结构如下:
index.tsx
:页面的入口文件,主要负责引入组件和处理数据请求。service.ts
:负责与后端进行数据交互,定义了数据请求的接口。useColumns.tsx
:定义了表格的列和搜索条件,使用了自定义的useBaseColumns
钩子来获取一些通用的列。typing.d.ts
:定义了表格数据的类型,确保数据的类型安全。
以上面的TaskCenter
页面为例,目录结构如下:
index.tsx
tsx
import React from 'react';
import PageTable from '@/components/PageTableNew';
import { apiQuery, apiExport } from './service';
import useColumns from './useColumns';
type TaskCenterProps = {};
const TaskCenter: React.FC<TaskCenterProps> = () => {
const { formRef, searchColumns, tableColumns } = useColumns();
const apiQueryList = (params: any) => {
/*这里可以处理参数逻辑然后再返回*/ return apiQuery(params);
};
const apiExportList = (params: any) => {
/*这里可以处理参数逻辑然后再返回*/ return apiExport(params);
};
return (
<>
<PageTable
apiQueryList={apiQueryList}
apiExportList={apiExportList}
searchColumns={searchColumns}
tableColumns={tableColumns}
formRef={formRef}
actionRef={formActionRef}
ButtonsElse={[
// 额外按钮,包括规则说明和新建按钮
<Button
type="link"
href="https://xxxx.com/rules"
target="_blank"
rel="noopener noreferrer"
>
规则说明
</Button>,
<Button type="primary" onClick={handleAdd}>
新建
</Button>,
]}
/>
</>
);
};
TaskCenter.displayName = 'TaskCenter';
export default TaskCenter;
service.ts
ts
import { BASE_URL } from '@/utils/define';
import { request } from '@umijs/max';
export const apiQuery = (params: any) => {
return request(
`${BASE_URL}/k12-manager-backend/npad/paper/collection/task/getTaskLevelStatistics`,
{ method: 'GET', params }
);
};
export const apiExport = (data: any) => {
return request(
`${BASE_URL}/k12-manager-backend/npad/paper/collection/task/downloadTaskLevelStatistics`,
{ method: 'POST', data }
);
};
useColumns.tsx
tsx
import useBaseColumns from '@/hooks/useBaseColumns';
import { Button } from 'antd';
import type { ProColumns } from '@ant-design/pro-components';
import { TableListItem } from './typing';
export default function useColumns() {
const { formRef, schoolIdColumn, deptCodeColumn, gradeCodesColumn } =
useBaseColumns();
const searchColumns = [schoolIdColumn, deptCodeColumn, gradeCodesColumn];
const tableColumns: ProColumns<TableListItem>[] = [
{ title: '任务ID', dataIndex: 'taskId' },
{ title: '学校name', dataIndex: 'schoolName' },
{ title: '年级名', dataIndex: 'gradeName' },
{ title: '部门名', dataIndex: 'deptName' },
{ title: '任务名', dataIndex: 'taskName' },
{ title: '学年', dataIndex: 'examTime' },
{ title: '学期', dataIndex: 'semester' },
{ title: '考试类型', dataIndex: 'examType' },
{ title: '收到任务的老师数', dataIndex: 'receivedTaskTeacherNum' },
{ title: '收到任务的学生数', dataIndex: 'receivedTaskStuNum' },
{ title: '完成任务的学生数', dataIndex: 'finishedTaskStuNum' },
{ title: '完成率', dataIndex: 'finishedRate' },
{ title: '创建人', dataIndex: 'creatorEmail' },
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 100,
render(_: any, item: any) {
return (
<Button
type="link"
onClick={() => {
console.log(formRef.current?.getFieldsValue(), item);
}}
>
查看
</Button>
);
},
},
];
return {
formRef,
searchColumns,
tableColumns,
};
}
typing.d.ts
ts
export interface TableListItem {
taskId: number;
schoolId: string; // 学校id
schoolName: string; // 学校name
gradeCode: string; // 年级code
gradeName: string; // 年级名
deptCode: string; // 部门code
deptName: string; // 部门名
taskName: string; // 任务名
examTime: string; // 学年
semester: string; // 学期
examType: number; // 考试类型
receivedTaskTeacherNum: number; // 收到任务的老师数
receivedTaskStuNum: number; // 收到任务的学生数
finishedTaskStuNum: number; // 完成任务的学生数
finishedRate: string; // 完成率
creatorEmail: string;
}
分析各个文件的变化部分
在这个项目中,虽然每个页面的功能和数据结构可能不同,但大部分的代码结构是相似的。我们可以将这些相似的部分抽象出来,形成一个模板。
-
分析
index.tsx
- 页面名称(如
TaskCenter
) - 按钮的具体功能和链接
- 其他的一些特定逻辑(如
handleAdd
函数)
- 页面名称(如
-
分析
service.ts
:API 的请求的具体方法(如GET
或POST
)、具体路径、请求参数 -
分析
useColumns.tsx
- 基础查询列的不同
- 查询列的具体字段
- 表格列的具体字段
-
分析
typing.d.ts
:表格数据的具体字段和类型。
创建脚本
为了快速创建页面,我们可以编写一个脚本来自动生成这些文件。
写完脚本之后,我们可以通过命令行运行这个脚本来创建新的页面。
shell
create-page --p=src/pages/TaskCenter --q=73785 -e=74304
其中 p 是页面的路径,q 是查询接口的 ID,e 是导出接口的 ID。
运行命令之后,会在指定的路径下创建一个新的页面文件夹,里面包含了index.tsx
、service.ts
、useColumns.tsx
和typing.d.ts
四个文件,并且这些文件已经根据模板进行了填充。
下面说下实现的细节。
核心逻辑:通过查询接口获取查询列和表格列
这里模板代码有个关键的部分是:通过查询接口获取查询列和表格列。
借用后端的接口文档来获取查询列和表格列,这样可以确保前端和后端的数据结构保持一致。
我这边后端接口文档平台是YAPI
,搜索了一番,搞定了获取接口的请求参数定义和返回值定义。
js
import axios from 'axios';
const YAPI_CONFIG = {
baseUrl: 'http://mock.test.xxxx.cn', // 替换为你的YAPI地址
token: 'xxxx', // 替换为你的YAPI token
projectId: 'xxxx', // 替换为你的项目ID
};
/**
* 从YAPI平台获取接口详情信息
* @param {string} interfaceId - 需要查询的接口ID
* @param {Object} [yapiConfig=YAPI_CONFIG] - YAPI配置对象,包含baseUrl和token等配置信息
* @param {Object} [request=axios] - 请求库实例,默认为axios
* @returns {Promise<Object|null>} 返回接口详情数据对象,请求失败时返回null
* @description
* 函数内部会先等待500ms防止请求过快
* 构造请求URL并发送GET请求获取接口详情
* 返回response.data.data中的数据
* 捕获并处理请求错误,打印错误日志后返回null
*/
async function getInterfaceDetail(
interfaceId,
yapiConfig = YAPI_CONFIG,
request = axios
) {
try {
await sleep(500); // 等待0.5秒,避免接口请求过快
const url = `${yapiConfig.baseUrl}/api/interface/get?id=${interfaceId}&token=${yapiConfig.token}`;
const response = await request.get(url);
return response.data.data;
} catch (error) {
console.error('获取接口详情失败:', error);
return null;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
返回的结果,不同的平台可能会有不同的格式,这里以YAPI
为例,返回的结果大致如下:
json
{
"query_path": {
"path": "/npad/paper/collection/task/getTaskLevelStatistics",
"params": []
},
"req_body_is_json_schema": true,
"res_body_is_json_schema": true,
"_id": 73785,
"method": "GET",
"catid": 14094,
"title": "任务纬度数据统计",
"path": "/npad/paper/collection/task/getTaskLevelStatistics",
"project_id": 1056,
"req_params": [],
"res_body_type": "json",
"uid": 1885,
"add_time": 1744275392,
"up_time": 1744695504,
"req_query": [
{
"required": "1",
"_id": "67fdf0d03885661bc43a4c5d",
"name": "schoolId",
"desc": "学校"
},
{
"required": "1",
"_id": "67fdf0d03885661bc43a4c5c",
"name": "deptCode",
"example": "",
"desc": "部门"
},
{
"required": "1",
"_id": "67fdf0d03885661bc43a4c5b",
"name": "gradeCodes",
"example": "[863,864]",
"desc": "年级,string,用,分割"
}
],
"req_headers": [],
"req_body_form": [],
"__v": 0,
"markdown": "",
"desc": "",
"res_body": "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"type\":\"object\",\"properties\":{\"List\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"taskId\":{\"type\":\"number\"},\"schoolId\":{\"type\":\"string\",\"description\":\"学校id\"},\"schoolName\":{\"type\":\"string\",\"description\":\"学校name\"},\"gradeCode\":{\"type\":\"string\",\"description\":\"年级code\"},\"gradeName\":{\"type\":\"string\",\"description\":\"年级名\"},\"deptCode\":{\"type\":\"string\",\"description\":\"部门code\"},\"deptName\":{\"type\":\"string\",\"description\":\"部门名\"},\"taskName\":{\"type\":\"string\",\"description\":\"任务名\"},\"examTime\":{\"type\":\"string\",\"description\":\"学年\"},\"semester\":{\"type\":\"string\",\"description\":\"学期\"},\"examType\":{\"type\":\"number\",\"description\":\"考试类型\"},\"receivedTaskTeacherNum\":{\"type\":\"number\",\"description\":\"收到任务的老师数\"},\"receivedTaskStuNum\":{\"type\":\"number\",\"description\":\"收到任务的学生数\"},\"finishedTaskStuNum\":{\"type\":\"number\",\"description\":\"完成任务的学生数\"},\"finishedRate\":{\"type\":\"string\",\"description\":\"完成率\"},\"creatorEmail\":{\"type\":\"string\"}}}}}}",
"username": "花花"
}
进一步处理返回的结果
js
/**
* 从YAPI接口获取详细信息并格式化处理
* @param {string} interfaceId - 接口ID
* @param {Object} [yapiConfig=YAPI_CONFIG] - YAPI配置对象,包含baseUrl、projectId等配置信息:ml-citation{ref="1" data="citationList"}
* @param {Function} [requestApiDetail=getInterfaceDetail] - 获取接口详情的函数
* @returns {Promise<Object|undefined>} 返回格式化后的接口信息对象,包含请求参数、响应参数等详细信息:
* @description
* 1. 通过interfaceId获取接口详情数据
* 2. 处理请求参数和响应参数
* 3. 根据请求方法(GET/POST)格式化参数
* 4. 返回包含完整接口信息的对象
* @example
* const info = await getInfoById('73785');
* {
author, // 接口作者
urlDoc, // 接口文档URL
title, // 接口标题
pathApi, // 接口路径
method, // 请求方法
reqParams, // 格式化后的请求参数
resBodyItemProperties, // 响应列表项属性
* }
*/
export async function getInfoById(
interfaceId,
yapiConfig = YAPI_CONFIG,
requestApiDetail = getInterfaceDetail
) {
console.time(`getInfoById接口${interfaceId}耗时`);
const urlDoc = `${yapiConfig.baseUrl}/project/${yapiConfig.projectId}/interface/api/${interfaceId}?token=${yapiConfig.token}`;
// 获取接口详情数据
const interfaceDetail = await getInterfaceDetail(interfaceId);
if (!interfaceDetail) return;
// 解构接口详情数据
let {
req_query: reqQuery,
res_body,
path: pathApi,
title,
req_body_other,
_id: yid,
method,
username: author,
} = interfaceDetail;
// 处理路径中的特殊字符
pathApi = pathApi.replace(/\./, ''); // 去掉路径中的点
// 解析请求体数据
const reqBody =
typeof req_body_other === 'string'
? JSON.parse(req_body_other)
: req_body_other;
const reqBodyProperties = reqBody?.properties;
// 解析响应体数据
const res_body_parsed = res_body
? typeof res_body === 'string'
? JSON.parse(res_body)
: res_body
: undefined;
// 获取响应体属性
const resBodyProperties = res_body_parsed?.properties;
const resBodyItemProperties =
resBodyProperties?.List?.items?.properties ||
resBodyProperties?.list?.items?.properties;
// 根据请求方法处理请求参数
const reqParams = (() => {
/* 示例转换:
[ { required: '1', name: 'gradeCodes', desc: '年级list' } ]
=> { gradeCodes: { type: 'string', description: '年级list' } } */
if (method === 'GET') {
return reqQuery.reduce((acc, item) => {
acc[item.name] = {
type: 'string',
description: item.desc || item.example || '',
...item,
};
return acc;
}, {});
}
if (method === 'POST') {
return reqBodyProperties || {};
}
return {};
})();
// 返回格式化后的接口信息
const res = {
urlDoc, // 接口文档URL
yid, // 接口ID
title, // 接口标题
pathApi, // 接口路径
reqParams, // 格式化后的请求参数
reqBodyProperties, // 请求体属性
reqQuery, // 原始查询参数
resBodyProperties, // 响应体属性
resBodyItemProperties, // 响应列表项属性
method, // 请求方法
author, // 接口作者
};
console.timeEnd(`getInfoById接口${interfaceId}耗时`);
return res;
}
构建service.ts
文件
创建每个接口的函数文本字符串
ts
/**
* 生成API请求函数的文本模板
* @param {Object} options - 配置选项
* @param {Object} options.apiInfo - API接口信息对象
* @param {string} [options.fnName="apiFn"] - 生成的函数名称
* @returns {string} 返回生成的API请求函数文本
* @description
* 1. 根据API信息生成请求函数模板
* 2. 自动区分GET/POST请求参数命名
* 3. 生成包含BASE_URL和接口路径的完整请求URL
*/
export function createApiFnText({ apiInfo, fnName = 'apiFn' }) {
// 解构API信息,提供默认空对象防止报错
const { pathApi = '', method = 'GET', author, urlDoc, title } = apiInfo || {};
// 判断请求类型
const isGet = method.toUpperCase() === 'GET';
// 根据请求类型确定参数名称
const paramsName = isGet ? 'params' : 'data';
// 生成API函数模板文本
return `
/**
* ${title || 'API请求函数'}
* @param {Object} ${paramsName} - 请求参数
* @returns {Promise} 返回请求结果
* @author ${author || '未知'}
* @see ${urlDoc || '无文档链接'}
*/
export const ${fnName} = (${paramsName}: any) => {
return request(
\`\${BASE_URL}/k12-manager-backend${pathApi}\`,
{
method: '${method}',
${paramsName},
}
);
};
`;
}
基于上面,然后生成service.ts
文件的文本内容
ts
/**
* 创建API服务文件
* @param {Object} options - 配置选项
* @param {Object} [options.apiQueryInfo] - 查询API信息对象
* @param {Object} [options.apiExportInfo] - 导出API信息对象
* @param {string} [options.filePath='./service.ts'] - 生成文件路径
* @returns {Promise<string>} 返回生成的文件内容
* @description
* 1. 根据传入的API信息生成服务文件
* 2. 自动处理查询和导出两种API类型
* 3. 生成标准的TypeScript服务文件
*/
export async function createServiceFile({
apiQueryInfo,
apiExportInfo,
filePath = './service.ts',
}) {
// 生成查询API函数文本
const queryApiFnText = apiQueryInfo
? createApiFnText({
apiInfo: apiQueryInfo,
fnName: 'apiQuery',
})
: '';
// 生成导出API函数文本
const exportApiFnText = apiExportInfo
? createApiFnText({
apiInfo: apiExportInfo,
fnName: 'apiExport',
})
: '';
// 构建完整文件内容
const fileContent = `
// 自动生成的API服务文件
// 生成时间: ${new Date().toISOString()}
import { BASE_URL } from '@/utils/define';
import { request } from '@umijs/max';
${queryApiFnText}
${exportApiFnText}
`.trim();
try {
// 确保目录存在
fs.mkdirSync(path.dirname(filePath), { recursive: true });
// 写入文件
fs.writeFileSync(filePath, fileContent);
console.log(`服务文件创建成功: ${path.resolve(filePath)}`);
return fileContent;
} catch (error) {
console.error('文件创建失败:', error);
throw error;
}
}
创建typing.d.ts
文件
先实现一个工具函数,将接口返回的属性转换为 TypeScript 的类型定义。
ts
/**
* 将对象属性转换为TypeScript接口定义
* @param {Object} options - 配置选项
* @param {Object} options.objProperties - 源对象属性,格式如 {key: {type: string, description: string}}
* @param {string} [options.name="Obj"] - 生成的接口名称
* @returns {string} 返回生成的TypeScript接口定义字符串
* @example
* // 输入
* createObjectDes({
* objProperties: {
* teacherName: { type: 'string', description: '教师名' },
* teacherEmail: { type: 'string', description: '邮箱' }
* },
* name: "Teacher"
* });
*
* // 输出
* export interface Teacher {
* teacherName: string, // 教师名
* teacherEmail: string, // 邮箱
* }
*/
export function createObjectDes({ objProperties, name = 'Obj' }) {
// 处理空值情况
if (!objProperties || typeof objProperties !== 'object') {
return '';
}
let interfaceText = `export interface ${name} {\n`;
// 遍历对象属性
for (const [key, value] of Object.entries(objProperties)) {
const { type = 'any', description = '' } = value || {};
interfaceText += ` ${key}: ${type},${
description ? ` // ${description}` : ''
}\n`;
}
interfaceText += '}';
return interfaceText;
}
然后使用这个工具函数来生成typing.d.ts
文件的内容。
ts
/**
* 生成TypeScript类型定义文件
* @param {Object} params - 参数对象
* @param {Object} params.apiQueryInfo - API查询信息对象
* @param {string} [params.filePath='./typing.d.ts'] - 生成文件路径
* @param {string} [params.name='TableListItem'] - 生成的类型名称
* @returns {string} 生成的文件内容
* @throws {Error} 文件写入失败时抛出异常
*/
export function createTyingFile({
apiQueryInfo,
filePath = './typing.d.ts',
name = 'TableListItem',
}) {
// 解构获取响应体属性
const { resBodyItemProperties } = apiQueryInfo || {};
// 空值检查
if (!resBodyItemProperties) {
console.warn('缺少有效的resBodyItemProperties参数');
return '';
}
try {
// 生成类型定义文本
const typeDefinition = createObjectDes({
objProperties: resBodyItemProperties,
name,
});
// 确保目录存在
fs.mkdirSync(path.dirname(filePath), { recursive: true });
// 写入文件
fs.writeFileSync(filePath, typeDefinition);
console.log(`类型定义文件创建成功: ${path.resolve(filePath)}`);
return typeDefinition;
} catch (error) {
console.error('文件创建失败:', error);
throw error;
}
}
创建useColumns.tsx
文件
先实现一个工具函数,用于获取搜索数据索引。
ts
/**
* 分类处理请求参数中的搜索字段
* @param {Object} reqParams - 请求参数对象
* @returns {Object} 返回分类后的字段列表
* @property {Array} commonSearchDataIndexList - 公共查询字段列表
* @property {Array} uniqueSearchDataIndexList - 特有查询字段列表
* @description
* 1. 将请求参数分为公共查询字段和特有查询字段
* 2. 自动过滤表格控制字段(orderField/pageSize等)
* 3. 确保字段不重复
*/
function getSearchDataIndex(reqParams = {}) {
// 预定义公共字段和表格控制字段
const COMMON_FIELDS = [
'schoolId',
'stageCode',
'deptCode',
'gradeCodes',
'subjectCodes',
];
const TABLE_CONTROL_FIELDS = [
'orderField',
'orderType',
'pageSize',
'pageNum',
];
// 使用Set自动去重
const uniqueSearchSet = new Set();
const commonSearchSet = new Set();
Object.keys(reqParams).forEach((key) => {
if (COMMON_FIELDS.includes(key)) {
commonSearchSet.add(key);
} else if (!TABLE_CONTROL_FIELDS.includes(key)) {
uniqueSearchSet.add(key);
}
});
return {
commonSearchDataIndexList: Array.from(commonSearchSet),
uniqueSearchDataIndexList: Array.from(uniqueSearchSet),
};
}
再使用这个工具函数来生成useColumns.tsx
文件的内容。
ts
/**
* 生成Ant Design Pro表格列配置Hook文件
* @param {Object} params - 参数对象
* @param {string} [params.filePath='./useColumns.ts'] - 生成文件路径
* @param {Object} params.apiQueryInfo - API查询信息对象
* @param {Object} params.apiQueryInfo.reqParams - 请求参数定义
* @param {Object} params.apiQueryInfo.resBodyItemProperties - 响应体属性定义
* @param {Object} params.getSearchDataIndexList - 获取搜索字段分类的函数
* @returns {string} 生成的文件内容
* @throws {Error} 文件写入失败时抛出异常
* @description
* 1. 根据API接口定义自动生成表格列配置Hook
* 2. 支持搜索列和表格列配置分离
* 3. 集成Ant Design Pro组件类型
*/
export function createUseColumnsFile({
filePath = './useColumns.ts',
apiQueryInfo,
getSearchDataIndexList = getSearchDataIndex,
}) {
// 解构API查询信息
const { reqParams = {}, resBodyItemProperties = {} } = apiQueryInfo || {};
// 获取搜索字段分类
const { commonSearchDataIndexList = [], uniqueSearchDataIndexList = [] } =
getSearchDataIndexList(reqParams);
// 生成唯一搜索列配置
const uniqueSearchColumns = uniqueSearchDataIndexList.map((dataIndex) => {
const { description: title = 'TODO', required = false } =
reqParams[dataIndex] || {};
return `{ title: '${title}', dataIndex: '${dataIndex}'${
required ? ', required: true' : ''
} }`;
});
// 生成表格列配置
const tableColumns = Object.entries(resBodyItemProperties).map(
([dataIndex, info]) => {
const title = info?.description || 'TODO';
return `{ title: '${title}', dataIndex: '${dataIndex}', width: 120 }`;
}
);
// 构建文件内容
const fileContent = `
import useBaseColumns from '@/hooks/useBaseColumns';
import { Button } from 'antd';
import { history } from '@umijs/max';
import type { ProColumns } from '@ant-design/pro-components';
import { TableListItem } from './typing';
export default function useColumns() {
const { formRef, ${commonSearchDataIndexList
.map((d) => `${d}Column`)
.join(', ')} } = useBaseColumns();
const searchColumns = [
${commonSearchDataIndexList.map((d) => `${d}Column`).join(',\n ')},
${uniqueSearchColumns.join(',\n ')}
];
const tableColumns: ProColumns<TableListItem>[] = [
${tableColumns.join(',\n ')},
{
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 100,
render(_: any, item: any) {
return (
<Button
type="link"
onClick={() => history.push('/detail', { ...item })}
>
查看
</Button>
);
},
}
];
return { formRef, searchColumns, tableColumns };
}
`;
try {
// 确保目录存在
fs.mkdirSync(path.dirname(filePath), { recursive: true });
// 写入文件
fs.writeFileSync(filePath, fileContent);
console.log(`Hook文件创建成功: ${path.resolve(filePath)}`);
return fileContent;
} catch (error) {
console.error('Hook文件创建失败:', error);
throw error;
}
}
创建index.tsx
文件
为了方便使用,我们可以创建一个函数来生成index.tsx
文件的内容。
ts
/**
* 创建React组件索引文件
* @param {Object} options - 配置选项
* @param {string} [options.filePath='./index.tsx'] - 生成的文件路径
* @param {string} options.dirname - 目录名称,用于生成组件名
* @returns {string} 生成的组件代码内容
* @throws {Error} 当缺少必要参数或文件写入失败时抛出错误
*/
export function createIndexFile({ filePath = './index.tsx', dirname }) {
if (!dirname) throw new Error('dirname参数是必需的');
const cpName = dirname.charAt(0).toUpperCase() + dirname.slice(1);
const text = `import React from "react";
import PageTable from '@/components/PageTableNew';
import { apiQuery, apiExport } from './service';
import useColumns from './useColumns';
// import useLocationState from '@/hooks/useLocationState'; // 处理路由跳转带过来的state
type ${cpName}Props = {};
const ${cpName}: React.FC<${cpName}Props> = () => {
const { formRef, searchColumns, tableColumns } = useColumns();
// useLocationState({ formRef }); // 处理跳转带过来的state
// const formActionRef = React.useRef<any>(null); // 表单操作引用
// const reload = () => { formActionRef.current?.reload(); }; // 触发查询,重置页码
// const submit = () => { formActionRef.current?.submit(); }; // 触发查询,这里页码不重置
// const getFormValues = () => { return formRef.current?.getFieldsValue() || {}; }; // 需要获取查询表单的值的话
const apiQueryList = (params: unknown) => { /* 这里可以添加参数处理逻辑 */ return apiQuery(params); };
const apiExportList = (params: unknown) => { /* 这里可以添加参数处理逻辑*/ return apiExport(params); };
return (
<PageTable
apiQueryList={apiQueryList}
apiExportList={apiExportList}
searchColumns={searchColumns}
tableColumns={tableColumns}
formRef={formRef}
// actionRef={formActionRef}
// ButtonsElse={[
// // 额外按钮,包括规则说明和新建按钮
// <Button type="link" href="https://docs.qq.com/doc/DYkdsRUFhb25EaFpY" target="_blank" rel="noopener noreferrer">使用说明</Button>,
// <Button type="primary" onClick={handleAdd}>新建</Button>
// ]}
/>
);
};
${cpName}.displayName = "${cpName}";
export default ${cpName};`;
try {
fs.writeFileSync(filePath, text);
console.log(`创建文件成功:${filePath}`);
return text;
} catch (error) {
console.error('文件创建失败:', error);
throw error;
}
}
整个脚本
最后,我们可以将所有的函数整合到一个脚本中,方便调用。
js
#!/usr/bin/env node
/**
* @description 创建页面脚手架
* 使用方法:
* 在项目根目录下运行命令:`create-page --p=src/pages/CollectionPaper/TaskCenter --q=74241 -e=74313`
* 其中:
* --p 或 --path: 指定页面的文件夹路径(相对于当前工作目录),比如 `src/pages/CollectionPaper/TaskCenter`
* --q 或 --api_query: 指定查询接口的ID,比如 `74241`
* --e 或 --api_export: 指定导出接口的ID,比如 `74313`
* 接口id就是https://blog-huahua.oss-cn-beijing.aliyuncs.com/blog/code/yapi.png
*/
import axios from 'axios';
import { hideBin } from 'yargs/helpers';
import yargs from 'yargs';
import path from 'path';
import fs from 'fs';
const YAPI_CONFIG = {
baseUrl: 'xx', // 替换为你的YAPI地址
token: 'x', // 替换为你的YAPI token
projectId: 'x', // 替换为你的项目ID
};
main();
async function main() {
// 获取参数 { _: [], path: 'demo/SchoolData1', api_query: '71760', api_export: '71761' }
const options = yargs(hideBin(process.argv))
.option('path', {
alias: 'p',
type: 'string',
description: '页面路径 (相对于当前工作目录) 如 src/Pages/SchoolData',
})
.option('api_query', {
alias: 'q',
type: 'string',
description: 'API查询接口的ID',
})
.option('api_export', {
alias: 'e',
type: 'string',
description: 'API导出接口的ID',
})
.demandOption(['path'], '必须提供页面路径')
.demandOption(['api_query'], '必须提供查询接口ID').argv;
const {
path: dirPath,
api_query: apiQueryYId,
api_export: apiExportYId,
} = options;
const fullPath = path.join(process.cwd(), dirPath);
// 创建文件夹
if (!fs.existsSync(fullPath)) {
fs.mkdirSync(fullPath, { recursive: true });
}
// 4个文件的路径
const pathMap = {
service: path.join(fullPath, 'service.ts'),
index: path.join(fullPath, 'index.tsx'),
typing: path.join(fullPath, 'typing.d.ts'),
useColumns: path.join(fullPath, 'useColumns.tsx'),
};
const apiQueryInfo = await getInfoById(apiQueryYId);
let apiExportInfo = null;
if (apiExportYId) {
apiExportInfo = await getInfoById(apiExportYId);
}
// 1. 创建service.ts文件
createServiceFile({ apiQueryInfo, apiExportInfo, filePath: pathMap.service });
// 2. 创建typing.d.ts文件
createTyingFile({ apiQueryInfo, filePath: pathMap.typing });
// 3. 创建useColumns.ts文件
createUseColumnsFile({ apiQueryInfo, filePath: pathMap.useColumns });
// 4. 创建index.tsx文件
createIndexFile({ filePath: pathMap.index, dirname: path.basename(dirPath) });
}
总结
通过这种方式,我们可以快速创建后台页面,减少重复的工作,提高开发效率。只需要修改少量的代码,就可以完成一个新的页面的开发。这种方法特别适合于后台管理系统中大量相似页面的场景。