2
0
فهرست منبع

加入 excel parser

yourname 5 ماه پیش
والد
کامیت
cb7e7510f8

+ 63 - 1
.gitignore

@@ -38,4 +38,66 @@ lerna-debug.log*
 .DS_Store
 
 .pnpm-store
-old
+old
+app/entry.client.tsx
+app/entry.server.tsx
+app/root.tsx
+app/tailwind.css
+app/vite-env.d.ts
+app/config/config.server.ts
+app/errors/AppError.ts
+app/handlers/MemberTemplateHandler.server.ts
+app/lib/AdminTableHandler.server.ts
+app/lib/ApiClient.server.ts
+app/lib/ExcelParser.server.ts
+app/lib/queryClient.ts
+app/lib/SystemParamsManager.server.ts
+app/middleware/authMiddleware.server.ts
+app/middleware/memberAuthMiddleware.server.ts
+app/routes/_index.tsx
+app/routes/api.v1.admin.ad.ts
+app/routes/api.v1.admin.article.ts
+app/routes/api.v1.admin.config.ts
+app/routes/api.v1.admin.department.ts
+app/routes/api.v1.admin.member.ts
+app/routes/api.v1.admin.menu.ts
+app/routes/api.v1.admin.role.ts
+app/routes/api.v1.admin.staff.login.ts
+app/routes/api.v1.admin.staff.logined.info.ts
+app/routes/api.v1.admin.staff.logout.ts
+app/routes/api.v1.admin.staff.ts
+app/routes/api.v1.admin.storage.policy.ts
+app/routes/api.v1.convert.ts
+app/routes/api.v1.member.auth.login.ts
+app/routes/api.v1.member.auth.logined.info.ts
+app/routes/api.v1.member.auth.logout.ts
+app/routes/api.v1.member.auth.register.ts
+app/routes/api.v1.member.auth.sms-code.ts
+app/routes/api.v1.member.config.ts
+app/routes/api.v1.member.templates.ts
+app/routes/admin/AdminMainApp.client.tsx
+app/routes/admin/routes.tsx
+app/routes/components/ExcelToJson/config.ts
+app/routes/components/ExcelToJson/index copy.tsx
+app/routes/components/ExcelToJson/index.tsx
+app/routes/components/ExcelToJson/styles.css
+app/routes/components/ExcelToJson/types.ts
+app/routes/components/ExcelToJson/components/FileUpload.tsx
+app/routes/components/ExcelToJson/components/DataViewer/index.tsx
+app/routes/components/ExcelToJson/components/DataViewer/JsonView.tsx
+app/routes/components/ExcelToJson/components/DataViewer/TableView.tsx
+app/routes/components/ExcelToJson/components/SheetConfig/FieldMapping.tsx
+app/routes/components/ExcelToJson/components/SheetConfig/index.tsx
+app/routes/components/ExcelToJson/hooks/useExcelParser.ts
+app/routes/components/ExcelToJson/hooks/useSheetConfig.ts
+app/routes/components/RemixQueryClientProvider/index.tsx
+app/routes/member/MemberMainApp.client.tsx
+app/routes/member/routes.tsx
+app/routes/member/api-playground/index.tsx
+app/routes/member/components/ApiTester.tsx
+app/routes/member/components/ApiTesterFileUpload.tsx
+app/routes/member/components/TemplateCard.tsx
+app/routes/member/components/TemplateSelector.tsx
+app/routes/member/home/index.tsx
+app/routes/member/template/edit.tsx
+app/routes/member/template/list.tsx

+ 28 - 0
src/client/admin/components/ExcelToJson/components/DataViewer/JsonView.tsx

@@ -0,0 +1,28 @@
+import React from 'react';
+import { ExcelRow } from '../../types';
+
+interface JsonViewProps {
+  data: ExcelRow[];
+}
+
+const JsonView: React.FC<JsonViewProps> = ({ data }) => {
+  return (
+    <div style={{ 
+      height: 'calc(100vh - 250px)', 
+      overflow: 'auto', 
+      backgroundColor: '#fafafa',
+      padding: '12px',
+      fontSize: '13px',
+      lineHeight: '1.5'
+    }}>
+      <pre style={{ 
+        margin: 0,
+        fontFamily: 'SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace'
+      }}>
+        {JSON.stringify(data, null, 2)}
+      </pre>
+    </div>
+  );
+};
+
+export default JsonView; 

+ 48 - 0
src/client/admin/components/ExcelToJson/components/DataViewer/TableView.tsx

@@ -0,0 +1,48 @@
+import React from "react";
+import { Table } from "antd";
+import type { ColumnsType } from "antd/es/table";
+import { ExcelRow, SheetConfig } from "../../types";
+
+interface TableViewProps {
+  data: ExcelRow[];
+  sheetConfig: SheetConfig;
+}
+
+const TableView: React.FC<TableViewProps> = ({ data, sheetConfig }) => {
+  const generateColumns = (): ColumnsType<ExcelRow> => {
+    return sheetConfig.exportFields.map((key) => {
+      const mappedKey = sheetConfig.fieldMappings[key] || key;
+      return {
+        title: `${key} (${mappedKey})`,
+        dataIndex: mappedKey,
+        key: mappedKey,
+        width: 120,
+        ellipsis: true,
+      };
+    });
+  };
+
+  return (
+    <Table
+      dataSource={data}
+      columns={generateColumns()}
+      scroll={{
+        // x: '100%',
+        x: true,
+        y: "calc(100vh - 360px)",
+      }}
+      size="small"
+      bordered
+      rowKey="_id"
+      pagination={{
+        defaultPageSize: 50,
+        showSizeChanger: true,
+        showQuickJumper: true,
+        showTotal: (total) => `共 ${total} 条数据`,
+        size: "small",
+      }}
+    />
+  );
+};
+
+export default TableView;

+ 31 - 0
src/client/admin/components/ExcelToJson/components/DataViewer/index.tsx

