first update

This commit is contained in:
Zhuym
2025-01-28 15:22:18 +08:00
parent 0d8a82fc33
commit dd468817b4
13 changed files with 886 additions and 710 deletions

View File

@@ -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 && (