Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: json xml 互转 #101

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"swiper": "^8.4.7",
"typescript": "5.0.4",
"xlsx": "^0.18.5",
"xml2js": "^0.6.2",
"xmorse": "^1.0.0",
"yarn": "^1.22.21"
},
Expand All @@ -95,6 +96,7 @@
"@types/react-dom": "18.2.4",
"@types/react-syntax-highlighter": "^15.5.11",
"@types/sql.js": "^1.4.9",
"@types/xml2js": "^0.4.14",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
Expand Down
293 changes: 293 additions & 0 deletions src/pages/json2xml.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import MainContent from '@/components/MainContent';
import alert from '@/components/Alert';
import { Box, Button, Stack } from '@mui/material';
import { useCallback, useEffect, useState } from 'react';
import AceEditor from 'react-ace';
import SwapHorizontalCircleIcon from '@mui/icons-material/SwapHorizontalCircle';
import { CopyToClipboard } from 'react-copy-to-clipboard';
import { parseStringPromise, Builder } from 'xml2js';

import 'ace-builds/src-noconflict/ext-language_tools';
import 'ace-builds/src-noconflict/theme-monokai';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-xml';

/**
* A => json
* B => xml
*
* 适用于两种语言的互转模版
* 1. 更改 import ace-builds/src-noconflict/mode-*
* 2. 更改所有 enum 枚举值
* 3. 在 a2b() 函数和 b2a() 函数中添加转换逻辑 return 转换结果
* */

// 转换顺序
enum ConvertType {
A2B = 1,
B2A = 2,
}
// 导出文件 MIME 类型;未知设为空
enum ConvertFileType {
A2B = 'application/json',
B2A = 'application/xml',
}
// 导出文件名;未知 MIME 类型补充后缀
enum ExportType {
A2B = 'jsontoxml',
B2A = 'xmltojson',
}
// 按钮文字
enum ButtonText {
A2B = 'JSON',
B2A = 'XML',
}
// 导入限制类型
enum InputAccept {
A2B = '.json',
B2A = '.xml',
}
// ace 编辑器 mode 类型
enum AceMode {
A2B = 'json',
B2A = 'xml',
}

const _C = () => {
const [a, setA] = useState('');
const [b, setB] = useState('');
const [convert, setConvert] = useState(ConvertType.A2B);
const [error, setError] = useState('');

// 处理 a2b
const a2b = async (v: string) => {
try {
const builder = new Builder();
const xml = builder.buildObject(JSON.parse(v));
setB(xml);
} catch (e) {
setError(String(e));
}
};

// 处理 b2a
const b2a = async (v: string) => {
try {
const json = await parseStringPromise(v, { explicitArray: false });
setA(JSON.stringify(json, null, 2));
} catch (e) {
setError(String(e) || '未知错误');
}
};

const saveStringToFile = () => {
const blob =
convert === ConvertType.A2B
? new Blob([b], { type: ConvertFileType.B2A })
: new Blob([a], { type: ConvertFileType.A2B });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download =
convert === ConvertType.A2B ? ExportType.A2B : ExportType.B2A;

document.body.appendChild(link);
link.click();

document.body.removeChild(link);
};

const handleClick = useCallback(() => {
alert.success('复制成功');
}, []);

const handleButtonClick = () => {
const fileInput = document.getElementById('fileInput');
fileInput?.click();
};

const handleFileChange = (event: any) => {
const file = event.target.files[0];
if (file) {
console.log(
'🐵 ~ file: json2xml.tsx:112 ~ handleFileChange ~ file:',
file
);
if (
file.type ===
(convert === ConvertType.A2B
? ConvertFileType.A2B
: ConvertFileType.B2A)
) {
const reader = new FileReader();
reader.onload = (e: any) => {
const content = e.target.result;
if (convert === ConvertType.A2B) {
setA(content);
} else setB(content);
setError('');
};

reader.readAsText(file);
} else {
setA('');
setError('Invalid file type.');
}
}
};

useEffect(() => {
if (a.trim() === '') {
setB('');
setError('');
return;
}
if (convert === ConvertType.A2B) {
try {
a2b(a);
setError('');
} catch (e) {
setError(String(e));
}
}
}, [a, convert]);

useEffect(() => {
if (!b) {
setA('');
setError('');
return;
}
if (convert === ConvertType.B2A) {
// 判断是否是数组或对象
try {
b2a(b);
setError('');
} catch (e) {
setError(String(e));
}
} else {
setError('');
}
}, [b, convert]);

return (
<MainContent>
<Box
sx={{
'#ace-editor *': {
fontFamily: 'Mono',
},
}}
>
<Stack
sx={{ mb: 2 }}
justifyContent={'center'}
direction={'row'}
spacing={2}
>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'space-between'}
sx={{ width: '100%' }}
>
<Button size='small' variant='outlined' onClick={handleButtonClick}>
上传{' '}
{convert === ConvertType.B2A ? ButtonText.B2A : ButtonText.A2B}
</Button>
<Box>
{convert === ConvertType.B2A ? ButtonText.B2A : ButtonText.A2B}
</Box>
</Stack>
<Box
sx={{
width: '24px',
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
mx: 3,
flexShrink: 0,
color: '#999',
}}
onClick={() =>
setConvert(
convert === ConvertType.B2A ? ConvertType.A2B : ConvertType.B2A
)
}
>
<SwapHorizontalCircleIcon />
</Box>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'space-between'}
sx={{ width: '100%' }}
>
<Box>
{convert === ConvertType.B2A ? ButtonText.A2B : ButtonText.B2A}
</Box>
<Stack direction={'row'} alignItems={'center'}>
<CopyToClipboard
text={convert === ConvertType.B2A ? a : b}
onCopy={handleClick}
>
<Button size='small'>复制</Button>
</CopyToClipboard>
<Button
size='small'
variant='contained'
disabled={!!error}
onClick={saveStringToFile}
>
导出{' '}
{convert === ConvertType.B2A ? ButtonText.A2B : ButtonText.B2A}
</Button>
</Stack>
</Stack>
</Stack>
<Stack direction={'row'} spacing={3}>
<AceEditor
name='a'
fontSize={16}
style={{
width: '100%',
borderRadius: '4px',
height: 'calc(100vh - 310px)',
}}
value={convert === ConvertType.B2A ? b : a}
mode={convert === ConvertType.B2A ? AceMode.B2A : AceMode.A2B}
theme='monokai'
onChange={convert === ConvertType.B2A ? setB : setA}
editorProps={{ $blockScrolling: true }}
/>
<AceEditor
name='b'
fontSize={16}
style={{
width: '100%',
borderRadius: '4px',
height: 'calc(100vh - 310px)',
}}
value={error || (convert === ConvertType.B2A ? a : b)}
mode={convert === ConvertType.B2A ? AceMode.A2B : AceMode.B2A}
theme='monokai'
readOnly
editorProps={{ $blockScrolling: true }}
/>
</Stack>
<Box
component={'input'}
id='fileInput'
type='file'
accept={
convert === ConvertType.B2A ? InputAccept.B2A : InputAccept.A2B
}
style={{ display: 'none' }}
onChange={handleFileChange}
/>
</Box>
</MainContent>
);
};

export default _C;
7 changes: 7 additions & 0 deletions src/utils/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,4 +381,11 @@ export const allTools: Tool[] = [
key: [],
subTitle: '在线颜色吸取器,可以快速生成十种常用颜色的代码',
},
{
label: 'JSON XML 互转',
tags: [Tags.JSON],
path: '/json2xml',
key: [],
subTitle: 'JSON 转 XML,XML 转 JSON',
},
];