@@ -0,0 +1,31 @@
+import React from 'react';
+import { Tabs } from 'antd';
+import { DataViewerProps } from '../../types';
+import TableView from './TableView';
+import JsonView from './JsonView';
+
+const DataViewer: React.FC<DataViewerProps> = ({ data, viewMode, config }) => {
+  const getSheetConfig = (sheetName: string) => {
+    const sheetIndex = config.sheets.findIndex(sheet => sheet.sheetName === sheetName);
+    return config.sheets[sheetIndex >= 0 ? sheetIndex : config.activeSheetIndex];
+  };
+
+  return (
+    <div style={{ height: '100%' }}>
+      <Tabs
+        items={Object.entries(data).map(([sheetName, sheetData]) => ({
+          key: sheetName,
+          label: sheetName,
+          children: viewMode === 'table' ? (
+            <TableView data={sheetData} sheetConfig={getSheetConfig(sheetName)} />
+          ) : (
+            <JsonView data={sheetData} />
+          )
+        }))}
+        style={{ height: '100%' }}
+      />
+    </div>
+  );
+};
+
+export default DataViewer; 

+ 42 - 0
src/client/admin/components/ExcelToJson/components/FileUpload.tsx

@@ -0,0 +1,42 @@
+import React, { useState } from 'react';
+import { Upload, Button, message } from 'antd';
+import { UploadOutlined } from '@ant-design/icons';
+import type { UploadFile } from 'antd';
+import { FileUploadProps } from '../types';
+
+const FileUpload: React.FC<FileUploadProps> = ({ onFileChange }) => {
+  const [fileList, setFileList] = useState<UploadFile[]>([]);
+
+  const uploadProps = {
+    beforeUpload: (file: File) => {
+      const isExcel = file.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' || // xlsx
+                     file.type === 'application/vnd.ms-excel' || // xls
+                     /\.(xlsx|xls)$/.test(file.name.toLowerCase()); // 通过文件扩展名判断
+      
+      if (!isExcel) {
+        message.error('只能上传 Excel 格式的文件(.xls 或 .xlsx)!');
+        return false;
+      }
+
+      onFileChange(file);
+      return false;
+    },
+    fileList,
+    onChange: ({ fileList }: { fileList: UploadFile[] }) => {
+      setFileList(fileList);
+    },
+    accept: '.xlsx,.xls',
+    maxCount: 1,
+    showUploadList: false
+  };
+
+  return (
+    <Upload {...uploadProps}>
+      <Button icon={<UploadOutlined />} style={{ height: '32px' }}>
+        选择 Excel 文件
+      </Button>
+    </Upload>
+  );
+};
+
+export default FileUpload; 

+ 95 - 0
src/client/admin/components/ExcelToJson/components/SheetConfig/FieldMapping.tsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import { Space, Input, Button, Tooltip, Checkbox } from 'antd';
+import { ArrowUpOutlined, ArrowDownOutlined, DeleteOutlined } from '@ant-design/icons';
+
+interface FieldMappingProps {
+  field: string;
+  mappedValue: string;
+  isRequired?: boolean;
+  isFirst: boolean;
+  isLast: boolean;
+  onMappingChange: (value: string) => void;
+  onRequiredChange?: (checked: boolean) => void;
+  onMoveUp: () => void;
+  onMoveDown: () => void;
+  onDelete: () => void;
+}
+
+const FieldMapping: React.FC<FieldMappingProps> = ({
+  field,
+  mappedValue,
+  isRequired,
+  isFirst,
+  isLast,
+  onMappingChange,
+  onRequiredChange,
+  onMoveUp,
+  onMoveDown,
+  onDelete,
+}) => {
+  return (
+    <div style={{ 
+      display: 'flex',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+      padding: '6px 8px',
+      backgroundColor: '#fafafa',
+      marginBottom: '4px',
+      borderRadius: '2px'
+    }}>
+      <Space size={8}>
+        <span style={{ 
+          minWidth: '80px',
+          fontSize: '13px'
+        }}>{field}</span>
+        <Input
+          size="small"
+          style={{ width: 120 }}
+          placeholder="映射字段名"
+          value={mappedValue}
+          onChange={(e) => onMappingChange(e.target.value)}
+        />
+      </Space>
+      <Space size={2}>
+        {onRequiredChange && (
+          <Checkbox
+            checked={isRequired}
+            onChange={(e) => onRequiredChange(e.target.checked)}
+            style={{ fontSize: '13px' }}
+          >
+            必需
+          </Checkbox>
+        )}
+        <Tooltip title="上移">
+          <Button
+            type="text"
+            size="small"
+            icon={<ArrowUpOutlined />}
+            disabled={isFirst}
+            onClick={onMoveUp}
+          />
+        </Tooltip>
+        <Tooltip title="下移">
+          <Button
+            type="text"
+            size="small"
+            icon={<ArrowDownOutlined />}
+            disabled={isLast}
+            onClick={onMoveDown}
+          />
+        </Tooltip>
+        <Tooltip title="删除">
+          <Button
+            type="text"
+            size="small"
+            danger
+            icon={<DeleteOutlined />}
+            onClick={onDelete}
+          />
+        </Tooltip>
+      </Space>
+    </div>
+  );
+};
+
+export default FieldMapping; 

+ 242 - 0
src/client/admin/components/ExcelToJson/components/SheetConfig/index.tsx

