component-template.cjs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. #!/usr/bin/env node
  2. /**
  3. * Ant Design v6 组件迁移模板生成器
  4. *
  5. * 功能:生成符合 v6 最佳实践的组件模板
  6. * 使用方法:node component-template.js <组件类型> <组件名>
  7. */
  8. const fs = require('fs');
  9. const path = require('path');
  10. // 组件模板
  11. const templates = {
  12. // 基础功能组件
  13. basic: (componentName) => `import React from 'react';
  14. import { Card, Button } from 'antd';
  15. interface ${componentName}Props {
  16. // TODO: 添加你的 props 类型
  17. }
  18. /**
  19. * ${componentName} 组件
  20. *
  21. * v4 → v6 迁移说明:
  22. * - 已更新到 v6 API
  23. * - 使用新的类型定义
  24. */
  25. const ${componentName}: React.FC<${componentName}Props> = (props) => {
  26. return (
  27. <Card title="${componentName}">
  28. {/* TODO: 实现你的组件逻辑 */}
  29. </Card>
  30. );
  31. };
  32. export default ${componentName};
  33. `,
  34. // 表单组件
  35. form: (componentName) => `import React from 'react';
  36. import { Form, Input, Button, message, App } from 'antd';
  37. import type { FormProps } from 'antd';
  38. interface ${componentName}FormValues {
  39. // TODO: 添加表单字段类型
  40. name?: string;
  41. }
  42. interface ${componentName}Props {
  43. onSubmit?: (values: ${componentName}FormValues) => void;
  44. }
  45. /**
  46. * ${componentName} 表单组件
  47. *
  48. * v4 → v6 迁移说明:
  49. * - ✅ 使用 App.useApp() 获取 message 实例
  50. * - ✅ Form API 已更新到 v6
  51. * - ✅ 使用 TypeScript 类型定义
  52. */
  53. const ${componentName}: React.FC<${componentName}Props> = ({ onSubmit }) => {
  54. const [form] = Form.useForm<${componentName}FormValues>();
  55. const { message } = App.useApp();
  56. const handleFinish: FormProps<${componentName}FormValues>['onFinish'] = async (values) => {
  57. try {
  58. await onSubmit?.(values);
  59. message.success('提交成功');
  60. form.resetFields();
  61. } catch (error) {
  62. message.error('提交失败');
  63. console.error(error);
  64. }
  65. };
  66. return (
  67. <Form
  68. form={form}
  69. layout="vertical"
  70. onFinish={handleFinish}
  71. autoComplete="off"
  72. >
  73. <Form.Item
  74. label="名称"
  75. name="name"
  76. rules={[{ required: true, message: '请输入名称' }]}
  77. >
  78. <Input placeholder="请输入名称" />
  79. </Form.Item>
  80. {/* TODO: 添加更多表单项 */}
  81. <Form.Item>
  82. <Button type="primary" htmlType="submit">
  83. 提交
  84. </Button>
  85. </Form.Item>
  86. </Form>
  87. );
  88. };
  89. export default ${componentName};
  90. `,
  91. // 带 Modal 的组件
  92. modal: (componentName) => `import React, { useState } from 'react';
  93. import { Modal, Button, App } from 'antd';
  94. import type { ModalProps } from 'antd';
  95. interface ${componentName}Props {
  96. title?: string;
  97. onConfirm?: () => Promise<void>;
  98. }
  99. /**
  100. * ${componentName} Modal 组件
  101. *
  102. * v4 → v6 迁移说明:
  103. * - ✅ visible 改为 open
  104. * - ✅ onVisibleChange 改为 onOpenChange
  105. * - ✅ 使用 App.useApp() 获取 message
  106. */
  107. const ${componentName}: React.FC<${componentName}Props> = ({
  108. title = '${componentName}',
  109. onConfirm
  110. }) => {
  111. const [open, setOpen] = useState(false);
  112. const [loading, setLoading] = useState(false);
  113. const { message } = App.useApp();
  114. const handleOpen = () => setOpen(true);
  115. const handleCancel = () => {
  116. if (!loading) {
  117. setOpen(false);
  118. }
  119. };
  120. const handleOk = async () => {
  121. setLoading(true);
  122. try {
  123. await onConfirm?.();
  124. message.success('操作成功');
  125. setOpen(false);
  126. } catch (error) {
  127. message.error('操作失败');
  128. console.error(error);
  129. } finally {
  130. setLoading(false);
  131. }
  132. };
  133. return (
  134. <>
  135. <Button type="primary" onClick={handleOpen}>
  136. 打开 {title}
  137. </Button>
  138. <Modal
  139. title={title}
  140. open={open}
  141. onOk={handleOk}
  142. onCancel={handleCancel}
  143. confirmLoading={loading}
  144. destroyOnClose
  145. >
  146. {/* TODO: 添加 Modal 内容 */}
  147. <p>Modal 内容</p>
  148. </Modal>
  149. </>
  150. );
  151. };
  152. export default ${componentName};
  153. `,
  154. // 带 Table 的组件
  155. table: (componentName) => `import React, { useState, useEffect } from 'react';
  156. import { Table, Button, Space, App } from 'antd';
  157. import type { TableProps } from 'antd';
  158. interface DataType {
  159. key: string;
  160. // TODO: 添加你的数据类型
  161. name: string;
  162. }
  163. interface ${componentName}Props {
  164. // TODO: 添加 props
  165. }
  166. /**
  167. * ${componentName} Table 组件
  168. *
  169. * v4 → v6 迁移说明:
  170. * - ✅ Table API 已更新到 v6
  171. * - ✅ 使用 TypeScript 类型定义
  172. * - ✅ 分页配置已更新
  173. */
  174. const ${componentName}: React.FC<${componentName}Props> = (props) => {
  175. const [loading, setLoading] = useState(false);
  176. const [dataSource, setDataSource] = useState<DataType[]>([]);
  177. const [pagination, setPagination] = useState({
  178. current: 1,
  179. pageSize: 10,
  180. total: 0,
  181. });
  182. const { message } = App.useApp();
  183. const columns: TableProps<DataType>['columns'] = [
  184. {
  185. title: '名称',
  186. dataIndex: 'name',
  187. key: 'name',
  188. },
  189. // TODO: 添加更多列
  190. {
  191. title: '操作',
  192. key: 'action',
  193. render: (_, record) => (
  194. <Space>
  195. <Button type="link" size="small">
  196. 编辑
  197. </Button>
  198. <Button type="link" size="small" danger>
  199. 删除
  200. </Button>
  201. </Space>
  202. ),
  203. },
  204. ];
  205. const fetchData = async (page = 1, pageSize = 10) => {
  206. setLoading(true);
  207. try {
  208. // TODO: 实现数据获取逻辑
  209. const response = { data: [], total: 0 };
  210. setDataSource(response.data);
  211. setPagination({
  212. current: page,
  213. pageSize,
  214. total: response.total,
  215. });
  216. } catch (error) {
  217. message.error('获取数据失败');
  218. console.error(error);
  219. } finally {
  220. setLoading(false);
  221. }
  222. };
  223. useEffect(() => {
  224. fetchData();
  225. }, []);
  226. const handleTableChange: TableProps<DataType>['onChange'] = (
  227. pagination,
  228. filters,
  229. sorter
  230. ) => {
  231. fetchData(pagination.current, pagination.pageSize);
  232. };
  233. return (
  234. <Table
  235. columns={columns}
  236. dataSource={dataSource}
  237. loading={loading}
  238. pagination={pagination}
  239. onChange={handleTableChange}
  240. rowKey="key"
  241. />
  242. );
  243. };
  244. export default ${componentName};
  245. `,
  246. // ProTable 组件
  247. proTable: (componentName) => `import React, { useRef } from 'react';
  248. import { ProTable } from '@ant-design/pro-components';
  249. import type { ProColumns, ActionType } from '@ant-design/pro-components';
  250. import { Button, Space, App } from 'antd';
  251. interface DataType {
  252. id: string;
  253. // TODO: 添加你的数据类型
  254. name: string;
  255. createdAt: string;
  256. }
  257. /**
  258. * ${componentName} ProTable 组件
  259. *
  260. * v4 → v6 迁移说明:
  261. * - ✅ ProTable API 已更新
  262. * - ✅ request 函数签名保持兼容
  263. * - ✅ 使用新的类型定义
  264. */
  265. const ${componentName}: React.FC = () => {
  266. const actionRef = useRef<ActionType>();
  267. const { message } = App.useApp();
  268. const columns: ProColumns<DataType>[] = [
  269. {
  270. title: '名称',
  271. dataIndex: 'name',
  272. key: 'name',
  273. },
  274. {
  275. title: '创建时间',
  276. dataIndex: 'createdAt',
  277. key: 'createdAt',
  278. valueType: 'dateTime',
  279. },
  280. // TODO: 添加更多列
  281. {
  282. title: '操作',
  283. key: 'action',
  284. valueType: 'option',
  285. render: (_, record) => [
  286. <Button key="edit" type="link" size="small">
  287. 编辑
  288. </Button>,
  289. <Button key="delete" type="link" size="small" danger>
  290. 删除
  291. </Button>,
  292. ],
  293. },
  294. ];
  295. const handleRequest = async (
  296. params: any,
  297. sort: any,
  298. filter: any
  299. ) => {
  300. try {
  301. // TODO: 实现数据获取逻辑
  302. const response = {
  303. data: [],
  304. total: 0,
  305. success: true,
  306. };
  307. return response;
  308. } catch (error) {
  309. message.error('获取数据失败');
  310. return {
  311. data: [],
  312. total: 0,
  313. success: false,
  314. };
  315. }
  316. };
  317. return (
  318. <ProTable<DataType>
  319. columns={columns}
  320. actionRef={actionRef}
  321. request={handleRequest}
  322. rowKey="id"
  323. search={{
  324. labelWidth: 'auto',
  325. }}
  326. pagination={{
  327. defaultPageSize: 10,
  328. showSizeChanger: true,
  329. }}
  330. dateFormatter="string"
  331. headerTitle="${componentName}"
  332. toolBarRender={() => [
  333. <Button key="button" type="primary">
  334. 新建
  335. </Button>,
  336. ]}
  337. />
  338. );
  339. };
  340. export default ${componentName};
  341. `,
  342. // 主题切换组件(特殊模板)
  343. theme: () => `import React, { useState, useEffect } from 'react';
  344. import { Switch, Space, Typography } from 'antd';
  345. import { MoonOutlined, SunOutlined } from '@ant-design/icons';
  346. const { Text } = Typography;
  347. type ThemeMode = 'light' | 'dark';
  348. interface ThemeSwitchProps {
  349. onChange?: (theme: ThemeMode) => void;
  350. }
  351. /**
  352. * 主题切换组件
  353. *
  354. * v4 → v6 迁移说明:
  355. * - ❌ 废弃了 CSS 文件方式 (theme/antd.dark.css)
  356. * - ✅ 使用 ConfigProvider theme 配置
  357. * - ✅ 支持动态主题切换
  358. * - ✅ 使用 Design Token
  359. *
  360. * 使用方法:
  361. * 1. 在 App 根组件中使用 ConfigProvider
  362. * 2. 根据此组件的状态切换 theme.algorithm
  363. */
  364. const ThemeSwitch: React.FC<ThemeSwitchProps> = ({ onChange }) => {
  365. const [theme, setTheme] = useState<ThemeMode>('light');
  366. useEffect(() => {
  367. // 从 localStorage 读取保存的主题
  368. const savedTheme = localStorage.getItem('theme') as ThemeMode;
  369. if (savedTheme) {
  370. setTheme(savedTheme);
  371. onChange?.(savedTheme);
  372. }
  373. }, []);
  374. const handleChange = (checked: boolean) => {
  375. const newTheme: ThemeMode = checked ? 'dark' : 'light';
  376. setTheme(newTheme);
  377. localStorage.setItem('theme', newTheme);
  378. onChange?.(newTheme);
  379. };
  380. return (
  381. <Space align="center">
  382. <SunOutlined style={{ color: theme === 'light' ? '#1890ff' : '#999' }} />
  383. <Switch
  384. checked={theme === 'dark'}
  385. onChange={handleChange}
  386. checkedChildren={<MoonOutlined />}
  387. unCheckedChildren={<SunOutlined />}
  388. />
  389. <Text type="secondary">{theme === 'dark' ? '暗黑' : '明亮'}</Text>
  390. </Space>
  391. );
  392. };
  393. export default ThemeSwitch;
  394. `,
  395. };
  396. // App Provider 模板
  397. const appProviderTemplate = () => `import React, { useState } from 'react';
  398. import { ConfigProvider, App as AntdApp, theme } from 'antd';
  399. import zhCN from 'antd/locale/zh_CN';
  400. import ThemeSwitch from './components/ThemeSwitch';
  401. type ThemeMode = 'light' | 'dark';
  402. /**
  403. * App Provider 组件
  404. *
  405. * v4 → v6 主题系统迁移:
  406. * - ✅ 使用 ConfigProvider 管理全局配置
  407. * - ✅ 使用 theme.algorithm 切换主题
  408. * - ✅ 使用 Design Token 自定义主题
  409. * - ✅ 使用 App 组件提供全局 message/notification/modal
  410. */
  411. const AppProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  412. const [themeMode, setThemeMode] = useState<ThemeMode>('light');
  413. return (
  414. <ConfigProvider
  415. locale={zhCN}
  416. theme={{
  417. // 主题算法:暗黑或明亮
  418. algorithm: themeMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm,
  419. // 全局 Design Token
  420. token: {
  421. colorPrimary: '#1890ff',
  422. borderRadius: 6,
  423. // TODO: 添加更多自定义 token
  424. },
  425. // 组件级别的主题定制
  426. components: {
  427. Button: {
  428. // TODO: 自定义 Button 样式
  429. },
  430. Card: {
  431. // TODO: 自定义 Card 样式
  432. },
  433. // TODO: 添加更多组件定制
  434. },
  435. }}
  436. >
  437. <AntdApp>
  438. {/* 主题切换器(可以放在布局的任意位置) */}
  439. <div style={{ position: 'fixed', top: 16, right: 16, zIndex: 1000 }}>
  440. <ThemeSwitch onChange={setThemeMode} />
  441. </div>
  442. {children}
  443. </AntdApp>
  444. </ConfigProvider>
  445. );
  446. };
  447. export default AppProvider;
  448. `;
  449. // 主题 Token 配置模板
  450. const themeTokensTemplate = () => `import { ThemeConfig } from 'antd';
  451. /**
  452. * 自定义主题 Token 配置
  453. *
  454. * Design Token 说明:
  455. * - token: 全局 token,影响所有组件
  456. * - components: 组件级别的 token,只影响特定组件
  457. *
  458. * 常用 Token:
  459. * - colorPrimary: 品牌主色
  460. * - colorSuccess: 成功色
  461. * - colorWarning: 警告色
  462. * - colorError: 错误色
  463. * - colorInfo: 信息色
  464. * - colorText: 文本色
  465. * - colorBgContainer: 容器背景色
  466. * - borderRadius: 圆角大小
  467. * - fontSize: 字体大小
  468. *
  469. * 完整 Token 列表:
  470. * https://ant.design/docs/react/customize-theme#theme
  471. */
  472. export const lightTheme: ThemeConfig = {
  473. token: {
  474. colorPrimary: '#1890ff',
  475. colorSuccess: '#52c41a',
  476. colorWarning: '#faad14',
  477. colorError: '#ff4d4f',
  478. colorInfo: '#1890ff',
  479. borderRadius: 6,
  480. fontSize: 14,
  481. // TODO: 添加更多自定义 token
  482. },
  483. components: {
  484. Button: {
  485. controlHeight: 32,
  486. borderRadius: 6,
  487. },
  488. Card: {
  489. borderRadiusLG: 8,
  490. },
  491. // TODO: 添加更多组件定制
  492. },
  493. };
  494. export const darkTheme: ThemeConfig = {
  495. token: {
  496. ...lightTheme.token,
  497. // 暗黑模式可以覆盖特定的 token
  498. // 注意:使用 theme.darkAlgorithm 会自动处理大部分颜色
  499. },
  500. components: lightTheme.components,
  501. };
  502. `;
  503. // 生成组件文件
  504. function generateComponent(type, componentName, outputDir = '.') {
  505. const template = templates[type];
  506. if (!template) {
  507. console.error(`❌ 未知的组件类型: ${type}`);
  508. console.log(`可用类型: ${Object.keys(templates).join(', ')}`);
  509. process.exit(1);
  510. }
  511. const content = template(componentName);
  512. const filename = `${componentName}.tsx`;
  513. const filepath = path.join(outputDir, filename);
  514. // 检查文件是否已存在
  515. if (fs.existsSync(filepath)) {
  516. console.log(`⚠️ 文件已存在: ${filepath}`);
  517. console.log('是否覆盖?(请手动确认)');
  518. return;
  519. }
  520. fs.writeFileSync(filepath, content);
  521. console.log(`✅ 已生成组件: ${filepath}`);
  522. }
  523. // 生成主题相关文件
  524. function generateThemeFiles(outputDir = '.') {
  525. const files = [
  526. { name: 'ThemeSwitch.tsx', content: templates.theme() },
  527. { name: 'AppProvider.tsx', content: appProviderTemplate() },
  528. { name: 'tokens.ts', content: themeTokensTemplate() },
  529. ];
  530. files.forEach(({ name, content }) => {
  531. const filepath = path.join(outputDir, name);
  532. if (fs.existsSync(filepath)) {
  533. console.log(`⚠️ 文件已存在: ${filepath} (跳过)`);
  534. return;
  535. }
  536. fs.writeFileSync(filepath, content);
  537. console.log(`✅ 已生成: ${filepath}`);
  538. });
  539. console.log('\n📝 使用说明:');
  540. console.log('1. 在你的根组件中引入 AppProvider:');
  541. console.log(' import AppProvider from "./AppProvider";');
  542. console.log(' <AppProvider><App /></AppProvider>');
  543. console.log('');
  544. console.log('2. 在任意需要使用 message/modal 的组件中:');
  545. console.log(' const { message } = App.useApp();');
  546. console.log('');
  547. console.log('3. 主题切换器已集成在 AppProvider 中');
  548. console.log(' 你可以根据需要调整其位置\n');
  549. }
  550. // 主函数
  551. function main() {
  552. const args = process.argv.slice(2);
  553. if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
  554. console.log(`
  555. Ant Design v6 组件模板生成器
  556. 用法:
  557. node component-template.js <类型> <组件名> [输出目录]
  558. node component-template.js --theme [输出目录]
  559. 组件类型:
  560. basic - 基础组件
  561. form - 表单组件
  562. modal - Modal 组件
  563. table - Table 组件
  564. proTable - ProTable 组件
  565. 特殊命令:
  566. --theme - 生成完整的主题系统文件(ThemeSwitch + AppProvider + tokens)
  567. 示例:
  568. # 生成基础组件
  569. node component-template.js basic UserCard ./src/components
  570. # 生成表单组件
  571. node component-template.js form UserForm ./src/components
  572. # 生成主题系统文件
  573. node component-template.js --theme ./src/theme
  574. 可用模板:
  575. ${Object.keys(templates).join(', ')}
  576. `);
  577. process.exit(0);
  578. }
  579. if (args[0] === '--theme') {
  580. const outputDir = args[1] || '.';
  581. console.log(`\n🎨 生成主题系统文件到: ${outputDir}\n`);
  582. if (!fs.existsSync(outputDir)) {
  583. fs.mkdirSync(outputDir, { recursive: true });
  584. }
  585. generateThemeFiles(outputDir);
  586. return;
  587. }
  588. const [type, componentName, outputDir = '.'] = args;
  589. if (!componentName) {
  590. console.error('❌ 请提供组件名称');
  591. process.exit(1);
  592. }
  593. console.log(`\n📝 生成 ${type} 类型的组件: ${componentName}\n`);
  594. if (!fs.existsSync(outputDir)) {
  595. fs.mkdirSync(outputDir, { recursive: true });
  596. }
  597. generateComponent(type, componentName, outputDir);
  598. }
  599. main();