Feat:增加后端调试工具
This commit is contained in:
27
src/App.tsx
27
src/App.tsx
@@ -1,7 +1,7 @@
|
|||||||
// 你好,感谢你愿意看源代码,但是悄悄告诉你,代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。
|
// 你好,感谢你愿意看源代码,但是悄悄告诉你,代码其实是AI写的所以质量很差喵。抱歉呜呜呜😭。
|
||||||
|
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
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 { BrowserRouter, Routes, Route } from 'react-router-dom';
|
||||||
import PostCard from './components/PostCard';
|
import PostCard from './components/PostCard';
|
||||||
import MainLayout from './layouts/MainLayout';
|
import MainLayout from './layouts/MainLayout';
|
||||||
@@ -20,6 +20,8 @@ import NotFound from './pages/NotFound';
|
|||||||
import ImageViewer from './components/ImageViewer';
|
import ImageViewer from './components/ImageViewer';
|
||||||
import NoticeModal from './components/NoticeModal';
|
import NoticeModal from './components/NoticeModal';
|
||||||
import type { NoticeData } from './components/NoticeModal';
|
import type { NoticeData } from './components/NoticeModal';
|
||||||
|
import DevToolsModal from './components/DevToolsModal';
|
||||||
|
import { Bug24Regular } from '@fluentui/react-icons';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [isDarkMode, setIsDarkMode] = React.useState(() => {
|
const [isDarkMode, setIsDarkMode] = React.useState(() => {
|
||||||
@@ -54,6 +56,13 @@ function App() {
|
|||||||
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
|
const [imageViewer, setImageViewer] = useState<{ open: boolean; src?: string; alt?: string }>({ open: false });
|
||||||
const [noticeData, setNoticeData] = useState<NoticeData | null>(null);
|
const [noticeData, setNoticeData] = useState<NoticeData | null>(null);
|
||||||
const [showNotice, setShowNotice] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
getNotice().then(data => {
|
getNotice().then(data => {
|
||||||
@@ -258,6 +267,20 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</FluentProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -270,3 +293,5 @@ export default App;
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
makeStyles,
|
makeStyles,
|
||||||
Button,
|
Button,
|
||||||
@@ -173,6 +173,7 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
|
|||||||
onToggleTheme
|
onToggleTheme
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
const styles = useStyles();
|
const styles = useStyles();
|
||||||
const [activeTab, setActiveTab] = React.useState<TabValue>('systemSettings');
|
const [activeTab, setActiveTab] = React.useState<TabValue>('systemSettings');
|
||||||
const [postReviewSubTab, setPostReviewSubTab] = React.useState<TabValue>('pending');
|
const [postReviewSubTab, setPostReviewSubTab] = React.useState<TabValue>('pending');
|
||||||
@@ -962,10 +963,23 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
|
|||||||
<Button appearance="secondary" onClick={() => setClearCacheConfirmOpen(true)}>
|
<Button appearance="secondary" onClick={() => setClearCacheConfirmOpen(true)}>
|
||||||
清除缓存
|
清除缓存
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
<Text size={200} color="subtle" style={{ marginTop: tokens.spacingVerticalS, display: 'block' }}>
|
|
||||||
将清理 localStorage、Cookies 及静态资源缓存。
|
|
||||||
</Text>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 确认对话框 */}
|
{/* 确认对话框 */}
|
||||||
@@ -1276,3 +1290,4 @@ const AdminDashboard: React.FC<AdminDashboardProps> = ({
|
|||||||
export default AdminDashboard;
|
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