From bb71a2e6ae4ccaf7a712ce5ed29f0f5ffade52bd Mon Sep 17 00:00:00 2001
From: vincentlu <t1341870251@gmail.com>
Date: 星期五, 30 一月 2026 13:58:28 +0800
Subject: [PATCH] #

---
 zy-acs-flow/package.json                                                               |    5 
 zy-acs-flow/src/page/components/ImportXlsxModal.jsx                                    |  191 +++++++++++++++++
 zy-acs-flow/package-lock.json                                                          |  106 +++++++++
 zy-acs-flow/src/i18n/en.js                                                             |    4 
 zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/CodeController.java |    2 
 zy-acs-flow/public/imports/code_import_template.xlsx                                   |    0 
 zy-acs-flow/src/page/components/ImportTxtModal.jsx                                     |  213 +++++++++++++++++++
 zy-acs-flow/src/page/components/useExcelParse.js                                       |   77 +++++++
 zy-acs-flow/src/i18n/zh.js                                                             |    4 
 zy-acs-manager/src/main/java/com/zy/acs/manager/common/utils/ExcelUtil.java            |   16 +
 zy-acs-flow/src/page/code/CodeList.jsx                                                 |    8 
 zy-acs-flow/src/page/components/ImportButton.jsx                                       |   17 +
 12 files changed, 624 insertions(+), 19 deletions(-)

diff --git a/zy-acs-flow/package-lock.json b/zy-acs-flow/package-lock.json
index e6cdf6a..b000d2c 100644
--- a/zy-acs-flow/package-lock.json
+++ b/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",
diff --git a/zy-acs-flow/package.json b/zy-acs-flow/package.json
index 4340f1b..77bdb38 100644
--- a/zy-acs-flow/package.json
+++ b/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"
-}
+}
\ No newline at end of file
diff --git a/zy-acs-flow/public/imports/code_import_template.xlsx b/zy-acs-flow/public/imports/code_import_template.xlsx
new file mode 100644
index 0000000..929295f
--- /dev/null
+++ b/zy-acs-flow/public/imports/code_import_template.xlsx
Binary files differ
diff --git a/zy-acs-flow/src/i18n/en.js b/zy-acs-flow/src/i18n/en.js
index f80d1d6..a01ba94 100644
--- a/zy-acs-flow/src/i18n/en.js
+++ b/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',
             },
diff --git a/zy-acs-flow/src/i18n/zh.js b/zy-acs-flow/src/i18n/zh.js
index 914a9ce..8ea789e 100644
--- a/zy-acs-flow/src/i18n/zh.js
+++ b/zy-acs-flow/src/i18n/zh.js
@@ -62,9 +62,9 @@
             import: {
                 title: '瀵煎叆',
                 stop: '鍋滄瀵煎叆',
-                msg: '杩欐槸涓�涓彲浠ョ敤浣滄ā鏉跨殑绀轰緥 CSV 鏂囦欢',
+                msg: '杩欐槸涓�涓彲浠ョ敤浣滄ā鏉跨殑绀轰緥鏂囦欢',
                 tips: '姝e湪瀵煎叆涓紝璇蜂笉瑕佸叧闂绐楀彛',
-                err: '鏃犳硶瀵煎叆姝ゆ枃浠讹紝璇风‘淇濇偍鎻愪緵浜嗘湁鏁堢殑 CSV 鏂囦欢',
+                err: '鏃犳硶瀵煎叆姝ゆ枃浠讹紝璇风‘淇濇偍鎻愪緵浜嗘湁鏁堢殑鏂囦欢',
                 download: '涓嬭浇瀵煎叆妯℃澘',
                 result: '瀵煎叆瀹屾垚銆傚凡瀵煎叆 %{success} 鎴愬姛, 鍜� %{error} 澶辫触',
             },
diff --git a/zy-acs-flow/src/page/code/CodeList.jsx b/zy-acs-flow/src/page/code/CodeList.jsx
index 0746122..797ea44 100644
--- a/zy-acs-flow/src/page/code/CodeList.jsx
+++ b/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>
                 )}
diff --git a/zy-acs-flow/src/page/components/ImportButton.jsx b/zy-acs-flow/src/page/components/ImportButton.jsx
index 147ec9e..f382b9f 100644
--- a/zy-acs-flow/src/page/components/ImportButton.jsx
+++ b/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} />)}
         </>
     );
 };
diff --git a/zy-acs-flow/src/page/components/ImportTxtModal.jsx b/zy-acs-flow/src/page/components/ImportTxtModal.jsx
new file mode 100644
index 0000000..a42b465
--- /dev/null
+++ b/zy-acs-flow/src/page/components/ImportTxtModal.jsx
@@ -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;
\ No newline at end of file
diff --git a/zy-acs-flow/src/page/components/ImportXlsxModal.jsx b/zy-acs-flow/src/page/components/ImportXlsxModal.jsx
new file mode 100644
index 0000000..9fc24d1
--- /dev/null
+++ b/zy-acs-flow/src/page/components/ImportXlsxModal.jsx
@@ -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;
\ No newline at end of file
diff --git a/zy-acs-flow/src/page/components/useExcelParse.js b/zy-acs-flow/src/page/components/useExcelParse.js
new file mode 100644
index 0000000..1e79d19
--- /dev/null
+++ b/zy-acs-flow/src/page/components/useExcelParse.js
@@ -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]);
+}
\ No newline at end of file
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/common/utils/ExcelUtil.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/common/utils/ExcelUtil.java
index f7f47b6..b7f3266 100644
--- a/zy-acs-manager/src/main/java/com/zy/acs/manager/common/utils/ExcelUtil.java
+++ b/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);
         }
 
diff --git a/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/CodeController.java b/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/CodeController.java
index e4b5621..462d8a3 100644
--- a/zy-acs-manager/src/main/java/com/zy/acs/manager/manager/controller/CodeController.java
+++ b/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);

--
Gitblit v1.9.1