Feat:增加后端调试工具
This commit is contained in:
27
src/App.tsx
27
src/App.tsx
@@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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' }}>
|
||||
将清理 localStorage、Cookies 及静态资源缓存。
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 确认对话框 */}
|
||||
@@ -1276,3 +1290,4 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
|
||||
export default AdminDashboard;
|
||||
|
||||
|
||||
|
||||
|
||||
204
src/components/DevToolsModal.tsx
Normal file
204
src/components/DevToolsModal.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user