@@ -0,0 +1,242 @@
+import React from 'react';
+import { Form, Input, InputNumber, Space, Card, Select, Switch } from 'antd';
+import { SheetConfigProps } from '../../types.ts';
+import FieldMapping from './FieldMapping.tsx';
+
+const SheetConfig: React.FC<SheetConfigProps> = ({
+  config,
+  availableFields,
+  onConfigChange,
+}) => {
+  const handleFieldOrderChange = (field: string, direction: 'up' | 'down') => {
+    const currentFields = [...config.exportFields];
+    const index = currentFields.indexOf(field);
+    if (index === -1) return;
+
+    if (direction === 'up' && index > 0) {
+      [currentFields[index], currentFields[index - 1]] = [currentFields[index - 1], currentFields[index]];
+    } else if (direction === 'down' && index < currentFields.length - 1) {
+      [currentFields[index], currentFields[index + 1]] = [currentFields[index + 1], currentFields[index]];
+    }
+
+    onConfigChange({ exportFields: currentFields });
+  };
+
+  const handleAddField = (field: string) => {
+    if (!config.exportFields.includes(field)) {
+      onConfigChange({
+        exportFields: [...config.exportFields, field]
+      });
+    }
+  };
+
+  const handleRemoveField = (field: string) => {
+    const newRequiredFields = config.requiredFields.filter(f => f !== field);
+    onConfigChange({
+      exportFields: config.exportFields.filter(f => f !== field),
+      requiredFields: newRequiredFields
+    });
+  };
+
+  const handleFieldMappingChange = (field: string, mappedValue: string) => {
+    onConfigChange({
+      fieldMappings: {
+        ...config.fieldMappings,
+        [field]: mappedValue
+      }
+    });
+  };
+
+  const handleRequiredChange = (field: string, isRequired: boolean) => {
+    const newRequiredFields = isRequired
+      ? [...config.requiredFields, field]
+      : config.requiredFields.filter(f => f !== field);
+    
+    onConfigChange({
+      requiredFields: newRequiredFields
+    });
+  };
+
+  return (
+    <Form
+      layout="vertical"
+      initialValues={config}
+      onValuesChange={(_, allValues) => onConfigChange(allValues)}
+      size="small"
+      style={{ padding: '8px 0' }}
+    >
+      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px', marginBottom: '12px' }}>
+        <Form.Item
+          label="工作表名称"
+          name="sheetName"
+          rules={[{ required: true, message: '请输入工作表名称' }]}
+          style={{ marginBottom: '8px' }}
+        >
+          <Input />
+        </Form.Item>
+        <Form.Item
+          label="表头行号"
+          name="headerRowIndex"
+          rules={[{ required: true, message: '请输入表头行号' }]}
+          style={{ marginBottom: '8px' }}
+        >
+          <InputNumber style={{ width: '100%' }} min={1} />
+        </Form.Item>
+        <Form.Item
+          label="订单号行号"
+          name="orderNumberRow"
+          rules={[{ required: true, message: '请输入订单号行号' }]}
+          style={{ marginBottom: '8px' }}
+        >
+          <InputNumber style={{ width: '100%' }} min={1} />
+        </Form.Item>
+        <Form.Item
+          label="订单号列号"
+          name="orderNumberCol"
+          rules={[{ required: true, message: '请输入订单号列号' }]}
+          style={{ marginBottom: '8px' }}
+        >
+          <InputNumber style={{ width: '100%' }} min={1} />
+        </Form.Item>
+        <Form.Item
+          label="产品名称行号"
+          name="productNameRow"
+          style={{ marginBottom: '8px' }}
+        >
+          <InputNumber style={{ width: '100%' }} min={1} />
+        </Form.Item>
+        <Form.Item
+          label="产品名称列号"
+          name="productNameCol"
+          style={{ marginBottom: '8px' }}
+        >
+          <InputNumber style={{ width: '100%' }} min={1} />
+        </Form.Item>
+        <Form.Item
+          label="数据起始行号"
+          name="dataStartRow"
+          rules={[{ required: true, message: '请输入数据起始行号' }]}
+          style={{ marginBottom: '8px' }}
+        >
+          <InputNumber style={{ width: '100%' }} min={1} />
+        </Form.Item>
+        <Form.Item
+          label="终止标记"
+          name="endMarker"
+          rules={[{ required: true, message: '请输入终止标记' }]}
+          style={{ marginBottom: '8px' }}
+        >
+          <Input />
+        </Form.Item>
+      </div>
+
+      <Card 
+        size="small" 
+        title="多表格模式配置" 
+        style={{ marginBottom: '12px' }}
+        styles={{
+          body: {
+            padding: '8px'
+          }
+        }}
+      >
+        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
+          <Form.Item
+            label="启用多表格模式"
+            name="isMultiTable"
+            valuePropName="checked"
+            style={{ marginBottom: '8px' }}
+          >
+            <Switch />
+          </Form.Item>
+          <Form.Item
+            label="表头偏移量"
+            name="multiTableHeaderOffset"
+            tooltip="新表格的表头在结束标记后的偏移行数"
+            style={{ marginBottom: '8px' }}
+          >
+            <InputNumber style={{ width: '100%' }} min={0} disabled={!config.isMultiTable} />
+          </Form.Item>
+          <Form.Item
+            label="数据偏移量"
+            name="multiTableDataOffset"
+            tooltip="数据起始行相对于表头的偏移行数"
+            style={{ marginBottom: '8px' }}
+          >
+            <InputNumber style={{ width: '100%' }} min={0} disabled={!config.isMultiTable} />
+          </Form.Item>
+          <Form.Item
+            label="订单号偏移量"
+            name="multiTableOrderNumberOffset"
+            tooltip="订单号行相对于表头的偏移行数(负数表示在表头之前)"
+            style={{ marginBottom: '8px' }}
+          >
+            <InputNumber style={{ width: '100%' }} disabled={!config.isMultiTable} />
+          </Form.Item>
+          <Form.Item
+            label="产品名称偏移量"
+            name="multiTableProductNameOffset"
+            tooltip="产品名称行相对于表头的偏移行数(负数表示在表头之前)"
+            style={{ marginBottom: '8px' }}
+          >
+            <InputNumber style={{ width: '100%' }} disabled={!config.isMultiTable} />
+          </Form.Item>
+        </div>
+      </Card>
+
+      <Card 
+        size="small" 
+        title="导出字段配置" 
+        styles={{
+          body: {
+            padding: '8px'
+          }
+        }}
+      >
+        <Space direction="vertical" style={{ width: '100%' }} size={8}>
+          {availableFields.length > 0 && (
+            <Select
+              style={{ width: '100%' }}
+              placeholder="添加导出字段"
+              onChange={handleAddField}
+              value={undefined}
+              size="small"
+            >
+              {availableFields
+                .filter(field => !config.exportFields.includes(field))
+                .map(field => (
+                  <Select.Option key={field} value={field}>
+                    {field}
+                  </Select.Option>
+                ))}
+            </Select>
+          )}
+          
+          <div style={{ 
+            backgroundColor: '#fafafa'
+          }}>
+            <Space direction="vertical" style={{ width: '100%' }} size={2}>
+              {config.exportFields.map((field, index) => (
+                <FieldMapping
+                  key={field}
+                  field={field}
+                  mappedValue={config.fieldMappings[field] || ''}
+                  isRequired={config.requiredFields.includes(field)}
+                  isFirst={index === 0}
+                  isLast={index === config.exportFields.length - 1}
+                  onMappingChange={(value) => handleFieldMappingChange(field, value)}
+                  onRequiredChange={(checked) => handleRequiredChange(field, checked)}
+                  onMoveUp={() => handleFieldOrderChange(field, 'up')}
+                  onMoveDown={() => handleFieldOrderChange(field, 'down')}
+                  onDelete={() => handleRemoveField(field)}
+                />
+              ))}
+            </Space>
+          </div>
+        </Space>
+      </Card>
+    </Form>
+  );
+};
+
+export default SheetConfig; 

