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