#
vincentlu
7 天以前 225130190f0ec44cc1f82245655a635c949d257c
#
4个文件已添加
8个文件已修改
643 ■■■■■ 已修改文件
zy-acs-flow/package-lock.json 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/package.json 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/public/imports/code_import_template.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/i18n/en.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/i18n/zh.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/page/code/CodeList.jsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/page/components/ImportButton.jsx 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/page/components/ImportTxtModal.jsx 213 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/page/components/ImportXlsxModal.jsx 191 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/src/page/components/useExcelParse.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-manager/src/main/java/com/zy/acs/manager/common/utils/ExcelUtil.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/CodeController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zy-acs-flow/package-lock.json
@@ -24,7 +24,8 @@
        "react-router-dom": "^6.26.1",
        "react-syntax-highlighter": "^15.5.0",
        "three": "^0.155.0",
        "tweedle.js": "^2.1.0"
        "tweedle.js": "^2.1.0",
        "xlsx": "^0.18.5"
      },
      "devDependencies": {
        "@types/node": "^20.10.7",
@@ -2339,6 +2340,15 @@
        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
      }
    },
    "node_modules/adler-32": {
      "version": "1.3.1",
      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
      "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
      "license": "Apache-2.0",
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/ajv": {
      "version": "6.12.6",
      "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@@ -2702,6 +2712,19 @@
        }
      ]
    },
    "node_modules/cfb": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
      "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
      "license": "Apache-2.0",
      "dependencies": {
        "adler-32": "~1.3.0",
        "crc-32": "~1.2.0"
      },
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/chalk": {
      "version": "2.4.2",
      "resolved": "https://registry.npmmirror.com/chalk/-/chalk-2.4.2.tgz",
@@ -2748,6 +2771,15 @@
      "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
      "engines": {
        "node": ">=6"
      }
    },
    "node_modules/codepage": {
      "version": "1.15.0",
      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
      "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
      "license": "Apache-2.0",
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/color-convert": {
@@ -2808,6 +2840,18 @@
      },
      "engines": {
        "node": ">=10"
      }
    },
    "node_modules/crc-32": {
      "version": "1.2.2",
      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
      "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
      "license": "Apache-2.0",
      "bin": {
        "crc32": "bin/crc32.njs"
      },
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/cross-spawn": {
@@ -3785,6 +3829,15 @@
      "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
      "engines": {
        "node": ">=0.4.x"
      }
    },
    "node_modules/frac": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
      "license": "Apache-2.0",
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/fs.realpath": {
@@ -5982,6 +6035,18 @@
        "node": ">=6"
      }
    },
    "node_modules/ssf": {
      "version": "0.11.2",
      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
      "license": "Apache-2.0",
      "dependencies": {
        "frac": "~1.1.2"
      },
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/strict-uri-encode": {
      "version": "2.0.0",
      "resolved": "https://registry.npmmirror.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
@@ -6552,6 +6617,24 @@
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/wmf": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
      "license": "Apache-2.0",
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/word": {
      "version": "0.3.0",
      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
      "license": "Apache-2.0",
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/word-wrap": {
      "version": "1.2.5",
      "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -6567,6 +6650,27 @@
      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
      "dev": true
    },
    "node_modules/xlsx": {
      "version": "0.18.5",
      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
      "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
      "license": "Apache-2.0",
      "dependencies": {
        "adler-32": "~1.3.0",
        "cfb": "~1.2.1",
        "codepage": "~1.15.0",
        "crc-32": "~1.2.1",
        "ssf": "~0.11.2",
        "wmf": "~1.0.1",
        "word": "~0.3.0"
      },
      "bin": {
        "xlsx": "bin/xlsx.njs"
      },
      "engines": {
        "node": ">=0.8"
      }
    },
    "node_modules/xtend": {
      "version": "4.0.2",
      "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz",
zy-acs-flow/package.json
@@ -28,7 +28,8 @@
    "react-router-dom": "^6.26.1",
    "react-syntax-highlighter": "^15.5.0",
    "three": "^0.155.0",
    "tweedle.js": "^2.1.0"
    "tweedle.js": "^2.1.0",
    "xlsx": "^0.18.5"
  },
  "devDependencies": {
    "@types/node": "^20.10.7",
@@ -46,4 +47,4 @@
    "vite": "^5.3.5"
  },
  "name": "cool-admin-flow"
}
}
zy-acs-flow/public/imports/code_import_template.xlsx
Binary files differ
zy-acs-flow/src/i18n/en.js
@@ -62,9 +62,9 @@
            import: {
                title: 'Import',
                stop: 'Stop import',
                msg: 'Here is a sample CSV file you can use as a template',
                msg: 'Here is a sample file you can use as a template',
                tips: 'The import is running, please do not close this tab.',
                err: 'Failed to import this file, please make sure your provided a valid CSV file.',
                err: 'Failed to import this file, please make sure your provided a valid file.',
                download: 'Download Import Template',
                result: 'Contacts import complete. Imported %{success} success, with %{error} errors',
            },