+ 138 - 0
src/client/admin/components/ExcelToJson/config.ts

@@ -0,0 +1,138 @@
+import { SheetConfig, ExcelConfig } from './types';
+
+// 五金清单
+export const defaultSheetConfig: SheetConfig = {
+  headerRowIndex: 5,
+  orderNumberRow: 2,
+  orderNumberCol: 3,
+  dataStartRow: 6,
+  endMarker: '订单统计数据:',
+  sheetName: '五金清单',
+  exportFields: ['订单号', '序号', '名称', '颜色', '长度', '宽度', '数量', '单位', '工艺说明'],
+  fieldMappings: {
+    '序号': 'index',
+    '名称': 'name',
+    '颜色': 'color',
+    '长度': 'length',
+    '宽度': 'width',
+    '数量': 'quantity',
+    '单位': 'unit',
+    '工艺说明': 'processDescription',
+    '订单号': 'orderNumber'
+  },
+  requiredFields: ['名称', '数量']
+};
+
+// 灯带
+export const lightBandSheetConfig: SheetConfig = {
+  headerRowIndex: 5,
+  orderNumberRow: 2,
+  orderNumberCol: 3,
+  dataStartRow: 6,
+  endMarker: '拆单签名',
+  sheetName: '灯带',
+  exportFields: ['订单号', '序号', '名称', '型号', '规格', '数量', '单位', '柜号'],
+  fieldMappings: {
+    '序号': 'index',
+    '名称': 'name',
+    '型号': 'model',
+    '规格': 'specification',
+    '数量': 'quantity',
+    '单位': 'unit',
+    '柜号': 'cabinetNumber',
+    '订单号': 'orderNumber'
+  },
+  requiredFields: ['名称', '型号', '数量']
+};
+
+// 统计
+export const statisticsSheetConfig: SheetConfig = {
+  headerRowIndex: 3,
+  orderNumberRow: 1,
+  orderNumberCol: 1,
+  dataStartRow: 4,
+  endMarker: '备注:',
+  sheetName: '统计',
+  exportFields: ['序号', '封边条颜色', '封边条规格', '单位(米)', '备注'],
+  fieldMappings: {
+    '序号': 'index',
+    '封边条颜色': 'color',
+    '封边条规格': 'specification',
+    '单位(米)': 'unit',
+    '备注': 'remark',
+  },
+  requiredFields: ['封边条颜色', '封边条规格']
+};
+
+// 外购单
+export const purchaseSheetConfig: SheetConfig = {
+  headerRowIndex: 9,
+  orderNumberRow: 7,
+  orderNumberCol: 3,
+  dataStartRow: 10,
+  endMarker: '合计数量:',
+  sheetName: '外购单',
+  exportFields: ['订单号', '序号', '柜名', '材料名称', '长(高)', '宽', '厚(深)', '单位', '数量', '门铰位置', '单价', '金额', '备注', '分类'],
+  fieldMappings: {
+    '订单号': 'orderNumber',
+    '序号': 'index',
+    '柜名': 'cabinetName',
+    '材料名称': 'materialName',
+    '长(高)': 'length',
+    '宽': 'width',
+    '厚(深)': 'depth',
+    '单位': 'unit',
+    '数量': 'quantity',
+    '门铰位置': 'hingePosition',
+    '单价': 'unitPrice',
+    '金额': 'amount',
+    '备注': 'remark',
+    '分类': 'category',
+  },
+  requiredFields: ['材料名称', '数量']
+};
+
+// 包装数据
+export const packagingSheetConfig: SheetConfig = {
+  headerRowIndex: 4,
+  orderNumberRow: 3,
+  orderNumberCol: 1,
+  dataStartRow: 5,
+  endMarker: '<数据区域结束>',
+  sheetName: '包装数据',
+  exportFields: ['订单号', '序号', '名称', '部件代码', '颜色', '长度', '宽度', '厚度', '数量', '平方数', '开料长度', '开料宽度', '开料厚度', '备注', '正面条码', '反面条码'],
+  fieldMappings: {
+    '订单号': 'orderNumber',
+    '序号': 'index',
+    '名称': 'name',
+    '部件代码': 'componentCode',
+    '颜色': 'color',
+    '长度': 'length',
+    '宽度': 'width',
+    '厚度': 'thickness',
+    '数量': 'quantity',
+    '平方数': 'square',
+    '开料长度': 'cuttingLength',
+    '开料宽度': 'cuttingWidth',
+    '开料厚度': 'cuttingThickness',
+    '备注': 'remark',
+    '正面条码': 'frontBarcode',
+    '反面条码': 'backBarcode',
+  },
+  requiredFields: ['名称', '颜色', '数量'],
+  isMultiTable: true,
+  multiTableHeaderOffset: 4, // 新表格的表头在结束标记后的偏移行数
+  multiTableDataOffset: 1,   // 数据起始行相对于表头的偏移行数
+  multiTableOrderNumberOffset: -1, // 订单号行相对于表头的偏移行数
+};
+
+export const defaultConfig: ExcelConfig = {
+  sheets: [
+    defaultSheetConfig, 
+    lightBandSheetConfig, 
+    statisticsSheetConfig,
+    purchaseSheetConfig,
+    packagingSheetConfig
+  ],
+  activeSheetIndex: 0
+}; 

+ 426 - 0
src/client/admin/components/ExcelToJson/hooks/useExcelParser.ts

