feat: 悬挂ICP备案号,修复验证码、500登录、/admin Failed to Fetch,优化首页编辑器100%全屏平铺

This commit is contained in:
Chen Xiao
2026-06-01 15:28:18 +08:00
parent 7fec1aa9fd
commit 038ce9e831
12 changed files with 609 additions and 59 deletions
+33 -13
View File
@@ -1,5 +1,6 @@
import { Routes, Route } from 'react-router-dom';
import { Routes, Route, useLocation } from 'react-router-dom';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Login from './pages/Login';
import Register from './pages/Register';
import MD2Doc from './pages/MD2Doc';
@@ -9,19 +10,38 @@ import Issues from './pages/Issues';
import IssueDetail from './pages/IssueDetail';
export default function App() {
const location = useLocation();
const isHomePage = location.pathname === '/';
if (isHomePage) {
// 首页(MD2Doc)完全平铺屏幕,配合 flex 容器,使 MD2Doc 高度自动填充 Navbar 下方剩余空间,不产生页面滚动条
return (
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<Navbar />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minHeight: 0 }}>
<Routes>
<Route path="/" element={<MD2Doc />} />
</Routes>
</div>
</div>
);
}
// 其它分页面采用 Flex 自适应贴底页脚布局
return (
<>
<div style={{ minHeight: '100vh', display: 'flex', flexDirection: 'column' }}>
<Navbar />
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/admin" element={<Admin />} />
<Route path="/profile" element={<Profile />} />
<Route path="/issues" element={<Issues />} />
<Route path="/issues/:id" element={<IssueDetail />} />
{/* 取消拦截,将转换工具设为首页 */}
<Route path="/" element={<MD2Doc />} />
</Routes>
</>
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/admin" element={<Admin />} />
<Route path="/profile" element={<Profile />} />
<Route path="/issues" element={<Issues />} />
<Route path="/issues/:id" element={<IssueDetail />} />
</Routes>
</div>
<Footer />
</div>
);
}
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
export default function Footer() {
const currentYear = new Date().getFullYear();
return (
<footer style={styles.footer}>
<div style={styles.container}>
<span style={styles.copyright}>
© {currentYear} To Docx. 保留所有权利
</span>
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noreferrer"
style={styles.link}
onMouseOver={(e) => e.target.style.color = '#3b82f6'}
onMouseOut={(e) => e.target.style.color = '#9ca3af'}
>
闽ICP备2026017655号-1
</a>
</div>
</footer>
);
}
const styles = {
footer: {
padding: '20px 0',
backgroundColor: '#f9fafb',
borderTop: '1px solid #e5e7eb',
width: '100%',
boxSizing: 'border-box',
marginTop: 'auto'
},
container: {
maxWidth: '1200px',
margin: '0 auto',
padding: '0 20px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '16px',
flexWrap: 'wrap',
fontSize: '13px',
color: '#9ca3af'
},
copyright: {
fontWeight: '400'
},
link: {
color: '#9ca3af',
textDecoration: 'none',
transition: 'color 0.2s ease',
fontWeight: '500'
}
};
+7 -7
View File
@@ -48,9 +48,9 @@ export default function Admin() {
try {
const token = localStorage.getItem('token');
const [usersRes, settingsRes, issuesRes] = await Promise.all([
fetch('http://localhost:3001/api/admin/users', { headers: { Authorization: `Bearer ${token}` } }),
fetch('http://localhost:3001/api/admin/settings', { headers: { Authorization: `Bearer ${token}` } }),
fetch('http://localhost:3001/api/admin/issues', { headers: { Authorization: `Bearer ${token}` } })
fetch('/api/admin/users', { headers: { Authorization: `Bearer ${token}` } }),
fetch('/api/admin/settings', { headers: { Authorization: `Bearer ${token}` } }),
fetch('/api/admin/issues', { headers: { Authorization: `Bearer ${token}` } })
]);
if (!usersRes.ok || !settingsRes.ok) {
@@ -82,7 +82,7 @@ export default function Admin() {
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:3001/api/admin/settings', {
const res = await fetch('/api/admin/settings', {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
@@ -105,7 +105,7 @@ export default function Admin() {
const fetchIssueDetail = async (issueId) => {
try {
const token = localStorage.getItem('token');
const res = await fetch(`http://localhost:3001/api/issues/${issueId}`, {
const res = await fetch(`/api/issues/${issueId}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
@@ -125,7 +125,7 @@ export default function Admin() {
setReplyMsg('');
try {
const token = localStorage.getItem('token');
const res = await fetch(`http://localhost:3001/api/admin/issues/${selectedIssue.id}/reply`, {
const res = await fetch(`/api/admin/issues/${selectedIssue.id}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -150,7 +150,7 @@ export default function Admin() {
const handleStatusChange = async (issueId, newStatus) => {
try {
const token = localStorage.getItem('token');
const res = await fetch(`http://localhost:3001/api/admin/issues/${issueId}/status`, {
const res = await fetch(`/api/admin/issues/${issueId}/status`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
+2 -2
View File
@@ -35,7 +35,7 @@ export default function IssueDetail() {
const fetchIssue = async () => {
try {
const token = localStorage.getItem('token');
const res = await fetch(`http://localhost:3001/api/issues/${id}`, {
const res = await fetch(`/api/issues/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
@@ -56,7 +56,7 @@ export default function IssueDetail() {
setReplyMsg('');
try {
const token = localStorage.getItem('token');
const res = await fetch(`http://localhost:3001/api/issues/${id}/reply`, {
const res = await fetch(`/api/issues/${id}/reply`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+2 -2
View File
@@ -51,7 +51,7 @@ export default function Issues() {
const fetchMyIssues = async () => {
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:3001/api/issues/my', {
const res = await fetch('/api/issues/my', {
headers: { Authorization: `Bearer ${token}` }
});
if (res.ok) {
@@ -82,7 +82,7 @@ export default function Issues() {
description: description.trim(),
markdown_sample: attachMarkdown ? getEditorMarkdown() : ''
};
const res = await fetch('http://localhost:3001/api/issues', {
const res = await fetch('/api/issues', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+47 -24
View File
@@ -268,7 +268,7 @@ export default function MD2Doc() {
const token = localStorage.getItem('token');
if (!token) return;
try {
const res = await fetch('http://localhost:3001/api/history', {
const res = await fetch('/api/history', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (res.ok) {
@@ -452,7 +452,7 @@ export default function MD2Doc() {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await fetch('http://localhost:3001/api/convert', {
const response = await fetch('/api/convert', {
method: 'POST',
headers,
body: JSON.stringify({ markdown, styleOptions })
@@ -499,14 +499,11 @@ export default function MD2Doc() {
{user && (
<aside style={{
...styles.sidebar,
width: showHistory ? '260px' : '40px',
padding: showHistory ? '16px' : '16px 4px',
alignItems: showHistory ? 'stretch' : 'center',
cursor: showHistory ? 'default' : 'pointer',
transition: 'width 0.2s ease-in-out'
width: showHistory ? '260px' : '0px',
padding: showHistory ? '16px' : '0px',
borderRight: showHistory ? '1px solid #d1d5db' : 'none',
transition: 'all 0.2s ease-in-out'
}}
onClick={() => { if (!showHistory) setShowHistory(true); }}
title={!showHistory ? "点击展开转换记录" : ""}
>
{showHistory ? (
<>
@@ -545,6 +542,18 @@ export default function MD2Doc() {
<main style={styles.main}>
<section style={styles.inputSection}>
<div style={styles.toolbar}>
{user && (
<>
<button
style={{...styles.btn, backgroundColor: showHistory ? '#3b82f6' : '#6b7280'}}
onClick={() => setShowHistory(!showHistory)}
title={showHistory ? "隐藏历史记录" : "显示历史记录"}
>
{showHistory ? "📋 隐藏历史" : "📋 历史记录"}
</button>
<div style={{width: '1px', backgroundColor: '#d1d5db', margin: '0 4px'}}></div>
</>
)}
<button
style={{...styles.btn, backgroundColor: isSyncScroll ? '#10b981' : '#6b7280', padding: '4px 8px'}}
onClick={() => setIsSyncScroll(!isSyncScroll)}
@@ -680,13 +689,27 @@ export default function MD2Doc() {
</main>
<div style={styles.bottomBar}>
<button
style={styles.mainExportBtn}
onClick={handleExportWord}
disabled={exporting || !markdown.trim()}
>
{exporting ? '正在生成文档...' : '一键导出 Word 文档 (.docx)'}
</button>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '8px' }}>
<button
style={styles.mainExportBtn}
onClick={handleExportWord}
disabled={exporting || !markdown.trim()}
>
{exporting ? '正在生成文档...' : '一键导出 Word 文档 (.docx)'}
</button>
<div style={{ fontSize: '12px', marginTop: '4px' }}>
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noreferrer"
style={{ color: '#9ca3af', textDecoration: 'none', transition: 'color 0.2s' }}
onMouseOver={(e) => e.target.style.color = '#3b82f6'}
onMouseOut={(e) => e.target.style.color = '#9ca3af'}
>
闽ICP备2026017655号-1
</a>
</div>
</div>
</div>
</div>
@@ -816,22 +839,22 @@ export default function MD2Doc() {
}
const styles = {
container: { maxWidth: '1600px', margin: '0 auto', padding: '24px', height: '100vh', display: 'flex', gap: '20px', backgroundColor: '#f9fafb', boxSizing: 'border-box' },
sidebar: { width: '260px', backgroundColor: 'white', borderRadius: '8px', padding: '16px', border: '1px solid #d1d5db', display: 'flex', flexDirection: 'column', height: '100%', boxSizing: 'border-box' },
container: { width: '100%', maxWidth: '100%', margin: '0', padding: '0', height: '100%', display: 'flex', gap: '0', backgroundColor: '#f9fafb', boxSizing: 'border-box' },
sidebar: { backgroundColor: 'white', display: 'flex', flexDirection: 'column', height: '100%', boxSizing: 'border-box', overflow: 'hidden' },
sidebarTitle: { fontSize: '18px', fontWeight: 'bold', margin: '0 0 4px 0' },
historyList: { flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '10px' },
historyItem: { padding: '10px', backgroundColor: '#f3f4f6', borderRadius: '6px', cursor: 'pointer', transition: 'background 0.2s' },
historyTime: { fontSize: '12px', color: '#6b7280', marginBottom: '4px' },
historySummary: { fontSize: '14px', color: '#374151', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
mainContentWrapper: { flex: 1, display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' },
main: { display: 'flex', gap: '8px', flex: 1, overflow: 'hidden' },
main: { display: 'flex', gap: '0', flex: 1, overflow: 'hidden' },
inputSection: { flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', height: '100%', minWidth: 0 },
textarea: { flex: 1, padding: '40px 20px 20px 20px', border: '1px solid #d1d5db', borderRadius: '8px', fontFamily: 'monospace', fontSize: '15px', resize: 'none', boxSizing: 'border-box' },
toolbar: { position: 'absolute', top: '10px', right: '10px', display: 'flex', gap: '6px', zIndex: 10 },
textarea: { flex: 1, padding: '45px 24px 24px 24px', border: 'none', borderRight: '1px solid #d1d5db', fontFamily: 'monospace', fontSize: '15px', resize: 'none', boxSizing: 'border-box', outline: 'none' },
toolbar: { position: 'absolute', top: '10px', right: '15px', display: 'flex', gap: '6px', zIndex: 10 },
btn: { padding: '4px 10px', fontSize: '12px', backgroundColor: '#3b82f6', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', opacity: 0.85, transition: 'opacity 0.2s' },
previewSection: { flex: 1, position: 'relative', padding: '0', backgroundColor: 'white', border: '1px solid #d1d5db', borderRadius: '8px', overflowY: 'auto', height: '100%', boxSizing: 'border-box', minWidth: 0 },
previewContent: { backgroundColor: 'white', width: '100%', minHeight: '100%', padding: '40px', lineHeight: 1.6, boxSizing: 'border-box' },
bottomBar: { marginTop: '20px', display: 'flex', justifyContent: 'center' },
previewSection: { flex: 1, position: 'relative', padding: '0', backgroundColor: 'white', border: 'none', overflowY: 'auto', height: '100%', boxSizing: 'border-box', minWidth: 0 },
previewContent: { backgroundColor: 'white', width: '100%', minHeight: '100%', padding: '40px 48px', lineHeight: 1.6, boxSizing: 'border-box' },
bottomBar: { padding: '12px 0', borderTop: '1px solid #e5e7eb', display: 'flex', justifyContent: 'center', backgroundColor: '#fff' },
mainExportBtn: { padding: '12px 32px', backgroundColor: '#10b981', color: 'white', border: 'none', borderRadius: '8px', fontSize: '16px', fontWeight: 'bold', cursor: 'pointer' },
// Modal 样式
+3 -3
View File
@@ -16,7 +16,7 @@ export default function Profile() {
const [smtpEnabled, setSmtpEnabled] = useState(false);
useEffect(() => {
fetch('http://localhost:3001/api/sys/config')
fetch('/api/sys/config')
.then(res => res.json())
.then(data => {
if (data && data.smtp_enabled !== undefined) {
@@ -37,7 +37,7 @@ export default function Profile() {
setLoadingCode(true);
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:3001/api/auth/send-code', {
const res = await fetch('/api/auth/send-code', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` }
});
@@ -69,7 +69,7 @@ export default function Profile() {
setLoadingSubmit(true);
try {
const token = localStorage.getItem('token');
const res = await fetch('http://localhost:3001/api/auth/update-profile', {
const res = await fetch('/api/auth/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
+1 -1
View File
@@ -22,7 +22,7 @@ export default function Register() {
const fetchCaptcha = async () => {
try {
const res = await fetch('http://localhost:3001/api/captcha');
const res = await fetch('/api/captcha');
if (res.ok) {
const data = await res.json();
setCaptchaId(data.captcha_id);