zy-acs-flow/src/i18n/zh.js
@@ -62,9 +62,9 @@
            import: {
                title: '导入',
                stop: '停止导入',
                msg: '这是一个可以用作模板的示例 CSV 文件',
                msg: '这是一个可以用作模板的示例文件',
                tips: '正在导入中,请不要关闭此窗口',
                err: '无法导入此文件,请确保您提供了有效的 CSV 文件',
                err: '无法导入此文件,请确保您提供了有效的文件',
                download: '下载导入模板',
                result: '导入完成。已导入 %{success} 成功, 和 %{error} 失败',
            },
zy-acs-flow/src/page/code/CodeList.jsx
@@ -46,8 +46,10 @@
import ImportButton from '../components/ImportButton'
import { useCodeImport } from './useCodeImport';
import * as importTemp from './importTemp.csv?raw';
const IMPORT_TEMP_URL = `data:text/csv;name=crm_contacts_sample.csv;charset=utf-8,${encodeURIComponent(importTemp.default)}`;
// import * as importTemp from './importTemp.csv?raw';
// const IMPORT_TEMP_URL = `data:text/csv;name=crm_contacts_sample.csv;charset=utf-8,${encodeURIComponent(importTemp.default)}`;
const IMPORT_TEMP_URL = '/imports/code_import_template.xlsx';
const StyledDatagrid = styled(DatagridConfigurable)(({ theme }) => ({
    '& .css-1vooibu-MuiSvgIcon-root': {
@@ -126,7 +128,7 @@
                        <FilterButton />
                        <MyCreateButton onClick={() => { setCreateDialog(true) }} />
                        <SelectColumnsButton preferenceKey='code' />
                        <ImportButton importTemp={IMPORT_TEMP_URL} useCodeImport={useCodeImport} onceBatch={10} />
                        <ImportButton type="xlsx" importTemp={IMPORT_TEMP_URL} useImport={useCodeImport} onceBatch={10} />
                        <MyExportButton />
                    </TopToolbar>
                )}
zy-acs-flow/src/page/components/ImportButton.jsx
@@ -2,11 +2,18 @@
import { useState } from 'react';
import { Button } from 'react-admin';
import ImportModal from './ImportModal';
import ImportTxtModal from './ImportTxtModal';
import ImportXlsxModal from './ImportXlsxModal';
const ImportButton = (props) => {
const ImportButton = ({
    type = 'csv', // csv, txt, xlsx,
    variant = 'text',
    ...props
}) => {
    const [modalOpen, setModalOpen] = useState(false);
    const handleOpenModal = () => {
    const handleOpenModal = (e) => {
        e.stopPropagation();
        setModalOpen(true);
    };
@@ -17,12 +24,14 @@
    return (
        <>
            <Button
                variant={variant}
                startIcon={<UploadIcon />}
                label="common.action.import.title"
                onClick={handleOpenModal}
            />
            <ImportModal open={modalOpen} onClose={handleCloseModal} {...props} />
            {type === 'csv' && (<ImportModal open={modalOpen} onClose={handleCloseModal} {...props} />)}
            {type === 'txt' && (<ImportTxtModal open={modalOpen} onClose={handleCloseModal} {...props} />)}
            {type === 'xlsx' && (<ImportXlsxModal open={modalOpen} onClose={handleCloseModal} {...props} />)}
        </>
    );
};
zy-acs-flow/src/page/components/ImportTxtModal.jsx
New file
@@ -0,0 +1,213 @@
import { useEffect, useState } from 'react';
import { Box, CircularProgress, Stack, Typography } from '@mui/material';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MuiLink from '@mui/material/Link';
import {
    Button,
    FileField,
    FileInput,
    Form,
    Toolbar,
    useRefresh,
    useTranslate
} from 'react-admin';
import { Link } from 'react-router-dom';
import DialogCloseButton from './DialogCloseButton';
import { usePapaParse } from './usePapaParse';
const ImportTxtModal = ({ open, onClose, useImport, params, title = '', onceBatch = 10 }) => {
    const refresh = useRefresh();
    const translate = useTranslate();
    const { processBatch } = useImport();
    const { importer, parseCsv, reset } = usePapaParse({
        batchSize: onceBatch,
        processBatch,
        params,
    });
    const [file, setFile] = useState(null);
    useEffect(() => {
        if (importer.state === 'complete') {
            refresh();
        }
    }, [importer.state, refresh]);
    const handleFileChange = (file) => {
        setFile(file);
    };
    const startImport = async () => {
        if (!file) {
            return;
        }
        parseCsv(file);
    };
    const handleClose = () => {
        reset();
        onClose();
    };
    const handleReset = (e) => {
        e.preventDefault();
        reset();
    };
    return (
        <Dialog
            open={open}
            maxWidth="md"
            fullWidth
            onClick={(e) => e.stopPropagation()}
        >
            <DialogCloseButton onClose={handleClose} />
            <DialogTitle>{translate('common.action.import.title')} {title}</DialogTitle>
            <DialogContent>
                <Form>
                    <Stack spacing={2}>
                        {importer.state === 'running' && (
                            <Stack gap={2}>
                                <Alert
                                    severity="info"
                                    action={
                                        <Box
                                            sx={{
                                                display: 'flex',
                                                height: '100%',
                                                alignItems: 'center',
                                                padding: '0',
                                            }}
                                        >
                                            <CircularProgress size={20} />
                                        </Box>
                                    }
                                    sx={{
                                        alignItems: 'center',
                                        '& .MuiAlert-action': {
                                            padding: 0,
                                            marginRight: 0,
                                        },
                                    }}
                                >
                                    {translate('common.action.import.tips')}
                                </Alert>
                                <Typography variant="body2">
                                    Imported{' '}
                                    <strong>
                                        {importer.importCount} /{' '}
                                        {importer.rowCount}
                                    </strong>{' '}
                                    contacts, with{' '}
                                    <strong>{importer.errorCount}</strong>{' '}
                                    errors.
                                    {importer.remainingTime !== null && (
                                        <>
                                            {' '}
                                            Estimated remaining time:{' '}
                                            <strong>
                                                {millisecondsToTime(
                                                    importer.remainingTime
                                                )}
                                            </strong>
                                            .{' '}
                                            <MuiLink
                                                href="#"
                                                onClick={handleReset}
                                                color="error"
                                            >
                                                {translate('common.action.import.stop')}
                                            </MuiLink>
                                        </>
                                    )}
                                </Typography>
                            </Stack>
                        )}
                        {importer.state === 'error' && (
                            <Alert severity="error">
                                {translate('common.action.import.err')}
                            </Alert>
                        )}
                        {importer.state === 'complete' && (
                            <>
                                <Alert severity="success">
                                    {translate('common.action.import.result', {
                                        success: importer.importCount,
                                        error: importer.errorCount
                                    })}
                                </Alert>
                                {importer.errorMsg && (
                                    <Alert severity="error">
                                        {importer.errorMsg}
                                    </Alert>
                                )}
                            </>
                        )}
                        {importer.state === 'idle' && (
                            <>
                                <FileInput
                                    source="txt"
                                    label="Txt File"
                                    accept={{ 'text/plain': ['.txt'] }}
                                    onChange={handleFileChange}
                                >
                                    <FileField source="src" title="title" />
                                </FileInput>
                            </>
                        )}
                    </Stack>
                </Form>
            </DialogContent>
            <DialogActions
                sx={{
                    p: 0,
                    justifyContent: 'flex-start',
                }}
            >
                <Toolbar
                    sx={{
                        width: '100%',
                    }}
                >
                    {importer.state === 'idle' ? (
                        <>
                            <Button
                                label="common.action.import.title"
                                variant="contained"
                                onClick={startImport}
                                disabled={!file}
                            />
                        </>
                    ) : (
                        <Button
                            label="ra.action.close"
                            onClick={handleClose}
                            disabled={importer.state === 'running'}
                        />
                    )}
                </Toolbar>
            </DialogActions>
        </Dialog>
    );
}
function millisecondsToTime(ms) {
    var seconds = Math.floor((ms / 1000) % 60);
    var minutes = Math.floor((ms / (60 * 1000)) % 60);
    return `${minutes}m ${seconds}s`;
}
export default ImportTxtModal;
zy-acs-flow/src/page/components/ImportXlsxModal.jsx
New file
@@ -0,0 +1,191 @@
// ImportXlsxModal.jsx
import { useEffect, useState } from 'react';
import { Box, CircularProgress, Stack, Typography } from '@mui/material';
import Alert from '@mui/material/Alert';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import MuiLink from '@mui/material/Link';
import {
    Button,
    FileField,
    FileInput,
    Form,
    Toolbar,
    useRefresh,
    useTranslate,
} from 'react-admin';
import DialogCloseButton from './DialogCloseButton';
import { useExcelParse } from './useExcelParse';
const ImportXlsxModal = ({
    open,
    onClose,
    importTemp,
    useImport,
    params,
    title = '',
    onceBatch = 10,
}) => {
    const refresh = useRefresh();
    const t = useTranslate();
    const { processBatch } = useImport();
    const { importer, parseExcel, reset } = useExcelParse({
        batchSize: onceBatch,
        processBatch,
        params,
    });
    const [file, setFile] = useState(null);
    useEffect(() => {
        if (importer.state === 'complete') {
            refresh();
        }
    }, [importer.state, refresh]);
    const handleFileChange = (val) => {
        const f = Array.isArray(val) ? val[0]?.rawFile ?? val[0] : val?.rawFile ?? val;
        setFile(f || null);
    };
    const startImport = async () => {
        if (!file) return;
        parseExcel(file);
    };
    const handleClose = () => {
        reset();
        setFile(null);
        onClose();
    };
    const handleReset = (e) => {
        e.preventDefault();
        reset();
    };
    const nameMatch = importTemp?.split('/').pop() || 'import_template.xlsx';
    return (
        <Dialog open={open} maxWidth="md" fullWidth onClick={(e) => e.stopPropagation()}>
            <DialogCloseButton onClose={handleClose} />
            <DialogTitle>{t('common.action.import.title')} {title}</DialogTitle>
            <DialogContent>
                <Form>
                    <Stack spacing={2}>
                        <Alert
                            severity="info"
                            action={
                                importTemp ? (
                                    <Button
                                        component="a"
                                        label="common.action.import.download"
                                        color="info"
                                        href={importTemp}
                                        download={nameMatch}
                                    />
                                ) : null
                            }
                            sx={{
                                alignItems: 'center',
                                '& .MuiAlert-action': { p: 0, mr: 0 },
                            }}
                        >
                            {t('common.action.import.msg', { _: '这是一个可以用作模板的示例 Excel 文件' })}
                        </Alert>
                        {importer.state === 'running' && (
                            <Stack gap={2}>
                                <Alert
                                    severity="info"
                                    action={
                                        <Box sx={{ display: 'flex', alignItems: 'center', p: 0 }}>
                                            <CircularProgress size={20} />
                                        </Box>
                                    }
                                    sx={{ alignItems: 'center', '& .MuiAlert-action': { p: 0, mr: 0 } }}
                                >
                                    {t('common.action.import.tips')}
                                </Alert>
                                <Typography variant="body2">
                                    Imported <strong>{importer.importCount}</strong> /{' '}
                                    <strong>{importer.rowCount}</strong> rows, with{' '}
                                    <strong>{importer.errorCount}</strong> errors.
                                    {importer.remainingTime !== null && (
                                        <>
                                            {' '}ETA: <strong>{millisecondsToTime(importer.remainingTime)}</strong>.{' '}
                                            <MuiLink href="#" onClick={handleReset} color="error">
                                                {t('common.action.import.stop')}
                                            </MuiLink>
                                        </>
                                    )}
                                </Typography>
                            </Stack>
                        )}
                        {importer.state === 'error' && (
                            <Alert severity="error">{t('common.action.import.err')}</Alert>
                        )}
                        {importer.state === 'complete' && (
                            <>
                                <Alert severity="success">
                                    {t('common.action.import.result', {
                                        success: importer.importCount,
                                        error: importer.errorCount,
                                    })}
                                </Alert>
                                {importer.errorMsg && <Alert severity="error">{importer.errorMsg}</Alert>}
                            </>
                        )}
                        {importer.state === 'idle' && (
                            <>
                                <FileInput
                                    source="xlsx"
                                    label="Excel File (.xlsx / .xls)"
                                    accept={{
                                        'application/vnd.ms-excel': ['.xls'],
                                        'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
                                    }}
                                    onChange={handleFileChange}
                                >
                                    <FileField source="src" title="title" />
                                </FileInput>
                            </>
                        )}
                    </Stack>
                </Form>
            </DialogContent>
            <DialogActions sx={{ p: 0, justifyContent: 'flex-start' }}>
                <Toolbar sx={{ width: '100%' }}>
                    {importer.state === 'idle' ? (
                        <Button
                            label="common.action.import.title"
                            variant="contained"
                            onClick={startImport}
                            disabled={!file}
                        />
                    ) : (
                        <Button
                            label="ra.action.close"
                            onClick={handleClose}
                            disabled={importer.state === 'running'}
                        />
                    )}
                </Toolbar>
            </DialogActions>
        </Dialog>
    );
};
function millisecondsToTime(ms) {
    const seconds = Math.floor((ms / 1000) % 60);
    const minutes = Math.floor((ms / (60 * 1000)) % 60);
    return `${minutes}m ${seconds}s`;
}
export default ImportXlsxModal;
zy-acs-flow/src/page/components/useExcelParse.js
New file
@@ -0,0 +1,77 @@
import * as XLSX from 'xlsx';
import { useCallback, useMemo, useRef, useState } from 'react';
export function useExcelParse({ batchSize = 10, processBatch, params }) {
    const importIdRef = useRef(0);
    const [importer, setImporter] = useState({ state: 'idle' });
    const reset = useCallback(() => {
        setImporter({ state: 'idle' });
        importIdRef.current += 1;
    }, []);
    const parseExcel = useCallback(async (file) => {
        if (!file) return;
        setImporter({ state: 'parsing' });
        const importId = importIdRef.current;
        try {
            const buf = await file.arrayBuffer();
            const wb = XLSX.read(buf, { type: 'array' });
            const sheet = wb.SheetNames[0];
            const rows = XLSX.utils.sheet_to_json(wb.Sheets[sheet], { defval: '' });
            setImporter({
                state: 'running',
                rowCount: rows.length,
                importCount: 0,
                errorCount: 0,
                remainingTime: null,
            });
            let totalTime = 0;
            for (let i = 0; i < rows.length; i += batchSize) {
                if (importIdRef.current !== importId) return;
                const batch = rows.slice(i, i + batchSize);
                try {
                    const start = Date.now();
                    await processBatch(batch, params);
                    totalTime += Date.now() - start;
                    const mean = totalTime / (i + batch.length);
                    setImporter((prev) =>
                        prev.state === 'running'
                            ? {
                                ...prev,
                                importCount: prev.importCount + batch.length,
                                remainingTime: mean * (rows.length - (prev.importCount + batch.length)),
                            }
                            : prev
                    );
                } catch (err) {
                    setImporter((prev) =>
                        prev.state === 'running'
                            ? {
                                ...prev,
                                errorCount: prev.errorCount + batch.length,
                                errorMsg: prev.errorMsg
                                    ? `${prev.errorMsg}\n${err?.message || String(err)}`
                                    : (err?.message || String(err)),
                            }
                            : prev
                    );
                }
            }
            setImporter((prev) =>
                prev.state === 'running' ? { ...prev, state: 'complete', remainingTime: null } : prev
            );
        } catch (error) {
            setImporter({ state: 'error', error });
        }
    }, [batchSize, processBatch, params]);
    return useMemo(() => ({ importer, parseExcel, reset }), [importer, parseExcel, reset]);
}
zy-acs-manager/src/main/java/com/zy/acs/manager/common/utils/ExcelUtil.java
@@ -2,10 +2,10 @@
import com.zy.acs.framework.common.Cools;
import io.swagger.annotations.ApiModelProperty;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@@ -24,19 +24,24 @@
    public static void build(Workbook workbook, HttpServletResponse response) {
        response.reset();
        Http.cors(response);
        response.setContentType("application/octet-stream; charset=utf-8");
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; charset=utf-8");
        try {
            response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("export", "UTF-8"));
            response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("export.xlsx", "UTF-8"));
            workbook.write(response.getOutputStream());
            workbook.close();
        } catch (IOException ignore) {}
    }
    public static <T> Workbook create(List<T> list, Class<T> clz) {
        HSSFWorkbook workbook = new HSSFWorkbook();
        XSSFWorkbook workbook = new XSSFWorkbook();
        Sheet sheet = workbook.createSheet(clz.getSimpleName());
        Row header = sheet.createRow(0);
        Field[] fields = Cools.getAllFields(clz);
        int headerIdx = 0;
        for (Field field : fields) {
            if (Modifier.isFinal(field.getModifiers())
@@ -44,6 +49,7 @@
                    || Modifier.isTransient(field.getModifiers())) {
                continue;
            }
            String memo = "Undefined";
            if (field.isAnnotationPresent(ApiModelProperty.class)) {
                memo = field.getAnnotation(ApiModelProperty.class).value();
@@ -83,7 +89,7 @@
            }
        }
        for (int i = 0; i <= fields.length; i++) {
        for (int i = 0; i <= headerIdx; i++) {
            sheet.autoSizeColumn(i);
        }
zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/CodeController.java
@@ -17,6 +17,7 @@
import com.zy.acs.manager.manager.entity.Code;
import com.zy.acs.manager.manager.entity.CodeGap;
import com.zy.acs.manager.manager.entity.Route;
import com.zy.acs.manager.manager.enums.StatusType;
import com.zy.acs.manager.manager.service.CodeGapService;
import com.zy.acs.manager.manager.service.CodeService;
import com.zy.acs.manager.manager.service.RouteService;
@@ -159,6 +160,7 @@
            code.setUuid("code".concat(code.getData()));
//            code.setCorner(0);
            code.setScale(GsonUtils.toJson(Cools.add("x", 1).add("y", 1)));
            code.setStatus(StatusType.ENABLE.val);
            code.setCreateBy(userId);
            code.setCreateTime(now);
            code.setUpdateBy(userId);