Feat:增加后端调试工具

This commit is contained in:
LeonspaceX
2025-12-07 19:11:55 +08:00
parent 462650478b
commit e86a5dd4fa
3 changed files with 249 additions and 5 deletions

View File

@@ -1,7 +1,7 @@
// 你好感谢你愿意看源代码但是悄悄告诉你代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { FluentProvider, webLightTheme, webDarkTheme, tokens } from '@fluentui/react-components';
import { FluentProvider, webLightTheme, webDarkTheme, tokens, Button } from '@fluentui/react-components';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import PostCard from './components/PostCard';
import MainLayout from './layouts/MainLayout';
@@ -20,6 +20,8 @@ import NotFound from './pages/NotFound';
import ImageViewer from './components/ImageViewer';
import NoticeModal from './components/NoticeModal';
import type { NoticeData } from './components/NoticeModal';
import DevToolsModal from './components/DevToolsModal';
import { Bug24Regular } from '@fluentui/react-icons';
function App() {
const [isDarkMode, setIsDarkMode] = React.useState(() => {
@@ -54,6 +56,13 @@ function App() {
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
const [noticeData, setNoticeData] = useState<NoticeData | null>(null);
const [showNotice, setShowNotice] = useState(false);
const [showDevTools, setShowDevTools] = useState(false);
const [isDebugMode, setIsDebugMode] = useState(false);
useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
setIsDebugMode(searchParams.get('debug') === 'true');
}, []);
useEffect(() => {
getNotice().then(data => {
@@ -258,6 +267,20 @@ function App() {
/>
</div>
)}
{/* DevTools Trigger */}
{isDebugMode && (
<div style={{ position: 'fixed', bottom: '20px', left: '20px', zIndex: 9999 }}>
<Button
icon={<Bug24Regular />}
appearance="primary"
shape="circular"
onClick={() => setShowDevTools(true)}
aria-label="Developer Tools"
/>
</div>
)}
{showDevTools && <DevToolsModal onClose={() => setShowDevTools(false)} />}
</FluentProvider>
);
}
@@ -270,3 +293,5 @@ export default App;

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useNavigate, useLocation } from 'react-router-dom';
import {
makeStyles,
Button,
@@ -173,6 +173,7 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
onToggleTheme
}) => {
const navigate = useNavigate();
const location = useLocation();
const styles = useStyles();
const [activeTab, setActiveTab] = React.useState<TabValue>('systemSettings');
const [postReviewSubTab, setPostReviewSubTab] = React.useState<TabValue>('pending');
@@ -962,10 +963,23 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
<Button appearance="secondary" onClick={() => setClearCacheConfirmOpen(true)}>
</Button>
<Button
appearance={new URLSearchParams(location.search).get('debug') === 'true' ? "primary" : "secondary"}
style={{ marginLeft: tokens.spacingHorizontalS }}
onClick={() => {
const params = new URLSearchParams(location.search);
const isDebug = params.get('debug') === 'true';
if (isDebug) {
params.set('debug', 'false');
} else {
params.set('debug', 'true');
}
window.location.search = params.toString();
}}
>
{new URLSearchParams(location.search).get('debug') === 'true' ? '关闭调试工具' : '开启调试工具'}
</Button>
</div>
<Text size={200} color="subtle" style={{ marginTop: tokens.spacingVerticalS, display: 'block' }}>
localStorageCookies
</Text>
</div>
{/* 确认对话框 */}
@@ -1276,3 +1290,4 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
export default AdminDashboard;

View File

@@ -0,0 +1,204 @@
import React, { useState, useMemo } from 'react';
import {
makeStyles,
Button,
tokens,
Text,
Textarea,
Dropdown,
Option,
Label,
Input
} from '@fluentui/react-components';
import { Dismiss24Regular, Play24Regular } from '@fluentui/react-icons';
import * as Api from '../api';
import * as AdminApi from '../admin_api';
import { API_CONFIG } from '../config';
const useStyles = makeStyles({
modalOverlay: {
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
backgroundColor: 'rgba(0, 0, 0, 0.4)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10000,
},
modalContent: {
backgroundColor: tokens.colorNeutralBackground1,
padding: tokens.spacingHorizontalXXL,
borderRadius: tokens.borderRadiusXLarge,
boxShadow: tokens.shadow64,
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalM,
width: '600px',
maxWidth: '90vw',
maxHeight: '90vh',
position: 'relative',
overflowY: 'auto',
},
closeButton: {
position: 'absolute',
top: tokens.spacingVerticalS,
right: tokens.spacingHorizontalS,
},
title: {
fontSize: tokens.fontSizeBase500,
fontWeight: tokens.fontWeightSemibold,
marginBottom: tokens.spacingVerticalS,
},
section: {
display: 'flex',
flexDirection: 'column',
gap: tokens.spacingVerticalS,
},
resultBox: {
backgroundColor: tokens.colorNeutralBackground2,
padding: tokens.spacingHorizontalM,
borderRadius: tokens.borderRadiusMedium,
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
maxHeight: '200px',
overflow: 'auto',
fontSize: tokens.fontSizeBase200,
wordBreak: 'break-all',
},
});
interface DevToolsModalProps {
onClose: () => void;
}
const DevToolsModal: React.FC<DevToolsModalProps> = ({ onClose }) => {
const styles = useStyles();
const [selectedFunc, setSelectedFunc] = useState<string>('');
const [params, setParams] = useState<string>('[]');
const [result, setResult] = useState<string>('');
const [loading, setLoading] = useState(false);
// Combine APIs and filter functions
const apiFunctions = useMemo(() => {
const all = { ...Api, ...AdminApi };
return Object.entries(all)
.filter(([_, value]) => typeof value === 'function')
.map(([key]) => key)
.sort();
}, []);
const handleExecute = async () => {
if (!selectedFunc) return;
setLoading(true);
setResult('Executing...');
try {
// Parse params
let args = [];
try {
const parsed = JSON.parse(params);
if (Array.isArray(parsed)) {
args = parsed;
} else {
throw new Error('Params must be a JSON array, e.g. ["arg1", 123]');
}
} catch (e: any) {
setResult(`Error parsing params: ${e.message}`);
setLoading(false);
return;
}
const allApi: any = { ...Api, ...AdminApi };
const func = allApi[selectedFunc];
if (typeof func !== 'function') {
setResult('Selected item is not a function.');
setLoading(false);
return;
}
const res = await func(...args);
setResult(JSON.stringify(res, null, 2));
} catch (e: any) {
console.error(e);
setResult(`Error: ${e.message}\n${JSON.stringify(e, null, 2)}`);
} finally {
setLoading(false);
}
};
return (
<div className={styles.modalOverlay}>
<div className={styles.modalContent} role="dialog" aria-modal="true" aria-label="后端调试工具">
<Button
icon={<Dismiss24Regular />}
appearance="transparent"
className={styles.closeButton}
onClick={onClose}
aria-label="关闭"
/>
<Text as="h2" className={styles.title}></Text>
<div className={styles.section}>
<Label>Current Backend URL</Label>
<Input value={API_CONFIG.BASE_URL} readOnly />
</div>
<div className={styles.section}>
<Label>Select Function</Label>
<Dropdown
placeholder="Select an API function"
onOptionSelect={(_, data) => setSelectedFunc(data.optionValue || '')}
value={selectedFunc}
style={{ width: '100%' }}
>
{apiFunctions.map((funcName) => (
<Option key={funcName} value={funcName}>
{funcName}
</Option>
))}
</Dropdown>
</div>
<div className={styles.section}>
<Label>Parameters (JSON Array)</Label>
<Textarea
value={params}
onChange={(_, data) => setParams(data.value)}
placeholder='e.g. ["arg1", 123] or []'
rows={3}
style={{ fontFamily: 'monospace' }}
/>
</div>
<div className={styles.section}>
<Button
appearance="primary"
icon={<Play24Regular />}
onClick={handleExecute}
disabled={!selectedFunc || loading}
>
Execute
</Button>
</div>
<div className={styles.section}>
<Label>Result</Label>
<div className={styles.resultBox}>
{result || 'No result yet'}
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: tokens.spacingVerticalM }}>
<Button appearance="subtle" onClick={onClose}></Button>
</div>
</div>
</div>
);
};
export default DevToolsModal;