使用命令行一键创建后台页面

最近做后台项目,后台的很多页面都是类似的,都是一些表格、表单等。如图所示,为了提高效率,我们可以使用命令行一键创建后台页面。

页面的目录结构

这边项目,每个页面都是一个文件夹,里面的文件结构如下:

  • 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 的请求的具体方法(如GETPOST)、具体路径、请求参数

  • 分析useColumns.tsx

    • 基础查询列的不同
    • 查询列的具体字段
    • 表格列的具体字段
  • 分析typing.d.ts:表格数据的具体字段和类型。

创建脚本

为了快速创建页面,我们可以编写一个脚本来自动生成这些文件。

写完脚本之后,我们可以通过命令行运行这个脚本来创建新的页面。

shell 复制代码
create-page --p=src/pages/TaskCenter --q=73785 -e=74304

其中 p 是页面的路径,q 是查询接口的 ID,e 是导出接口的 ID。

运行命令之后,会在指定的路径下创建一个新的页面文件夹,里面包含了index.tsxservice.tsuseColumns.tsxtyping.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) });
}

总结

通过这种方式,我们可以快速创建后台页面,减少重复的工作,提高开发效率。只需要修改少量的代码,就可以完成一个新的页面的开发。这种方法特别适合于后台管理系统中大量相似页面的场景。

相关推荐
Arvin62737 分钟前
Nginx IP授权页面实现步骤
服务器·前端·nginx
初遇你时动了情1 小时前
react/vue vite ts项目中,自动引入路由文件、 import.meta.glob动态引入路由 无需手动引入
javascript·vue.js·react.js
摇滚侠2 小时前
JavaScript 浮点数计算精度错误示例
开发语言·javascript·ecmascript
xw52 小时前
Trae安装指定版本的插件
前端·trae
天蓝色的鱼鱼2 小时前
JavaScript垃圾回收:你不知道的内存管理秘密
javascript·面试
默默地离开2 小时前
前端开发中的 Mock 实践与接口联调技巧
前端·后端·设计模式
南岸月明2 小时前
做副业,稳住心态,不靠鸡汤!我的实操经验之路
前端
嘗_2 小时前
暑期前端训练day7——有关vue-diff算法的思考
前端·vue.js·算法
伍哥的传说2 小时前
React 英语打地鼠游戏——一个寓教于乐的英语学习游戏
学习·react.js·游戏
MediaTea3 小时前
Python 库手册:html.parser HTML 解析模块
开发语言·前端·python·html