| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import React, { useState, useEffect, useMemo } from 'react';
- import { Button, Card, Radio, Tabs, Input, Form, Row, Col } from 'antd';
- import { ViewMode } from './types';
- import { defaultConfig } from './config';
- import { useExcelParser } from './hooks/useExcelParser';
- import { useSheetConfig } from './hooks/useSheetConfig';
- import FileUpload from './components/FileUpload';
- import SheetConfig from './components/SheetConfig/index';
- import DataViewer from './components/DataViewer/index';
- import './styles.css';
- import type { Template } from '@/share/exceltypes';
- import type { UseMutateFunction } from '@tanstack/react-query';
- // 为组件定义 Props 接口
- export interface ExcelToJsonProps {
- initialValue?: Template | null;
- onSave?: UseMutateFunction<any, Error, Partial<Template>, unknown>;
- /** 是否使用默认配置 */
- useDefaultConfig?: boolean;
- }
- const ExcelToJson: React.FC<ExcelToJsonProps> = ({ initialValue, onSave, useDefaultConfig = false }) => {
- const [viewMode, setViewMode] = useState<ViewMode>('table');
- const [templateName, setTemplateName] = useState<string>(initialValue?.templateName || '');
- const [templateKey, setTemplateKey] = useState<string>(initialValue?.templateKey || '');
-
- const {
- jsonData,
- allFieldsData,
- parseExcelFile,
- generateExportData,
- getFieldsBySheet
- } = useExcelParser();
- const {
- config,
- addNewSheet,
- removeSheet,
- updateSheetConfig,
- setActiveSheet
- } = useSheetConfig(
- initialValue?.templateConfig || (useDefaultConfig ? defaultConfig : { sheets: [], activeSheetIndex: 0 }),
- useDefaultConfig
- );
- // 使用 useMemo 缓存 getFieldsBySheet 的结果,只在 allFieldsData 变化时重新计算
- const fieldsBySheet = useMemo(() => {
- if (Object.keys(allFieldsData).length === 0) return {};
- return getFieldsBySheet(allFieldsData);
- }, [allFieldsData]);
- // 添加对 initialValue 的处理
- useEffect(() => {
- if (initialValue?.templateConfig) {
- // 如果有初始值,可以在这里处理
- console.log('加载已有模板配置', initialValue.templateConfig);
- }
- }, [initialValue]);
- // 当配置或原始数据变化时,重新生成导出数据
- useEffect(() => {
- // 只有当原始数据存在时才处理
- if (Object.keys(allFieldsData).length > 0) {
- generateExportData(config.sheets);
- }
- }, [fieldsBySheet, config.sheets]);
-
- // 处理字段初始化
- const initializeFields = (fieldsBySheet: { [key: string]: string[] }) => {
- const activeSheet = config.sheets[config.activeSheetIndex];
- if (!activeSheet) return;
-
- // 获取当前工作表的字段
- const sheetName = activeSheet.sheetName;
- const fields = fieldsBySheet[sheetName] || [];
-
- // 只有当exportFields为空或者长度很少时才自动添加全部字段
- if (activeSheet.exportFields.length === 0 ||
- (activeSheet.exportFields.length < 3 && fields.length > 0)) {
-
- // 创建新的字段映射,key和value保持一致
- const newFieldMappings = { ...activeSheet.fieldMappings };
-
- // 添加缺失的字段映射
- fields.forEach(field => {
- if (!newFieldMappings[field]) {
- newFieldMappings[field] = field;
- }
- });
-
- // 更新工作表配置
- updateSheetConfig(config.activeSheetIndex, {
- exportFields: fields,
- fieldMappings: newFieldMappings
- });
-
- console.log('自动更新字段配置', fields);
- }
- };
- const downloadJson = () => {
- if (!jsonData || Object.keys(jsonData).length === 0) return;
- const dataStr = JSON.stringify(jsonData, null, 2);
- const blob = new Blob([dataStr], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = '导出数据.json';
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- };
- // 添加保存模板功能
- const saveTemplate = () => {
- if (!onSave) return;
-
- const templateData: Partial<Template> = {
- templateName: templateName || '新模板',
- templateKey: templateKey || `template_${Date.now()}`,
- templateConfig: config,
- };
-
- if (initialValue?.id) {
- templateData.id = initialValue.id;
- }
-
- onSave(templateData);
- };
- const handleFileChange = async (file: File) => {
- try {
- // 解析Excel文件,同时获取按工作表分组的可用字段
- const { rawData, availableFieldsBySheet } = await parseExcelFile(file, config.sheets);
-
- // 初始化字段配置
- if (rawData && Object.keys(rawData).length > 0) {
- initializeFields(availableFieldsBySheet);
- }
- } catch (error) {
- console.error('文件处理错误:', error);
- }
- };
- return (
- <div className="h-full flex flex-col">
- <div style={{
- display: 'flex',
- gap: '8px',
- marginBottom: '12px',
- alignItems: 'center'
- }}>
- {onSave && (
- <>
- <Form layout="inline" style={{ marginRight: 'auto' }}>
- <Form.Item label="模板名称" required style={{ marginBottom: 0 }}>
- <Input
- value={templateName}
- onChange={e => setTemplateName(e.target.value)}
- placeholder="请输入模板名称"
- style={{ width: '150px' }}
- />
- </Form.Item>
- <Form.Item label="模板标识" required style={{ marginBottom: 0 }}>
- <Input
- value={templateKey}
- onChange={e => setTemplateKey(e.target.value)}
- placeholder="请输入模板标识"
- style={{ width: '150px' }}
- />
- </Form.Item>
- </Form>
- </>
- )}
- <FileUpload onFileChange={handleFileChange} />
- {Object.keys(jsonData).length > 0 && (
- <>
- <Button type="primary" onClick={downloadJson}>
- 下载 JSON
- </Button>
- <Radio.Group
- value={viewMode}
- onChange={e => setViewMode(e.target.value)}
- optionType="button"
- buttonStyle="solid"
- >
- <Radio.Button value="table">表格</Radio.Button>
- <Radio.Button value="json">JSON</Radio.Button>
- </Radio.Group>
- </>
- )}
- {onSave && (
- <Button type="primary" onClick={saveTemplate}>
- 保存模板
- </Button>
- )}
- </div>
- <div style={{
- display: 'flex',
- gap: '16px',
- flex: 1,
- minHeight: 0
- }}>
- <div style={{
- width: '380px',
- display: 'flex',
- flexDirection: 'column',
- backgroundColor: '#fff',
- borderRadius: '2px'
- }}>
- <Tabs
- type="editable-card"
- activeKey={config.activeSheetIndex.toString()}
- onChange={(key) => setActiveSheet(parseInt(key))}
- onEdit={(targetKey, action) => {
- if (action === 'add') {
- addNewSheet();
- } else if (action === 'remove' && typeof targetKey === 'string') {
- removeSheet(parseInt(targetKey));
- }
- }}
- items={config.sheets.map((sheet, index) => ({
- key: index.toString(),
- label: sheet.sheetName,
- children: (
- <div style={{ overflow: 'auto', height: '100%', padding: '12px' }}>
- <SheetConfig
- config={sheet}
- availableFields={fieldsBySheet[sheet.sheetName] || []}
- onConfigChange={(updates) => updateSheetConfig(index, updates)}
- />
- </div>
- )
- }))}
- style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
- className="compact-tabs"
- />
- </div>
- <div style={{
- flex: 1,
- display: 'flex',
- flexDirection: 'column',
- backgroundColor: '#fff',
- borderRadius: '2px'
- }}>
- {Object.keys(jsonData).length > 0 && (
- <div style={{
- // overflow: 'auto',
- height: '100%',
- // padding: '12px'
- }}>
- <DataViewer
- data={jsonData}
- viewMode={viewMode}
- config={config}
- />
- </div>
- )}
- </div>
- </div>
- </div>
- );
- };
- export default ExcelToJson;
|