From dd468817b49b8da2330e74c6596bf677fb8a3df9 Mon Sep 17 00:00:00 2001 From: Zhuym <zhuym7@gmail.com> Date: Tue, 28 Jan 2025 15:22:18 +0800 Subject: [PATCH] first update --- ._index.html | Bin 0 -> 4096 bytes ._src | Bin 0 -> 4096 bytes .gitignore | 2 + .vscode/launch.json | 16 ++ config.json | 103 --------- config/label.json | 161 +++++++++++++ index.html | 465 +++++++++++++++++++++++--------------- index1.html | 431 ----------------------------------- src/components/ApiTabs.js | 228 +++++++++++++++++++ src/components/App.js | 79 +++++++ src/constants/index.js | 5 + src/styles/main.css | 85 +++++++ src/utils/cookies.js | 21 ++ 13 files changed, 886 insertions(+), 710 deletions(-) create mode 100644 ._index.html create mode 100644 ._src create mode 100644 .gitignore create mode 100644 .vscode/launch.json delete mode 100644 config.json create mode 100644 config/label.json delete mode 100644 index1.html create mode 100644 src/components/ApiTabs.js create mode 100644 src/components/App.js create mode 100644 src/constants/index.js create mode 100644 src/styles/main.css create mode 100644 src/utils/cookies.js diff --git a/._index.html b/._index.html new file mode 100644 index 0000000000000000000000000000000000000000..2043f67f7c01f6e223fe46a3b18d91a044ff11ca GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$s_?Xk1?_j8A#X&vQ`hQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kgR_7DJdHbEE+<U%qsixrAei}Op1l2aAZ x@{6(+GV@AO^GY)F^AdA%Div~4(@GSQauV~hfqh}9t|3jK`XBBU83wuk{{a}DCvX4& literal 0 HcmV?d00001 diff --git a/._src b/._src new file mode 100644 index 0000000000000000000000000000000000000000..2043f67f7c01f6e223fe46a3b18d91a044ff11ca GIT binary patch literal 4096 zcmZQz6=P>$Vqox1Ojhs@R)|o50+1L3ClDJkFz{^v(m+1nBL)UWIUt(=a103vf+zv$ zV3+~K+-O=D5#plB`MG+D1qC^&dId%KWvO|IdC92^j7$s_?Xk1?_j8A#X&vQ`hQMeD zjE2By2#kinXb6mkz-S1JhQMeDjE2By2#kgR_7DJdHbEE+<U%qsixrAei}Op1l2aAZ x@{6(+GV@AO^GY)F^AdA%Div~4(@GSQauV~hfqh}9t|3jK`XBBU83wuk{{a}DCvX4& literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..026930b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# 忽略UserDataDir文件夹 +UserDataDir \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e8549e7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Chrome", + "type": "chrome", + "request": "launch", + "file": "${workspaceFolder}/index.html", + "runtimeArgs": [ + "--disable-web-security", + "--user-data-dir=${workspaceFolder}/UserDataDir" + ], + "webRoot": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/config.json b/config.json deleted file mode 100644 index bd7dfc0..0000000 --- a/config.json +++ /dev/null @@ -1,103 +0,0 @@ -{ - "title": "API调试工具", - "theme": "light", - "apis": [ - { - "name": "二维码扫描", - "type": "qr", - "endpoint": "/qr", - "method": "GET", - "params": [ - { - "key": "qrcode", - "label": "二维码内容", - "type": "qr", - "required": true - } - ], - "responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"] - }, - { - "name": "发送6倍券", - "type": "action", - "endpoint": "/ticket", - "method": "GET", - "params": [ - { - "key": "userid", - "label": "用户ID", - "type": "text", - "required": true, - "source": "userId" - } - ], - "responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"] - }, - { - "name": "保存99公里", - "type": "action", - "endpoint": "/mapstock", - "method": "GET", - "params": [ - { - "key": "userid", - "label": "用户ID", - "type": "text", - "required": true, - "source": "userId" - } - ], - "responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"] - }, - { - "name": "解锁DX曲目", - "type": "action", - "endpoint": "/unlock", - "method": "GET", - "params": [ - { - "key": "userid", - "label": "用户ID", - "type": "text", - "required": true, - "source": "userId" - } - ], - "responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"] - }, - { - "name": "更新音乐数据", - "type": "form", - "endpoint": "/music", - "method": "POST", - "params": [ - { - "key": "userId", - "label": "用户ID", - "type": "text", - "required": true, - "source": "userId" - }, - { - "key": "musicId", - "label": "音乐ID", - "type": "number", - "required": true - }, - { - "key": "level", - "label": "难度", - "type": "number", - "required": true - }, - { - "key": "playCount", - "label": "游玩次数", - "type": "number", - "required": true - } - ], - "responseFields": ["status", "timestamp", "info", "apiName", "date", "userId"] - } - ] - } \ No newline at end of file diff --git a/config/label.json b/config/label.json new file mode 100644 index 0000000..0bdcad9 --- /dev/null +++ b/config/label.json @@ -0,0 +1,161 @@ +{ + "forms": { + "login": { + "tab": { + "index": 0, + "label": "🪪登录", + "description": "上传登录二维码解码后的字符串来获取 userID" + }, + "endpoint": "/qr", + "method": "GET", + "fields": [ + { + "id": "qrcode", + "label": "二维码内容", + "type": "text", + "required": true + } + ], + "confirmBeforeSubmit": false + }, + "ticket": { + "tab": { + "index": 1, + "label": "🎟️发6倍票", + "description": "发送「6倍功能票」到账户" + }, + "endpoint": "/ticket", + "method": "GET", + "fields": [ + { + "id": "userid", + "label": "🪪UserID", + "type": "text", + "required": true + } + ], + "confirmBeforeSubmit": true + }, + "mapstock": { + "tab": { + "index": 2, + "label": "🗺️存入Stock", + "description": "保存 99km Stocks 到账户" + }, + "endpoint": "/mapstock", + "method": "GET", + "fields": [ + { + "id": "userid", + "label": "🪪UserID", + "type": "text", + "required": true + } + ], + "confirmBeforeSubmit": true + }, + "unlock": { + "tab": { + "index": 3, + "label": "🔓解锁紫铺", + "description": "解锁所有 DX Master 谱面" + }, + "endpoint": "/unlock", + "method": "GET", + "fields": [ + { + "id": "userid", + "label": "🪪UserID", + "type": "text", + "required": true + } + ], + "confirmBeforeSubmit": true + }, + "music": { + "tab": { + "index": 4, + "label": "✏️修改成绩", + "description": "⚠警告\n这将[覆写]账户中的成绩数据" + }, + "endpoint": "/music", + "method": "POST", + "fields": [ + { + "id": "userId", + "label": "🪪UserID", + "type": "number", + "required": true + }, + { + "id": "music.achievement", + "label": "💯分数", + "type": "number", + "placeholder": "1010000", + "required": true + }, + { + "id": "music.level", + "label": "🧩难度", + "type": "select", + "options": [ + {"value": 0, "label": "0 (🟩Bas)"}, + {"value": 1, "label": "1 (🟨Adv)"}, + {"value": 2, "label": "2 (🟥Exp)"}, + {"value": 3, "label": "3 (🟪Mas)"}, + {"value": 4, "label": "4 (⬜ReM)"} + ], + "required": true + }, + { + "id": "music.comboStatus", + "label": "Full Combo 状态", + "type": "select", + "options": [ + {"value": 0, "label": "0 (⚪未FC)"}, + {"value": 1, "label": "1 (🟢已FC)"} + ], + "required": true + }, + { + "id": "music.syncStatus", + "label": "Full Sync 状态", + "type": "select", + "options": [ + {"value": 0, "label": "0 (⚪未Sync)"}, + {"value": 1, "label": "1 (🔵已Sync)"} + ], + "required": true + }, + { + "id": "music.musicId", + "label": "🎵musicID", + "type": "number", + "placeholder": "11529", + "required": true + }, + { + "id": "music.playCount", + "label": "🎰游玩次数", + "type": "number", + "placeholder": "1", + "required": true + }, + { + "id": "music.deluxscoreMax", + "label": "🔢deluxe分数", + "type": "number", + "required": true + }, + { + "id": "music.scoreRank", + "label": "scoreRank", + "type": "number", + "required": true + } + ], + "confirmBeforeSubmit": true, + "confirmTemplate": "即将发送的数据:\nUserID: {userId}\n歌曲ID: {music.musicId}\nachievement: {music.achievement}\n难度: {music.level}\nFC状态: {music.comboStatus}\nsyncStatus: {music.syncStatus}\nFS状态:{music.syncStatus}\n\n原始请求:\n{rawData}" + } + } +} diff --git a/index.html b/index.html index 637e2eb..5ce5602 100644 --- a/index.html +++ b/index.html @@ -16,14 +16,81 @@ <script src="https://unpkg.com/@mui/material@5.14.0/umd/material-ui.production.min.js"></script> <style> - body { margin: 20px; font-family: Arial, sans-serif; } + body { + margin: 0; + font-family: Arial, sans-serif; + min-height: 100vh; + display: flex; + flex-direction: column; + } + #root { + flex: 1; + } pre { background: #f5f5f5; padding: 10px; border-radius: 4px; } - .api-description { margin-bottom: 16px; color: #666; } + .api-description { + margin: 8px 0 16px !important; + color: #666; + white-space: pre-line; + line-height: 1.5; + background-color: #f8f9fa; + padding: 12px; + border-radius: 4px; + border-left: 4px solid #1976d2; + } .selected-backend { font-weight: bold; color: #1976d2; } + .scrollable-tabs { + max-width: calc(100% - 48px); /* 为菜单按钮留出空间 */ + overflow-x: auto; + overflow-y: hidden; + } + .tabs-container { + display: flex; + align-items: center; + position: relative; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + } + .menu-button { + position: absolute !important; + right: 0; + top: 50%; + transform: translateY(-50%); + } + footer { + margin-top: auto; + padding: 16px; + background-color: #f5f5f5; + text-align: center; + font-size: 0.875rem; + color: #666; + } + @media (max-width: 600px) { + .MuiContainer-root { + padding: 12px !important; + } + .MuiPaper-root { + padding: 12px !important; + } + h4.MuiTypography-root { + font-size: 1.5rem; + } + .scrollable-tabs::-webkit-scrollbar { + display: none; + } + .api-description { + font-size: 0.875rem; + padding: 8px; + } + } </style> </head> -<body> +<body></body> <div id="root"></div> + <footer> + <Typography variant="body2" color="textSecondary"> + © 2023 Tsumugiboshi. All rights reserved. + <br /> + </Typography> + </footer> <script type="text/babel"> // 从全局对象获取Material UI组件 @@ -48,7 +115,10 @@ DialogContent, DialogActions, Snackbar, - Alert + Alert, + Menu, + IconButton, + Link } = MaterialUI; // 创建主题 @@ -61,6 +131,87 @@ { label: "生产环境", value: "http://api.example.com" } ]; + // 通用表单组件 + const DynamicForm = ({ formConfig, onSubmit }) => { + const [formData, setFormData] = React.useState({}); + + const handleChange = (fieldId, value) => { + const keys = fieldId.split('.'); + setFormData(prev => { + let newData = {...prev}; + let current = newData; + for (let i = 0; i < keys.length - 1; i++) { + current[keys[i]] = current[keys[i]] || {}; + current = current[keys[i]]; + } + current[keys[keys.length - 1]] = value; + return newData; + }); + }; + + const handleSubmit = async () => { + if (formConfig.confirmBeforeSubmit) { + let confirmMessage = formConfig.confirmTemplate || "确认提交数据?"; + if (formConfig.confirmTemplate) { + confirmMessage = confirmMessage.replace(/\{([^}]+)\}/g, (_, key) => { + return key === 'rawData' ? JSON.stringify(formData, null, 2) : + key.split('.').reduce((obj, k) => obj && obj[k], formData) || ''; + }); + } + if (!confirm(confirmMessage)) return; + } + onSubmit(formData); + }; + + return ( + <div style={{ padding: '1rem' }}> + <Typography variant="body1" className="api-description"> + {formConfig.tab.description} + </Typography> + <Grid container spacing={2}> + {formConfig.fields.map(field => ( + <Grid item xs={12} key={field.id}> + {field.type === 'select' ? ( + <FormControl fullWidth> + <InputLabel>{field.label}</InputLabel> + <Select + value={field.id.split('.').reduce((obj, key) => obj && obj[key], formData) ?? ''} + onChange={(e) => handleChange(field.id, e.target.value)} + label={field.label} + > + {field.options.map(opt => ( + <MenuItem key={opt.value} value={opt.value}> + {opt.label} + </MenuItem> + ))} + </Select> + </FormControl> + ) : ( + <TextField + fullWidth + label={field.label} + type={field.type} + value={field.id.split('.').reduce((obj, key) => obj && obj[key], formData) || ''} + onChange={(e) => handleChange(field.id, e.target.value)} + placeholder={field.placeholder} + /> + )} + </Grid> + ))} + <Grid item xs={12}> + <Button + variant="contained" + color="primary" + onClick={handleSubmit} + > + 提交 + </Button> + </Grid> + </Grid> + </div> + ); + }; + function App() { const [tabValue, setTabValue] = React.useState(0); const [qrInput, setQrInput] = React.useState(''); @@ -90,6 +241,7 @@ const [newBackend, setNewBackend] = React.useState({ label: '', value: '' }); const [snackbarOpen, setSnackbarOpen] = React.useState(false); const [snackbarMessage, setSnackbarMessage] = React.useState(''); + const [menuAnchorEl, setMenuAnchorEl] = React.useState(null); // Cookie操作函数 function setCookie(name, value, days) { @@ -169,9 +321,52 @@ setSnackbarOpen(false); }; + const handleDeleteBackend = (backendValue) => { + const updatedBackends = backends.filter(b => b.value !== backendValue); + setBackends(updatedBackends); + + // 如果删除的是当前选中的后端,切换到第一个默认后端 + if (apiBase === backendValue) { + setApiBase(PRESET_BACKENDS[0].value); + localStorage.setItem('apiBase', PRESET_BACKENDS[0].value); + } + + // 只保存自定义后端 + localStorage.setItem('customBackends', + JSON.stringify(updatedBackends.filter(b => + !PRESET_BACKENDS.some(preset => preset.value === b.value) + )) + ); + + setSnackbarMessage('后端已删除'); + setSnackbarOpen(true); + }; + + const handleMenuClick = (event) => { + setMenuAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setMenuAnchorEl(null); + }; + + const handleMenuSelect = (index) => { + setTabValue(index); + handleMenuClose(); + }; + + // 加载表单配置 + const [formConfigs, setFormConfigs] = React.useState(null); + + React.useEffect(() => { + fetch('config/label.json') + .then(res => res.json()) + .then(config => setFormConfigs(config.forms)); + }, []); + return ( <ThemeProvider theme={theme}> - <Container maxWidth="md" style={{ marginTop: '2rem', marginBottom: '2rem' }}> + <Container maxWidth="md" style={{ marginTop: '1rem', marginBottom: '2rem' }}> {/* 后端地址选择器 */} <FormControl fullWidth style={{ marginBottom: '2rem' }}> <InputLabel>选择后端地址</InputLabel> @@ -180,11 +375,40 @@ onChange={handleApiBaseChange} label="选择后端地址" > - {backends.map((backend) => ( + {/* 默认后端 */} + {PRESET_BACKENDS.map((backend) => ( <MenuItem key={backend.value} value={backend.value}> - {backend.label} - {backend.value} {apiBase === backend.value} + {backend.label} - {backend.value} </MenuItem> ))} + + {/* 分割线 */} + {backends.some(b => !PRESET_BACKENDS.some(preset => preset.value === b.value)) && ( + <MenuItem disabled> + <hr style={{ width: '100%', margin: '4px 0' }} /> + </MenuItem> + )} + + {/* 自定义后端 */} + {backends + .filter(b => !PRESET_BACKENDS.some(preset => preset.value === b.value)) + .map((backend) => ( + <MenuItem key={backend.value} value={backend.value}> + <Box display="flex" justifyContent="space-between" width="100%"> + <span>{backend.label} - {backend.value}</span> + <Button + size="small" + color="error" + onClick={(e) => { + e.stopPropagation(); + handleDeleteBackend(backend.value); + }} + > + 删除 + </Button> + </Box> + </MenuItem> + ))} </Select> <Button variant="outlined" @@ -235,178 +459,67 @@ </Typography> <Paper elevation={3} style={{ padding: '1rem', marginBottom: '1rem' }}> - <Tabs - value={tabValue} - onChange={(e, v) => setTabValue(v)} - indicatorColor="primary" - textColor="primary" - > - <Tab label="🪪登录" /> - <Tab label="🎟️发6倍票" /> - <Tab label="🗺️存入Stock" /> - <Tab label="🔓解锁紫铺" /> - <Tab label="✏️修改成绩" /> - </Tabs> - - {/* 二维码转UID表单 */} - {tabValue === 0 && ( - <div style={{ padding: '1rem' }}> - <Typography variant="body1" className="api-description"> - 上传登录二维码解码后的字符串来获取 userID。 - </Typography> - <TextField - fullWidth - label="二维码内容" - value={qrInput} - onChange={(e) => setQrInput(e.target.value)} - style={{ marginBottom: '1rem' }} - /> - <Button - variant="contained" - color="primary" - onClick={() => handleApiCall(`/qr?qrcode=${encodeURIComponent(qrInput)}`)} + <div className="tabs-container"> + <Box className="scrollable-tabs"> + <Tabs + value={tabValue} + onChange={(e, v) => setTabValue(v)} + indicatorColor="primary" + textColor="primary" + variant="scrollable" + scrollButtons="auto" + allowScrollButtonsMobile > - 获取UserID - </Button> - </div> - )} - - {/* 通用GET请求表单 */} - {[1, 2, 3].includes(tabValue) && ( - <div style={{ padding: '1rem' }}> - <Typography variant="body1" className="api-description"> - {tabValue === 1 && "发送「6倍功能票」到账户。"} - {tabValue === 2 && "保存 99 公里 Stock 到账户。"} - {tabValue === 3 && "解锁所有 DX Master 谱面。"} - </Typography> - <TextField - label="UserID" - value={userId} - onChange={(e) => setUserId(e.target.value)} - style={{ marginBottom: '1rem' }} - fullWidth - /> - <Button - variant="contained" - color="primary" - onClick={() => { - const endpoints = ['/ticket', '/mapstock', '/unlock']; - handleApiCall(`${endpoints[tabValue-1]}?userid=${userId}`); - }} - > - 发送请求 - </Button> - </div> - )} - - {/* 音乐数据POST表单 */} - {tabValue === 4 && ( - <div style={{ padding: '1rem' }}> - <Typography variant="body1" className="api-description"> - *覆写* 账户中的成绩数据 - </Typography> - <Grid container spacing={2}> - <Grid item xs={12}> - <TextField - fullWidth - label="🪪UserID" - type="number" - value={musicData.userId} - onChange={(e) => setMusicData({...musicData, userId: e.target.value})} - /> - </Grid> - <Grid item xs={12}> - <TextField - fullWidth - label="💯分数" - type="number" - value={musicData.music.achievement} - onChange={(e) => setMusicData({...musicData, music: {...musicData.music, achievement: e.target.value}})} - placeholder="1010000" - InputProps={{ - style: { color: 'grey' } - }} - /> - </Grid> - <Grid item xs={12}> - <FormControl fullWidth> - <InputLabel id="level-select-label">🧩难度</InputLabel> - <Select - labelId="level-select-label" - id="level-select" - value={musicData.music.level} - onChange={(e) => setMusicData({...musicData, music: {...musicData.music, level: e.target.value}})} - > - <MenuItem value={0}>0 (🟩Bas)</MenuItem> - <MenuItem value={1}>1 (🟨Adv)</MenuItem> - <MenuItem value={2}>2 (🟥Exp)</MenuItem> - <MenuItem value={3}>3 (🟪Mas)</MenuItem> - <MenuItem value={4}>4 (⬜ReM)</MenuItem> - </Select> - </FormControl> - </Grid> - <Grid item xs={12}> - <FormControl fullWidth> - <InputLabel id="comboStatus-select-label">comboStatus</InputLabel> - <Select - labelId="comboStatus-select-label" - id="comboStatus-select" - value={musicData.music.comboStatus} - onChange={(e) => setMusicData({...musicData, music: {...musicData.music, comboStatus: e.target.value}})} - > - <MenuItem value={0}>0 (⚪未达成)</MenuItem> - <MenuItem value={1}>1 (🟢已达成)</MenuItem> - </Select> - </FormControl> - </Grid> - <Grid item xs={12}> - <FormControl fullWidth> - <InputLabel id="syncStatus-select-label">syncStatus</InputLabel> - <Select - labelId="syncStatus-select-label" - id="syncStatus-select" - value={musicData.music.syncStatus} - onChange={(e) => setMusicData({...musicData, music: {...musicData.music, syncStatus: e.target.value}})} - > - <MenuItem value={0}>0 (未同步)</MenuItem> - <MenuItem value={1}>1 (🔵已同步)</MenuItem> - </Select> - </FormControl> - </Grid> - {Object.entries(musicData.music).map(([key, val]) => ( - key !== 'level' && key !== 'achievement' && key !== 'comboStatus' && key !== 'syncStatus' && ( - <Grid item xs={6} sm={4} key={key}> - <TextField - fullWidth - label={key} - type="number" - name={key} - value={val} - onChange={handleMusicChange} + {formConfigs && Object.values(formConfigs) + .sort((a, b) => a.tab.index - b.tab.index) + .map(config => ( + <Tab + key={config.tab.index} + label={config.tab.label} + style={{ minWidth: 'max-content' }} /> - </Grid> - ) - ))} - <Grid item xs={12}> - <Button - variant="contained" - color="primary" - onClick={async () => { - const dataToSend = JSON.stringify(musicData, null, 2); - const confirmationMessage = `即将发送的数据:\nUserID: ${musicData.userId}\n歌曲ID: ${musicData.music.musicId}\nachievement: ${musicData.music.achievement}\nlevel: ${musicData.music.level}\ncomboStatus: ${musicData.music.comboStatus}\nsyncStatus: ${musicData.music.syncStatus}\n\n原始请求:\n${dataToSend}`; - alert(confirmationMessage); // 显示即将发送的数据 - const confirmation = window.confirm("确认提交数据?"); // 弹出确认对话框 - if (confirmation) { - handleApiCall('/music', 'POST', musicData); // 如果用户确认,发送请求 - } - }} - > - 提交音乐数据 - </Button> - </Grid> - </Grid> + ))} + </Tabs> + </Box> + <IconButton + className="menu-button" + onClick={handleMenuClick} + size="small" + > + <span className="material-icons">menu</span> + </IconButton> + <Menu + anchorEl={menuAnchorEl} + open={Boolean(menuAnchorEl)} + onClose={handleMenuClose} + > + {formConfigs && Object.values(formConfigs) + .sort((a, b) => a.tab.index - b.tab.index) + .map(config => ( + <MenuItem + key={config.tab.index} + onClick={() => handleMenuSelect(config.tab.index)} + selected={tabValue === config.tab.index} + > + {config.tab.label} + </MenuItem> + ))} + </Menu> </div> - )} + + {formConfigs && Object.entries(formConfigs).map(([key, config]) => ( + tabValue === config.tab.index && ( + <DynamicForm + key={key} + formConfig={config} + onSubmit={(data) => handleApiCall( + config.endpoint, + config.method, + config.method === 'POST' ? data : null + )} + /> + ) + ))} </Paper> {response && ( diff --git a/index1.html b/index1.html deleted file mode 100644 index 4e6dde1..0000000 --- a/index1.html +++ /dev/null @@ -1,431 +0,0 @@ -<!DOCTYPE html> -<html> -<head> - <title>API调试工具</title> - <!-- Material Icons --> - <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons"> - - <!-- React 依赖 --> - <script src="https://unpkg.com/react@17/umd/react.production.min.js"></script> - <script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script> - - <!-- Babel --> - <script src="https://unpkg.com/@babel/standalone@7.21.5/babel.min.js"></script> - - <!-- Material UI 组件库 --> - <script src="https://unpkg.com/@mui/material@5.14.0/umd/material-ui.production.min.js"></script> - - <style> - body { margin: 20px; font-family: Arial, sans-serif; } - pre { background: #f5f5f5; padding: 10px; border-radius: 4px; } - .api-description { margin-bottom: 16px; color: #666; } - .selected-backend { font-weight: bold; color: #1976d2; } - </style> -</head> -<body> - <div id="root"></div> - - <script type="text/babel"> - // 从全局对象获取Material UI组件 - const { - Container, - Tabs, - Tab, - TextField, - Button, - Paper, - Typography, - Grid, - Box, - createTheme, - ThemeProvider, - Select, - MenuItem, - InputLabel, - FormControl, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Snackbar, - Alert - } = MaterialUI; - - // 创建主题 - const theme = createTheme(); - - // 预设的后端地址 - const PRESET_BACKENDS = [ - { label: "开发环境", value: "http://dev-api.example.com" }, - { label: "测试环境", value: "http://test-api.example.com" }, - { label: "生产环境", value: "http://api.example.com" } - ]; - - function App() { - const [tabValue, setTabValue] = React.useState(0); - const [qrInput, setQrInput] = React.useState(''); - const [userId, setUserId] = React.useState(getCookie('userId') || ''); - const [response, setResponse] = React.useState(''); - const [musicData, setMusicData] = React.useState({ - userId: parseInt(userId) || 0, - music: { - musicId: 0, - level: 0, - playCount: 0, - comboStatus: 0, - syncStatus: 0, - deluxscoreMax: 0, - scoreRank: 0 - } - }); - const [apiBase, setApiBase] = React.useState( - localStorage.getItem('apiBase') || PRESET_BACKENDS[0].value - ); - const [backends, setBackends] = React.useState([ - ...PRESET_BACKENDS, - ...JSON.parse(localStorage.getItem('customBackends') || '[]') - ]); - const [isDialogOpen, setIsDialogOpen] = React.useState(false); - const [newBackend, setNewBackend] = React.useState({ label: '', value: '' }); - const [snackbarOpen, setSnackbarOpen] = React.useState(false); - const [snackbarMessage, setSnackbarMessage] = React.useState(''); - - // Cookie操作函数 - function setCookie(name, value, days) { - const d = new Date(); - d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); - document.cookie = `${name}=${value};expires=${d.toUTCString()};path=/`; - } - - function getCookie(name) { - return document.cookie.split('; ') - .find(row => row.startsWith(name + '=')) - ?.split('=')[1]; - } - - React.useEffect(() => { - if (userId) setCookie('userId', userId, 7); - }, [userId]); - - const handleApiCall = async (endpoint, method = 'GET', body = null) => { - try { - const options = { - method, - headers: { 'Content-Type': 'application/json' } - }; - if (body) options.body = JSON.stringify(body); - - const res = await fetch(`${apiBase}${endpoint}`, options); - const data = await res.json(); - setResponse(JSON.stringify(data, null, 2)); - - //记录日志 - const log = { - timestamp: new Date().toLocaleString(), - apiName: data.apiName || endpoint, - status: data.status || 'Unknown', - response: data, - expanded: false - }; - setLogs(prevLogs => [log, ...prevLogs]); - - //处理userID - if (data.userId) { - setUserId(data.userId); - localStorage.setItem('userId', data.userId); - } - - //显示成功toast - if (data.status === "200 OK") { - const date = new Date(data.timestamp * 1000).toLocaleString(); - setSnackbarMessage(`成功: ${data.apiName} (${date})`); - setSnackbarOpen(true); - } - - } catch (error) { - setResponse(`Error: ${error.message}`); - } - }; - - const handleMusicChange = (e) => { - const { name, value } = e.target; - setMusicData(prev => ({ - ...prev, - music: { - ...prev.music, - [name]: parseInt(value) || 0 - } - })); - }; - - const handleApiBaseChange = (event) => { - const value = event.target.value; - setApiBase(value); - localStorage.setItem('apiBase', value); - }; - - const handleAddBackend = () => { - setIsDialogOpen(true); - }; - - const handleDialogClose = () => { - setIsDialogOpen(false); - setNewBackend({ label: '', value: '' }); - }; - - const handleSaveBackend = () => { - if (!newBackend.label || !newBackend.value) { - setSnackbarMessage('后端名称和地址不能为空'); - setSnackbarOpen(true); - return; - } - - const updatedBackends = [...backends, newBackend]; - setBackends(updatedBackends); - localStorage.setItem('customBackends', JSON.stringify(updatedBackends.filter(b => !PRESET_BACKENDS.includes(b)))); - setIsDialogOpen(false); - setSnackbarMessage('自定义后端已保存'); - setSnackbarOpen(true); - }; - - const handleSnackbarClose = () => { - setSnackbarOpen(false); - }; - - // - const toggleLogExpanded = (index) => { - setLogs(prevLogs => { - const newLogs = [...prevLogs]; - newLogs[index].expanded = !newLogs[index].expanded; - return newLogs; - }); - }; - 0 - return ( - <ThemeProvider theme={theme}> - <Container maxWidth="md" style={{ marginTop: '2rem', marginBottom: '2rem' }}> - {/* 后端地址选择器 */} - <FormControl fullWidth style={{ marginBottom: '2rem' }}> - <InputLabel>选择后端地址</InputLabel> - <Select - value={apiBase} - onChange={handleApiBaseChange} - label="选择后端地址" - > - {backends.map((backend) => ( - <MenuItem key={backend.value} value={backend.value}> - {backend.label} {apiBase === backend.value && '◉'} - </MenuItem> - ))} - </Select> - <Button - variant="outlined" - color="primary" - style={{ marginTop: '1rem' }} - onClick={handleAddBackend} - > - 添加自定义后端 - </Button> - </FormControl> - - {/* 添加自定义后端对话框 */} - <Dialog open={isDialogOpen} onClose={handleDialogClose}> - <DialogTitle>添加自定义后端</DialogTitle> - <DialogContent> - <TextField - fullWidth - label="后端名称" - value={newBackend.label} - onChange={(e) => setNewBackend({ ...newBackend, label: e.target.value })} - style={{ marginBottom: '1rem' }} - /> - <TextField - fullWidth - label="后端地址" - value={newBackend.value} - onChange={(e) => setNewBackend({ ...newBackend, value: e.target.value })} - /> - </DialogContent> - <DialogActions> - <Button onClick={handleDialogClose}>取消</Button> - <Button onClick={handleSaveBackend} color="primary">保存</Button> - </DialogActions> - </Dialog> - - {/* Snackbar 提示 */} - <Snackbar - open={snackbarOpen} - autoHideDuration={3000} - onClose={handleSnackbarClose} - > - <Alert onClose={handleSnackbarClose} severity="success"> - {snackbarMessage} - </Alert> - </Snackbar> - - <Typography variant="h4" gutterBottom> - API调试工具 - </Typography> - - <Paper elevation={3} style={{ padding: '1rem', marginBottom: '1rem' }}> - <Tabs - value={tabValue} - onChange={(e, v) => setTabValue(v)} - indicatorColor="primary" - textColor="primary" - > - <Tab label="二维码转UID" /> - <Tab label="6倍票券" /> - <Tab label="地图库存" /> - <Tab label="解锁DX" /> - <Tab label="音乐数据" /> - </Tabs> - - {/* 二维码转UID表单 */} - {tabValue === 0 && ( - <div style={{ padding: '1rem' }}> - <Typography variant="body1" className="api-description"> - 通过上传用户登录二维码解码后的字符串来获取 userID。 - </Typography> - <TextField - fullWidth - label="二维码内容" - value={qrInput} - onChange={(e) => setQrInput(e.target.value)} - style={{ marginBottom: '1rem' }} - /> - <Button - variant="contained" - color="primary" - onClick={() => handleApiCall(`/qr?qrcode=${encodeURIComponent(qrInput)}`)} - > - 获取UserID - </Button> - </div> - )} - - {/* 通用GET请求表单 */} - {[1, 2, 3].includes(tabValue) && ( - <div style={{ padding: '1rem' }}> - <Typography variant="body1" className="api-description"> - {tabValue === 1 && "使用用户的 userID 发送「6倍チケット」到账户。"} - {tabValue === 2 && "使用用户的 userID 保存 99 公里地图库存到账户。"} - {tabValue === 3 && "使用用户的 userID 解锁所有 DX Master 谱面。"} - </Typography> - <TextField - label="UserID" - value={userId} - onChange={(e) => setUserId(e.target.value)} - style={{ marginBottom: '1rem' }} - fullWidth - /> - <Button - variant="contained" - color="primary" - onClick={() => { - const endpoints = ['/ticket', '/mapstock', '/unlock']; - handleApiCall(`${endpoints[tabValue-1]}?userid=${userId}`); - }} - > - 发送请求 - </Button> - </div> - )} - - {/* 音乐数据POST表单 */} - {tabValue === 4 && ( - <div style={{ padding: '1rem' }}> - <Typography variant="body1" className="api-description"> - 覆写账户中的音乐数据 - </Typography> - <Grid container spacing={2}> - <Grid item xs={12}> - <TextField - fullWidth - label="UserID" - type="number" - value={musicData.userId} - onChange={(e) => setMusicData({...musicData, userId: e.target.value})} - /> - </Grid> - <Grid item xs={12}> - <TextField - fullWidth - label="achievement" - type="number" - value={musicData.music.achievement} - onChange={(e) => setMusicData({...musicData, music: {...musicData.music, achievement: e.target.value}})} - placeholder="1010000" - InputProps={{ - style: { color: 'grey' } - }} - /> - </Grid> - <Grid item xs={12}> - <FormControl fullWidth> - <InputLabel id="level-select-label">level</InputLabel> - <Select - labelId="level-select-label" - id="level-select" - value={musicData.music.level} - onChange={(e) => setMusicData({...musicData, music: {...musicData.music, level: e.target.value}})} - > - <MenuItem value={0}>0 (BAS)</MenuItem> - <MenuItem value={1}>1 (ADV)</MenuItem> - <MenuItem value={2}>2 (EXP)</MenuItem> - <MenuItem value={3}>3 (MAS)</MenuItem> - <MenuItem value={4}>4 (ReM)</MenuItem> - </Select> - </FormControl> - </Grid> - {Object.entries(musicData.music).map(([key, val]) => ( - key !== 'level' && key !== 'achievement' && ( - <Grid item xs={6} sm={4} key={key}> - <TextField - fullWidth - label={key} - type="number" - name={key} - value={val} - onChange={handleMusicChange} - /> - </Grid> - ) - ))} - <Grid item xs={12}> - <Button - variant="contained" - color="primary" - onClick={() => handleApiCall('/music', 'POST', musicData)} - > - 提交音乐数据 - </Button> - </Grid> - </Grid> - </div> - )} - </Paper> - - {response && ( - <Paper elevation={3} style={{ padding: '1rem' }}> - <Typography variant="h6" gutterBottom>响应结果:</Typography> - <pre style={{ - whiteSpace: 'pre-wrap', - wordWrap: 'break-word', - backgroundColor: '#f5f5f5', - padding: '1rem', - borderRadius: '4px' - }}> - {response} - </pre> - </Paper> - )} - </Container> - </ThemeProvider> - ); - } - - ReactDOM.render(<App />, document.getElementById('root')); - </script> -</body> -</html> \ No newline at end of file diff --git a/src/components/ApiTabs.js b/src/components/ApiTabs.js new file mode 100644 index 0000000..061c580 --- /dev/null +++ b/src/components/ApiTabs.js @@ -0,0 +1,228 @@ +import React from 'react'; +import { + Tabs, + Tab, + TextField, + Button, + Paper, + Typography, + Grid, + Box, + Select, + MenuItem, + InputLabel, + FormControl, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Snackbar, + Alert, + Radio, + RadioGroup, + FormControlLabel +} from '@mui/material'; +import { setCookie } from '../utils/cookies'; + +function ApiTabs({ + tabValue, + setTabValue, + qrInput, + setQrInput, + userId, + setUserId, + response, + setResponse, + musicData, + setMusicData, + apiBase, + setApiBase, + backends, + setBackends, + isDialogOpen, + setIsDialogOpen, + newBackend, + setNewBackend, + snackbarOpen, + setSnackbarOpen, + snackbarMessage, + setSnackbarMessage +}) { + const handleTabChange = (event, newValue) => { + setTabValue(newValue); + }; + + const handleUserIdChange = (event) => { + const value = event.target.value; + setUserId(value); + setCookie('userId', value); + setMusicData(prev => ({ ...prev, userId: parseInt(value) || 0 })); + }; + + const handleBackendChange = (event) => { + const value = event.target.value; + setApiBase(value); + localStorage.setItem('apiBase', value); + }; + + const handleAddBackend = () => { + if (!newBackend.label || !newBackend.value) { + setSnackbarMessage('请填写完整的后端信息'); + setSnackbarOpen(true); + return; + } + + const updatedBackends = [...backends, newBackend]; + setBackends(updatedBackends); + localStorage.setItem('customBackends', JSON.stringify( + updatedBackends.slice(PRESET_BACKENDS.length) + )); + setIsDialogOpen(false); + setNewBackend({ label: '', value: '' }); + }; + + return ( + <> + <Paper elevation={3} sx={{ p: 3, mb: 4 }}> + <Typography variant="h4" gutterBottom> + API调试工具 + </Typography> + <div className="tabs-container"> + <div className="scrollable-tabs"> + <Tabs + value={tabValue} + onChange={handleTabChange} + variant="scrollable" + scrollButtons="auto" + > + <Tab label="二维码解析" /> + <Tab label="音乐数据" /> + </Tabs> + </div> + </div> + + <Box sx={{ mt: 3 }}> + <Grid container spacing={3}> + <Grid item xs={12} md={6}> + <TextField + fullWidth + label="用户ID" + value={userId} + onChange={handleUserIdChange} + variant="outlined" + /> + </Grid> + <Grid item xs={12} md={6}> + <FormControl fullWidth> + <InputLabel>后端环境</InputLabel> + <Select + value={apiBase} + onChange={handleBackendChange} + label="后端环境" + > + {backends.map((backend, index) => ( + <MenuItem key={index} value={backend.value}> + {backend.label} + </MenuItem> + ))} + </Select> + </FormControl> + </Grid> + </Grid> + + {tabValue === 0 && ( + <Box sx={{ mt: 3 }}> + <TextField + fullWidth + multiline + rows={4} + label="二维码内容" + value={qrInput} + onChange={(e) => setQrInput(e.target.value)} + variant="outlined" + /> + </Box> + )} + + {tabValue === 1 && ( + <Box sx={{ mt: 3 }}> + <Grid container spacing={2}> + {Object.entries(musicData.music).map(([key, value]) => ( + <Grid item xs={12} sm={6} md={4} key={key}> + <TextField + fullWidth + label={key} + type="number" + value={value} + onChange={(e) => { + const newValue = parseInt(e.target.value) || 0; + setMusicData(prev => ({ + ...prev, + music: { + ...prev.music, + [key]: newValue + } + })); + }} + variant="outlined" + /> + </Grid> + ))} + </Grid> + </Box> + )} + + {response && ( + <Box sx={{ mt: 3 }}> + <Typography variant="h6" gutterBottom> + 响应结果 + </Typography> + <pre>{JSON.stringify(response, null, 2)}</pre> + </Box> + )} + </Box> + </Paper> + + <Dialog open={isDialogOpen} onClose={() => setIsDialogOpen(false)}> + <DialogTitle>添加自定义后端</DialogTitle> + <DialogContent> + <TextField + autoFocus + margin="dense" + label="名称" + fullWidth + value={newBackend.label} + onChange={(e) => setNewBackend(prev => ({ ...prev, label: e.target.value }))} + /> + <TextField + margin="dense" + label="地址" + fullWidth + value={newBackend.value} + onChange={(e) => setNewBackend(prev => ({ ...prev, value: e.target.value }))} + /> + </DialogContent> + <DialogActions> + <Button onClick={() => setIsDialogOpen(false)}>取消</Button> + <Button onClick={handleAddBackend}>添加</Button> + </DialogActions> + </Dialog> + + <Snackbar + open={snackbarOpen} + autoHideDuration={6000} + onClose={() => setSnackbarOpen(false)} + > + <Alert + onClose={() => setSnackbarOpen(false)} + severity="error" + sx={{ width: '100%' }} + > + {snackbarMessage} + </Alert> + </Snackbar> + </> + ); +} + +export default ApiTabs; \ No newline at end of file diff --git a/src/components/App.js b/src/components/App.js new file mode 100644 index 0000000..2d8c45a --- /dev/null +++ b/src/components/App.js @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import { Container, Typography } from '@mui/material'; +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import ApiTabs from './ApiTabs'; +import { PRESET_BACKENDS } from '../constants'; +import { getCookie, setCookie } from '../utils/cookies'; + +const theme = createTheme(); + +function App() { + const [tabValue, setTabValue] = useState(0); + const [qrInput, setQrInput] = useState(''); + const [userId, setUserId] = useState(getCookie('userId') || ''); + const [response, setResponse] = useState(''); + const [musicData, setMusicData] = useState({ + userId: parseInt(userId) || 0, + music: { + musicId: 0, + level: 0, + achievement: 0, + playCount: 0, + comboStatus: 0, + syncStatus: 0, + deluxscoreMax: 0, + scoreRank: 0 + } + }); + const [apiBase, setApiBase] = useState( + localStorage.getItem('apiBase') || PRESET_BACKENDS[0].value + ); + const [backends, setBackends] = useState([ + ...PRESET_BACKENDS, + ...JSON.parse(localStorage.getItem('customBackends') || '[]') + ]); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [newBackend, setNewBackend] = useState({ label: '', value: '' }); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(''); + + return ( + <ThemeProvider theme={theme}> + <div id="root"> + <Container maxWidth="md" sx={{ mt: 4, mb: 4 }}> + <ApiTabs + tabValue={tabValue} + setTabValue={setTabValue} + qrInput={qrInput} + setQrInput={setQrInput} + userId={userId} + setUserId={setUserId} + response={response} + setResponse={setResponse} + musicData={musicData} + setMusicData={setMusicData} + apiBase={apiBase} + setApiBase={setApiBase} + backends={backends} + setBackends={setBackends} + isDialogOpen={isDialogOpen} + setIsDialogOpen={setIsDialogOpen} + newBackend={newBackend} + setNewBackend={setNewBackend} + snackbarOpen={snackbarOpen} + setSnackbarOpen={setSnackbarOpen} + snackbarMessage={snackbarMessage} + setSnackbarMessage={setSnackbarMessage} + /> + </Container> + </div> + <footer> + <Typography variant="body2" color="textSecondary"> + © 2023 Tsumugiboshi. All rights reserved. + </Typography> + </footer> + </ThemeProvider> + ); +} + +export default App; \ No newline at end of file diff --git a/src/constants/index.js b/src/constants/index.js new file mode 100644 index 0000000..c76a669 --- /dev/null +++ b/src/constants/index.js @@ -0,0 +1,5 @@ +export const PRESET_BACKENDS = [ + { label: "开发环境", value: "http://dev-api.example.com" }, + { label: "测试环境", value: "http://test-api.example.com" }, + { label: "生产环境", value: "http://api.example.com" } +]; \ No newline at end of file diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..16f5522 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,85 @@ +body { + margin: 0; + font-family: Arial, sans-serif; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +#root { + flex: 1; +} + +pre { + background: #f5f5f5; + padding: 10px; + border-radius: 4px; +} + +.api-description { + margin: 8px 0 16px !important; + color: #666; + white-space: pre-line; + line-height: 1.5; + background-color: #f8f9fa; + padding: 12px; + border-radius: 4px; + border-left: 4px solid #1976d2; +} + +.selected-backend { + font-weight: bold; + color: #1976d2; +} + +.scrollable-tabs { + max-width: calc(100% - 48px); + overflow-x: auto; + overflow-y: hidden; +} + +.tabs-container { + display: flex; + align-items: center; + position: relative; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); +} + +.menu-button { + position: absolute !important; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +footer { + margin-top: auto; + padding: 16px; + background-color: #f5f5f5; + text-align: center; + font-size: 0.875rem; + color: #666; +} + +@media (max-width: 600px) { + .MuiContainer-root { + padding: 12px !important; + } + + .MuiPaper-root { + padding: 12px !important; + } + + h4.MuiTypography-root { + font-size: 1.5rem; + } + + .scrollable-tabs::-webkit-scrollbar { + display: none; + } + + .api-description { + font-size: 0.875rem; + padding: 8px; + } +} \ No newline at end of file diff --git a/src/utils/cookies.js b/src/utils/cookies.js new file mode 100644 index 0000000..80b1a48 --- /dev/null +++ b/src/utils/cookies.js @@ -0,0 +1,21 @@ +export function setCookie(name, value, days = 30) { + const d = new Date(); + d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); + const expires = `expires=${d.toUTCString()}`; + document.cookie = `${name}=${value};${expires};path=/`; +} + +export function getCookie(name) { + const nameEQ = `${name}=`; + const ca = document.cookie.split(';'); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length); + } + return null; +} + +export function deleteCookie(name) { + document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:01 GMT;path=/`; +} \ No newline at end of file