@@ -0,0 +1,426 @@
+import { useState } from 'react';
+import * as XLSX from 'xlsx';
+import { message } from 'antd';
+import { ExcelRow, SheetConfig } from '../types';
+
+type ExcelJsonData = Array<Array<string | number | null>>;
+
+export const useExcelParser = () => {
+  const [jsonData, setJsonData] = useState<{ [key: string]: ExcelRow[] }>({});
+  // 存储所有字段的原始数据,不受exportFields配置影响
+  const [allFieldsData, setAllFieldsData] = useState<{ [key: string]: ExcelRow[] }>({});
+
+  const processDataForExport = (
+    data: ExcelRow[],
+    sheetConfig: SheetConfig,
+    tableIndex: number = 0
+  ): ExcelRow[] => {
+    return data
+      .map((row, index) => {
+        const processedRow: ExcelRow = {};
+        processedRow._id = `table-${tableIndex}-row-${index}`;
+        
+        // 如果exportFields为空,保留所有原始字段
+        if (sheetConfig.exportFields.length === 0) {
+          Object.keys(row).forEach(field => {
+            processedRow[field] = row[field] ?? null;
+          });
+        } else {
+          // 按照exportFields配置处理
+          sheetConfig.exportFields.forEach(field => {
+            const mappedField = sheetConfig.fieldMappings[field] || field;
+            processedRow[mappedField] = row[field] ?? null;
+          });
+        }
+        
+        return processedRow;
+      })
+      .filter(row => {
+        // 如果没有设置必需字段或exportFields为空,则不过滤
+        if (sheetConfig.requiredFields.length === 0 || sheetConfig.exportFields.length === 0) {
+          return true;
+        }
+        
+        // 检查必需字段是否有值
+        return sheetConfig.requiredFields.every(field => {
+          const value = row[sheetConfig.fieldMappings[field] || field];
+          return value !== null && value !== undefined && value !== '';
+        });
+      });
+  };
+
+  // 处理所有原始字段数据,不过滤
+  const processAllFieldsData = (
+    data: ExcelRow[],
+    tableIndex: number = 0
+  ): ExcelRow[] => {
+    return data.map((row, index) => {
+      const processedRow: ExcelRow = { ...row };
+      processedRow._id = `table-${tableIndex}-row-${index}`;
+      return processedRow;
+    });
+  };
+
+  // 获取所有可用字段
+  const getAllAvailableFields = (data: { [key: string]: ExcelRow[] }): { [key: string]: string[] } => {
+    // 按工作表名收集字段
+    const fieldsMap: { [key: string]: Set<string> } = {};
+    
+    // 遍历每个工作表
+    Object.entries(data).forEach(([sheetName, rows]) => {
+      if (!fieldsMap[sheetName]) {
+        fieldsMap[sheetName] = new Set<string>();
+      }
+      
+      // 从每行中收集字段
+      rows.forEach(row => {
+        Object.keys(row).forEach(key => {
+          // 过滤掉内部属性(如_id, tableIndex等)
+          if (!key.startsWith('_')) {
+            fieldsMap[sheetName].add(key);
+          }
+        });
+      });
+    });
+    
+    // 将Set转换为数组
+    const result: { [key: string]: string[] } = {};
+    Object.entries(fieldsMap).forEach(([sheetName, fieldsSet]) => {
+      result[sheetName] = Array.from(fieldsSet);
+    });
+    
+    return result;
+  };
+
+  // 处理单个表格的数据
+  const parseSingleTable = (
+    json: ExcelJsonData,
+    headers: string[],
+    startRow: number,
+    endMarker: string,
+    orderNumberRow: number,
+    orderNumberCol: number,
+    productNameRow?: number,
+    productNameCol?: number
+  ): { data: ExcelRow[]; endIndex: number } => {
+    const tableData: ExcelRow[] = [];
+    let currentOrderNumber: string | null = null;
+    let currentProductName: string | null = null;
+    let endIndex = json.length;
+
+    // 获取订单号
+    if (orderNumberRow > 0 && orderNumberCol > 0) {
+      currentOrderNumber = String(json[orderNumberRow - 1]?.[orderNumberCol - 1] ?? '');
+    }
+
+    // 获取产品名称(如果配置了产品名称行列号)
+    if (productNameRow && productNameCol && productNameRow > 0 && productNameCol > 0) {
+      currentProductName = String(json[productNameRow - 1]?.[productNameCol - 1] ?? '');
+    }
+
+    for (let i = startRow; i < json.length; i++) {
+      const row = json[i] || [];
+      const rowString = row.join('');
+
+      if (rowString.includes(endMarker)) {
+        endIndex = i;
+        break;
+      }
+
+      const obj: ExcelRow = {};
+      let hasData = false;
+
+      headers.forEach((header, index) => {
+        if (header) {
+          const value = row[index];
+          if (value !== undefined) {
+            obj[header] = value;
+            hasData = true;
+          } else {
+            obj[header] = null;
+          }
+        }
+      });
+
+      if (currentOrderNumber) {
+        obj['订单号'] = currentOrderNumber;
+      }
+
+      if (currentProductName) {
+        obj['产品名称'] = currentProductName;
+      }
+
+      if (hasData) {
+        tableData.push(obj);
+      }
+    }
+
+    return { data: tableData, endIndex };
+  };
+
+  // 处理多表格模式
+  const parseMultiTable = (
+    json: ExcelJsonData,
+    sheetConfig: SheetConfig
+  ): ExcelRow[][] => {
+    const tableDataSets: ExcelRow[][] = [];
+    let currentIndex = sheetConfig.dataStartRow - 1;
+    const headerOffset = sheetConfig.multiTableHeaderOffset || 1;
+    const dataOffset = sheetConfig.multiTableDataOffset || 1;
+    const orderNumberOffset = sheetConfig.multiTableOrderNumberOffset || -1;
+    const productNameOffset = sheetConfig.multiTableProductNameOffset || -1;
+
+    while (currentIndex < json.length) {
+      // 获取当前表格的表头行号
+      const headerIndex = currentIndex - (sheetConfig.dataStartRow - sheetConfig.headerRowIndex);
+      // 获取当前表格的表头
+      const headers = (json[headerIndex] || []) as string[];
+      if (!headers || headers.length === 0) break;
+
+      // 计算订单号行号
+      const orderNumberIndex = headerIndex + (orderNumberOffset || 0);
+      
+      // 计算产品名称行号(如果配置了产品名称行列号)
+      let productNameIndex = undefined;
+      if (sheetConfig.productNameRow && sheetConfig.productNameCol) {
+        productNameIndex = headerIndex + (productNameOffset || 0);
+      }
+
+      // 解析当前表格
+      const { data, endIndex } = parseSingleTable(
+        json,
+        headers,
+        currentIndex,
+        sheetConfig.endMarker,
+        orderNumberIndex + 1, // +1 因为 parseSingleTable 内部会 -1
+        sheetConfig.orderNumberCol,
+        productNameIndex !== undefined ? productNameIndex + 1 : undefined, // +1 因为 parseSingleTable 内部会 -1
+        sheetConfig.productNameCol
+      );
+
+      if (data.length > 0) {
+        tableDataSets.push(data);
+      }
+
+      // 如果没有找到结束标记,退出循环
+      if (endIndex === json.length) break;
+
+      // 更新下一个表格的起始位置
+      currentIndex = endIndex + headerOffset + dataOffset;
+    }
+
+    return tableDataSets;
+  };
+
+  const parseExcelFile = async (file: File, sheetConfigs: SheetConfig[]) => {
+    try {
+      // 显示开始处理的消息
+      const loadingMessage = message.loading({ content: `正在解析Excel文件: ${file.name}`, key: 'excelParsing', duration: 0 });
+      
+      console.log('开始处理文件:', file.name);
+      const data = await file.arrayBuffer();
+      // 更新进度提示
+      message.loading({ content: '正在读取Excel数据...', key: 'excelParsing', duration: 0 });
+      
+      const workbook = XLSX.read(data);
+      
+      const rawData: { [key: string]: ExcelRow[] } = {};
+      let totalTables = 0;
+      let processedSheets = 0;
+      
+      // 处理每个配置的工作表
+      for (const sheetConfig of sheetConfigs) {
+        // 更新进度提示,显示当前处理的工作表
+        message.loading({ 
+          content: `正在处理工作表 "${sheetConfig.sheetName}" (${processedSheets + 1}/${sheetConfigs.length})`, 
+          key: 'excelParsing', 
+          duration: 0 
+        });
+        
+        const worksheet = workbook.Sheets[sheetConfig.sheetName];
+        
+        if (!worksheet) {
+          console.error('未找到工作表:', sheetConfig.sheetName);
+          message.error({
+            content: `未找到工作表"${sheetConfig.sheetName}"!`,
+            key: 'excelParsing'
+          });
+          processedSheets++;
+          continue;
+        }
+
+        const json = XLSX.utils.sheet_to_json<string[]>(worksheet, { header: 1 }) as ExcelJsonData;
+        const headers = json[sheetConfig.headerRowIndex - 1] as string[];
+        
+        if (!headers || headers.length === 0) {
+          console.error(`工作表 ${sheetConfig.sheetName} 的表头数据无效`);
+          message.warning({
+            content: `工作表 "${sheetConfig.sheetName}" 的表头数据无效,已跳过`,
+            key: 'excelParsing'
+          });
+          processedSheets++;
+          continue;
+        }
+
+        if (sheetConfig.isMultiTable) {
+          // 处理多表格模式
+          message.loading({ 
+            content: `正在处理 "${sheetConfig.sheetName}" 的多表格数据...`, 
+            key: 'excelParsing', 
+            duration: 0 
+          });
+          
+          const tableDataSets = parseMultiTable(json, sheetConfig);
+          
+          if (tableDataSets.length > 0) {
+            message.loading({ 
+              content: `"${sheetConfig.sheetName}" 中发现 ${tableDataSets.length} 个数据表,正在处理...`, 
+              key: 'excelParsing', 
+              duration: 0 
+            });
+            
+            // 保存所有原始字段数据
+            rawData[sheetConfig.sheetName] = tableDataSets.flatMap((tableData, tableIndex) => {
+              return processAllFieldsData(tableData, tableIndex + 1).map(row => ({
+                ...row,
+                tableIndex: tableIndex + 1,
+              }));
+            });
+            
+            totalTables += tableDataSets.length;
+          } else {
+            message.warning({
+              content: `未在 "${sheetConfig.sheetName}" 中找到有效的数据表`,
+              duration: 2
+            });
+          }
+        } else {
+          // 处理单表格模式
+          message.loading({ 
+            content: `正在处理 "${sheetConfig.sheetName}" 的单表格数据...`, 
+            key: 'excelParsing', 
+            duration: 0 
+          });
+          
+          const { data } = parseSingleTable(
+            json,
+            headers,
+            sheetConfig.dataStartRow - 1,
+            sheetConfig.endMarker,
+            sheetConfig.orderNumberRow,
+            sheetConfig.orderNumberCol,
+            sheetConfig.productNameRow || undefined,
+            sheetConfig.productNameCol || undefined
+          );
+          
+          if (data.length > 0) {
+            // 保存所有原始字段数据
+            rawData[sheetConfig.sheetName] = processAllFieldsData(data, 1);
+            
+            totalTables += 1;
+          } else {
+            message.warning({
+              content: `未在 "${sheetConfig.sheetName}" 中找到有效数据`,
+              duration: 2
+            });
+          }
+        }
+        
+        processedSheets++;
+        
+        // 更新进度提示
+        if (processedSheets < sheetConfigs.length) {
+          message.loading({ 
+            content: `已处理 ${processedSheets}/${sheetConfigs.length} 个工作表...`, 
+            key: 'excelParsing', 
+            duration: 0 
+          });
+        }
+      }
+
+      // 更新进度提示
+      message.loading({ 
+        content: '正在整理数据字段...', 
+        key: 'excelParsing', 
+        duration: 0 
+      });
+
+      // 更新状态
+      setAllFieldsData(rawData);
+      
+      // 计算可用字段(按工作表分类)
+      const availableFieldsBySheet = getAllAvailableFields(rawData);
+      
+      // 完成解析,显示成功消息
+      message.success({
+        content: `Excel文件解析成功!共处理 ${Object.keys(rawData).length} 个工作表,${totalTables} 个数据表`,
+        key: 'excelParsing',
+        duration: 3
+      });
+      
+      console.log('所有字段原始数据:', rawData);
+      console.log('按工作表分类的可用字段:', availableFieldsBySheet);
+      
+      // 返回原始数据、分类字段
+      return {
+        rawData,
+        availableFieldsBySheet,
+      };
+    } catch (error) {
+      console.error('文件处理错误:', error);
+      message.error({
+        content: '文件处理失败,请检查文件格式是否正确!',
+        key: 'excelParsing'
+      });
+      throw error;
+    }
+  };
+
+  // 根据当前配置动态生成导出数据
+  const generateExportData = (sheetConfigs: SheetConfig[]) => {
+    // 如果没有原始数据,直接返回空对象
+    if (Object.keys(allFieldsData).length === 0) {
+      return {};
+    }
+    
+    const exportData: { [key: string]: ExcelRow[] } = {};
+    
+    // 处理每个工作表的数据
+    for (const sheetConfig of sheetConfigs) {
+      const rawDataForSheet = allFieldsData[sheetConfig.sheetName];
+      
+      if (!rawDataForSheet || rawDataForSheet.length === 0) {
+        continue;
+      }
+      
+      // 根据当前配置处理数据
+      exportData[sheetConfig.sheetName] = processDataForExport(
+        rawDataForSheet,
+        sheetConfig,
+        1
+      );
+    }
+    
+    // 只有当数据确实发生变化时才更新状态,避免不必要的渲染
+    const currentJsonData = JSON.stringify(jsonData);
+    const newJsonData = JSON.stringify(exportData);
+    
+    if (currentJsonData !== newJsonData) {
+      // 更新 jsonData 状态
+      setJsonData(exportData);
+      console.log('生成的导出数据:', exportData);
+    }
+    
+    return exportData;
+  };
+
+  const getFieldsBySheet = getAllAvailableFields;
+
+  return {
+    jsonData,
+    allFieldsData,
+    parseExcelFile,
+    generateExportData,
+    getFieldsBySheet
+  };
+}; 

