2
0

index.tsx 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import React, { useState, useEffect, useMemo } from 'react';
  2. import { Button, Card, Radio, Tabs, Input, Form, Row, Col } from 'antd';
  3. import { ViewMode } from './types';
  4. import { defaultConfig } from './config';
  5. import { useExcelParser } from './hooks/useExcelParser';
  6. import { useSheetConfig } from './hooks/useSheetConfig';
  7. import FileUpload from './components/FileUpload';
  8. import SheetConfig from './components/SheetConfig/index';
  9. import DataViewer from './components/DataViewer/index';
  10. import './styles.css';
  11. import type { Template } from '@/share/exceltypes';
  12. import type { UseMutateFunction } from '@tanstack/react-query';
  13. // 为组件定义 Props 接口
  14. export interface ExcelToJsonProps {
  15. initialValue?: Template | null;
  16. onSave?: UseMutateFunction<any, Error, Partial<Template>, unknown>;
  17. /** 是否使用默认配置 */
  18. useDefaultConfig?: boolean;
  19. }
  20. const ExcelToJson: React.FC<ExcelToJsonProps> = ({ initialValue, onSave, useDefaultConfig = false }) => {
  21. const [viewMode, setViewMode] = useState<ViewMode>('table');
  22. const [templateName, setTemplateName] = useState<string>(initialValue?.templateName || '');
  23. const [templateKey, setTemplateKey] = useState<string>(initialValue?.templateKey || '');
  24. const {
  25. jsonData,
  26. allFieldsData,
  27. parseExcelFile,
  28. generateExportData,
  29. getFieldsBySheet
  30. } = useExcelParser();
  31. const {
  32. config,
  33. addNewSheet,
  34. removeSheet,
  35. updateSheetConfig,
  36. setActiveSheet
  37. } = useSheetConfig(
  38. initialValue?.templateConfig || (useDefaultConfig ? defaultConfig : { sheets: [], activeSheetIndex: 0 }),
  39. useDefaultConfig
  40. );
  41. // 使用 useMemo 缓存 getFieldsBySheet 的结果,只在 allFieldsData 变化时重新计算
  42. const fieldsBySheet = useMemo(() => {
  43. if (Object.keys(allFieldsData).length === 0) return {};
  44. return getFieldsBySheet(allFieldsData);
  45. }, [allFieldsData]);
  46. // 添加对 initialValue 的处理
  47. useEffect(() => {
  48. if (initialValue?.templateConfig) {
  49. // 如果有初始值,可以在这里处理
  50. console.log('加载已有模板配置', initialValue.templateConfig);
  51. }
  52. }, [initialValue]);
  53. // 当配置或原始数据变化时,重新生成导出数据
  54. useEffect(() => {
  55. // 只有当原始数据存在时才处理
  56. if (Object.keys(allFieldsData).length > 0) {
  57. generateExportData(config.sheets);
  58. }
  59. }, [fieldsBySheet, config.sheets]);
  60. // 处理字段初始化
  61. const initializeFields = (fieldsBySheet: { [key: string]: string[] }) => {
  62. const activeSheet = config.sheets[config.activeSheetIndex];
  63. if (!activeSheet) return;
  64. // 获取当前工作表的字段
  65. const sheetName = activeSheet.sheetName;
  66. const fields = fieldsBySheet[sheetName] || [];
  67. // 只有当exportFields为空或者长度很少时才自动添加全部字段
  68. if (activeSheet.exportFields.length === 0 ||
  69. (activeSheet.exportFields.length < 3 && fields.length > 0)) {
  70. // 创建新的字段映射,key和value保持一致
  71. const newFieldMappings = { ...activeSheet.fieldMappings };
  72. // 添加缺失的字段映射
  73. fields.forEach(field => {
  74. if (!newFieldMappings[field]) {
  75. newFieldMappings[field] = field;
  76. }
  77. });
  78. // 更新工作表配置
  79. updateSheetConfig(config.activeSheetIndex, {
  80. exportFields: fields,
  81. fieldMappings: newFieldMappings
  82. });
  83. console.log('自动更新字段配置', fields);
  84. }
  85. };
  86. const downloadJson = () => {
  87. if (!jsonData || Object.keys(jsonData).length === 0) return;
  88. const dataStr = JSON.stringify(jsonData, null, 2);
  89. const blob = new Blob([dataStr], { type: 'application/json' });
  90. const url = URL.createObjectURL(blob);
  91. const a = document.createElement('a');
  92. a.href = url;
  93. a.download = '导出数据.json';
  94. document.body.appendChild(a);
  95. a.click();
  96. document.body.removeChild(a);
  97. URL.revokeObjectURL(url);
  98. };
  99. // 添加保存模板功能
  100. const saveTemplate = () => {
  101. if (!onSave) return;
  102. const templateData: Partial<Template> = {
  103. templateName: templateName || '新模板',
  104. templateKey: templateKey || `template_${Date.now()}`,
  105. templateConfig: config,
  106. };
  107. if (initialValue?.id) {
  108. templateData.id = initialValue.id;
  109. }
  110. onSave(templateData);
  111. };
  112. const handleFileChange = async (file: File) => {
  113. try {
  114. // 解析Excel文件,同时获取按工作表分组的可用字段
  115. const { rawData, availableFieldsBySheet } = await parseExcelFile(file, config.sheets);
  116. // 初始化字段配置
  117. if (rawData && Object.keys(rawData).length > 0) {
  118. initializeFields(availableFieldsBySheet);
  119. }
  120. } catch (error) {
  121. console.error('文件处理错误:', error);
  122. }
  123. };
  124. return (
  125. <div className="h-full flex flex-col">
  126. <div style={{
  127. display: 'flex',
  128. gap: '8px',
  129. marginBottom: '12px',
  130. alignItems: 'center'
  131. }}>
  132. {onSave && (
  133. <>
  134. <Form layout="inline" style={{ marginRight: 'auto' }}>
  135. <Form.Item label="模板名称" required style={{ marginBottom: 0 }}>
  136. <Input
  137. value={templateName}
  138. onChange={e => setTemplateName(e.target.value)}
  139. placeholder="请输入模板名称"
  140. style={{ width: '150px' }}
  141. />
  142. </Form.Item>
  143. <Form.Item label="模板标识" required style={{ marginBottom: 0 }}>
  144. <Input
  145. value={templateKey}
  146. onChange={e => setTemplateKey(e.target.value)}
  147. placeholder="请输入模板标识"
  148. style={{ width: '150px' }}
  149. />
  150. </Form.Item>
  151. </Form>
  152. </>
  153. )}
  154. <FileUpload onFileChange={handleFileChange} />
  155. {Object.keys(jsonData).length > 0 && (
  156. <>
  157. <Button type="primary" onClick={downloadJson}>
  158. 下载 JSON
  159. </Button>
  160. <Radio.Group
  161. value={viewMode}
  162. onChange={e => setViewMode(e.target.value)}
  163. optionType="button"
  164. buttonStyle="solid"
  165. >
  166. <Radio.Button value="table">表格</Radio.Button>
  167. <Radio.Button value="json">JSON</Radio.Button>
  168. </Radio.Group>
  169. </>
  170. )}
  171. {onSave && (
  172. <Button type="primary" onClick={saveTemplate}>
  173. 保存模板
  174. </Button>
  175. )}
  176. </div>
  177. <div style={{
  178. display: 'flex',
  179. gap: '16px',
  180. flex: 1,
  181. minHeight: 0
  182. }}>
  183. <div style={{
  184. width: '380px',
  185. display: 'flex',
  186. flexDirection: 'column',
  187. backgroundColor: '#fff',
  188. borderRadius: '2px'
  189. }}>
  190. <Tabs
  191. type="editable-card"
  192. activeKey={config.activeSheetIndex.toString()}
  193. onChange={(key) => setActiveSheet(parseInt(key))}
  194. onEdit={(targetKey, action) => {
  195. if (action === 'add') {
  196. addNewSheet();
  197. } else if (action === 'remove' && typeof targetKey === 'string') {
  198. removeSheet(parseInt(targetKey));
  199. }
  200. }}
  201. items={config.sheets.map((sheet, index) => ({
  202. key: index.toString(),
  203. label: sheet.sheetName,
  204. children: (
  205. <div style={{ overflow: 'auto', height: '100%', padding: '12px' }}>
  206. <SheetConfig
  207. config={sheet}
  208. availableFields={fieldsBySheet[sheet.sheetName] || []}
  209. onConfigChange={(updates) => updateSheetConfig(index, updates)}
  210. />
  211. </div>
  212. )
  213. }))}
  214. style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
  215. className="compact-tabs"
  216. />
  217. </div>
  218. <div style={{
  219. flex: 1,
  220. display: 'flex',
  221. flexDirection: 'column',
  222. backgroundColor: '#fff',
  223. borderRadius: '2px'
  224. }}>
  225. {Object.keys(jsonData).length > 0 && (
  226. <div style={{
  227. // overflow: 'auto',
  228. height: '100%',
  229. // padding: '12px'
  230. }}>
  231. <DataViewer
  232. data={jsonData}
  233. viewMode={viewMode}
  234. config={config}
  235. />
  236. </div>
  237. )}
  238. </div>
  239. </div>
  240. </div>
  241. );
  242. };
  243. export default ExcelToJson;