| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | } |
| | | ] |
| | | }, |
| | | "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", |
| | |
| | | "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": { |
| | |
| | | }, |
| | | "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": { |
| | |
| | | "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": { |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "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", |
| | |
| | | "vite": "^5.3.5" |
| | | }, |
| | | "name": "cool-admin-flow" |
| | | } |
| | | } |
| | |
| | | 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', |
| | | }, |
| | |
| | | import: { |
| | | title: '导入', |
| | | stop: '停止导入', |
| | | msg: '这是一个可以用作模板的示例 CSV 文件', |
| | | msg: '这是一个可以用作模板的示例文件', |
| | | tips: '正在导入中,请不要关闭此窗口', |
| | | err: '无法导入此文件,请确保您提供了有效的 CSV 文件', |
| | | err: '无法导入此文件,请确保您提供了有效的文件', |
| | | download: '下载导入模板', |
| | | result: '导入完成。已导入 %{success} 成功, 和 %{error} 失败', |
| | | }, |
| | |
| | | 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': { |
| | |
| | | <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> |
| | | )} |
| | |
| | | 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); |
| | | }; |
| | | |
| | |
| | | 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} />)} |
| | | </> |
| | | ); |
| | | }; |
| New file |
| | |
| | | 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; |
| New file |
| | |
| | | // 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; |
| New file |
| | |
| | | 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]); |
| | | } |
| | |
| | | |
| | | 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; |
| | |
| | | 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()) |
| | |
| | | || Modifier.isTransient(field.getModifiers())) { |
| | | continue; |
| | | } |
| | | |
| | | String memo = "Undefined"; |
| | | if (field.isAnnotationPresent(ApiModelProperty.class)) { |
| | | memo = field.getAnnotation(ApiModelProperty.class).value(); |
| | |
| | | } |
| | | } |
| | | |
| | | for (int i = 0; i <= fields.length; i++) { |
| | | for (int i = 0; i <= headerIdx; i++) { |
| | | sheet.autoSizeColumn(i); |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | 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); |