+ 79 - 0
src/client/admin/components/ExcelToJson/hooks/useSheetConfig.ts

@@ -0,0 +1,79 @@
+import { useState } from 'react';
+import { message } from 'antd';
+import { ExcelConfig, SheetConfig } from '../types';
+import { defaultSheetConfig } from '../config';
+
+export const useSheetConfig = (initialConfig: ExcelConfig, useDefaultSheet: boolean = false) => {
+  const [config, setConfig] = useState<ExcelConfig>(initialConfig);
+
+  const getCurrentSheetConfig = () => config.sheets[config.activeSheetIndex];
+
+  const addNewSheet = () => {
+    const emptySheetConfig: SheetConfig = {
+      headerRowIndex: 1,
+      orderNumberRow: 0,
+      orderNumberCol: 0,
+      dataStartRow: 2,
+      endMarker: '',
+      sheetName: `工作表${config.sheets.length + 1}`,
+      exportFields: [],
+      fieldMappings: {},
+      requiredFields: []
+    };
+
+    const newSheetConfig = useDefaultSheet 
+      ? { ...defaultSheetConfig, sheetName: `工作表${config.sheets.length + 1}` }
+      : emptySheetConfig;
+
+    const newConfig = {
+      ...config,
+      sheets: [...config.sheets, newSheetConfig],
+      activeSheetIndex: config.sheets.length
+    };
+    setConfig(newConfig);
+    return newConfig;
+  };
+
+  const removeSheet = (index: number) => {
+    if (config.sheets.length === 1) {
+      message.warning('至少需要保留一个工作表配置!');
+      return config;
+    }
+    const newConfig = {
+      ...config,
+      sheets: config.sheets.filter((_, i) => i !== index),
+      activeSheetIndex: Math.min(config.activeSheetIndex, config.sheets.length - 2)
+    };
+    setConfig(newConfig);
+    return newConfig;
+  };
+
+  const updateSheetConfig = (index: number, updates: Partial<SheetConfig>) => {
+    const newConfig = {
+      ...config,
+      sheets: [...config.sheets]
+    };
+    newConfig.sheets[index] = {
+      ...newConfig.sheets[index],
+      ...updates
+    };
+    setConfig(newConfig);
+    return newConfig;
+  };
+
+  const setActiveSheet = (index: number) => {
+    setConfig((prevConfig) => ({
+      ...prevConfig,
+      activeSheetIndex: index
+    }));
+  };
+
+  return {
+    config,
+    getCurrentSheetConfig,
+    addNewSheet,
+    removeSheet,
+    updateSheetConfig,
+    setActiveSheet
+  };
+}; 

+ 267 - 0
src/client/admin/components/ExcelToJson/index.tsx

@@ -0,0 +1,267 @@
+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;
+  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?.template_name || '');
+  const [templateKey, setTemplateKey] = useState<string>(initialValue?.template_key || '');
+  
+  const { 
+    jsonData, 
+    allFieldsData, 
+    parseExcelFile, 
+    generateExportData, 
+    getFieldsBySheet 
+  } = useExcelParser();
+  const { 
+    config, 
+    addNewSheet,
+    removeSheet,
+    updateSheetConfig,
+    setActiveSheet
+  } = useSheetConfig(
+    initialValue?.template_config || (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?.template_config) {
+      // 如果有初始值,可以在这里处理
+      console.log('加载已有模板配置', initialValue.template_config);
+    }
+  }, [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> = {
+      template_name: templateName || '新模板',
+      template_key: templateKey || `template_${Date.now()}`,
+      template_config: 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; 

+ 74 - 0
src/client/admin/components/ExcelToJson/styles.css

@@ -0,0 +1,74 @@
+.compact-tabs .ant-tabs-nav {
+  margin-bottom: 0 !important;
+  padding: 0 8px;
+}
+
+.compact-tabs .ant-tabs-tab {
+  padding: 4px 8px !important;
+  margin: 0 2px !important;
+}
+
+.compact-tabs .ant-tabs-tab-btn {
+  font-size: 13px;
+}
+
+/* 表单样式优化 */
+.ant-form-item-label > label {
+  font-size: 13px !important;
+  height: 28px !important;
+}
+
+.ant-form-item-explain {
+  font-size: 12px;
+  min-height: 20px !important;
+}
+
+.ant-card-head {
+  min-height: 36px !important;
+  padding: 0 12px !important;
+}
+
+.ant-card-head-title {
+  padding: 8px 0 !important;
+  font-size: 13px !important;
+}
+
+/* 输入框样式优化 */
+.ant-input-number-group-addon {
+  padding: 0 8px !important;
+}
+
+.ant-select-selection-placeholder,
+.ant-select-selection-item {
+  font-size: 13px !important;
+}
+
+/* 表格样式优化 */
+.ant-table {
+  font-size: 13px !important;
+}
+
+.ant-table-thead > tr > th {
+  padding: 8px 6px !important;
+  font-size: 13px !important;
+}
+
+.ant-table-tbody > tr > td {
+  padding: 6px !important;
+}
+
+/* Tabs容器样式 */
+.compact-tabs {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+
+.compact-tabs .ant-tabs-content {
+  height: 100%;
+  overflow: hidden;
+}
+
+.compact-tabs .ant-tabs-tabpane {
+  height: 100%;
+} 

+ 69 - 0
src/client/admin/components/ExcelToJson/types.ts

@@ -0,0 +1,69 @@
+export interface ExcelRow {
+  [key: string]: string | number | null | undefined;
+  _id?: string;
+  tableIndex?: number;
+}
+
+export interface SheetConfig {
+  /** 表头行号 */
+  headerRowIndex: number;
+  /** 订单号行号 */
+  orderNumberRow: number;
+  /** 订单号列号 */
+  orderNumberCol: number;
+  /** 产品名称行号 */
+  productNameRow?: number;
+  /** 产品名称列号 */
+  productNameCol?: number;
+  /** 数据起始行号 */
+  dataStartRow: number;
+  /** 终止标记 */
+  endMarker: string;
+  /** 工作表名称 */
+  sheetName: string;
+  /** 导出字段 */
+  exportFields: string[];
+  /** 字段映射 */
+  fieldMappings: { [key: string]: string };
+  /** 必需字段 */
+  requiredFields: string[];
+  /** 是否支持多表格模式 */
+  isMultiTable?: boolean;
+  /** 多表格模式下,新表格的表头行号偏移量(相对于上一个表格的结束标记) */
+  multiTableHeaderOffset?: number;
+  /** 多表格模式下,新表格的数据起始行偏移量(相对于新表头) */
+  multiTableDataOffset?: number;
+  /** 多表格模式下,订单号行相对于表头的偏移量(负数表示在表头之前) */
+  multiTableOrderNumberOffset?: number;
+  /** 多表格模式下,产品名称行相对于表头的偏移量(负数表示在表头之前) */
+  multiTableProductNameOffset?: number;
+}
+
+export interface ExcelConfig {
+  sheets: SheetConfig[];
+  activeSheetIndex: number;
+}
+
+export type ViewMode = 'table' | 'json';
+
+export interface DataViewerProps {
+  data: { [key: string]: ExcelRow[] };
+  viewMode: ViewMode;
+  config: ExcelConfig;
+}
+
+export interface SheetConfigProps {
+  config: SheetConfig;
+  availableFields: string[];
+  onConfigChange: (config: Partial<SheetConfig>) => void;
+  onRemove?: () => void;
+}
+
+export interface FileUploadProps {
+  onFileChange: (file: File) => Promise<void>;
+}
+
+export interface ExportButtonProps {
+  data: { [key: string]: ExcelRow[] };
+  disabled?: boolean;
+} 

+ 1 - 1
src/server/utils/excelParser.ts

@@ -1,5 +1,5 @@
+import type { SheetConfig } from '@/client/admin/components/ExcelToJson/types';
 import * as XLSX from 'xlsx';
-import type { SheetConfig } from '@/share/exceltypes';
 
 // Excel数据行类型
 export interface ExcelRow {

+ 48 - 59
src/share/exceltypes.ts

@@ -1,69 +1,58 @@
-export interface ExcelRow {
-    [key: string]: string | number | null | undefined;
-    _id?: string;
-    tableIndex?: number;
+import type { ExcelConfig } from "@/client/admin/components/ExcelToJson/types";
+
+
+
+  // 模板相关类型
+export interface Template {
+    id: number;
+    template_name: string;
+    template_key: string;
+    template_config: ExcelConfig;
+    created_by: number;
+    is_deleted?: number;
+    created_at: string;
+    updated_at: string;
   }
   
-  export interface SheetConfig {
-    /** 表头行号 */
-    headerRowIndex: number;
-    /** 订单号行号 */
-    orderNumberRow: number;
-    /** 订单号列号 */
-    orderNumberCol: number;
-    /** 产品名称行号 */
-    productNameRow?: number;
-    /** 产品名称列号 */
-    productNameCol?: number;
-    /** 数据起始行号 */
-    dataStartRow: number;
-    /** 终止标记 */
-    endMarker: string;
-    /** 工作表名称 */
-    sheetName: string;
-    /** 导出字段 */
-    exportFields: string[];
-    /** 字段映射 */
-    fieldMappings: { [key: string]: string };
-    /** 必需字段 */
-    requiredFields: string[];
-    /** 是否支持多表格模式 */
-    isMultiTable?: boolean;
-    /** 多表格模式下,新表格的表头行号偏移量(相对于上一个表格的结束标记) */
-    multiTableHeaderOffset?: number;
-    /** 多表格模式下,新表格的数据起始行偏移量(相对于新表头) */
-    multiTableDataOffset?: number;
-    /** 多表格模式下,订单号行相对于表头的偏移量(负数表示在表头之前) */
-    multiTableOrderNumberOffset?: number;
-    /** 多表格模式下,产品名称行相对于表头的偏移量(负数表示在表头之前) */
-    multiTableProductNameOffset?: number;
+  export interface TemplateAddForm {
+    template_name: string;
+    template_key: string;
+    template_config: ExcelConfig;
+    created_by?: number;
   }
   
-  export interface ExcelConfig {
-    sheets: SheetConfig[];
-    activeSheetIndex: number;
+  export interface TemplateEditForm extends TemplateAddForm {
+    id: number;
   }
   
-  export type ViewMode = 'table' | 'json';
-  
-  export interface DataViewerProps {
-    data: { [key: string]: ExcelRow[] };
-    viewMode: ViewMode;
-    config: ExcelConfig;
-  }
-  
-  export interface SheetConfigProps {
-    config: SheetConfig;
-    availableFields: string[];
-    onConfigChange: (config: Partial<SheetConfig>) => void;
-    onRemove?: () => void;
+  // API 测试记录
+  export interface ApiTestRecord {
+    method: string;
+    url: string;
+    contentType: string;
+    headers?: string;
+    body?: string;
+    timestamp: string;
+    status?: number;
+    response?: any;
   }
   
-  export interface FileUploadProps {
-    onFileChange: (file: File) => Promise<void>;
+  // Excel转换API相关接口
+  export interface ConvertRequest {
+    templateId?: string;     // 模板ID,如果提供则使用模板配置
+    input: string;          // 文件URL或Base64编码
+    config?: any;           // 可选的自定义配置,覆盖模板配置
   }
   
-  export interface ExportButtonProps {
-    data: { [key: string]: ExcelRow[] };
-    disabled?: boolean;
-  } 
+  export interface ConvertResponse {
+    success: boolean;
+    data?: { 
+      [sheetName: string]: any[] 
+    };
+    warnings?: string[];
+    availableFields?: { 
+      [sheetName: string]: string[] 
+    };
+    totalTables?: number;
+  } 
+