- server.js: serveIndex 改读构建产物 dist/index.html(占位符替换保留);新增 /admin* → dist/admin.html;/forum/manage/:id 改 302 重定向 - Setup 初始化向导三步迁移;Embed 面板嵌入迁移(proxy 代理) - v1 前端全部退役(12 html + public/js/ 12 文件 + deploy 脚本) - README/AGENTS.md 更新为 v2 架构(React+Vite、better-sqlite3、构建流程)
167 lines
7.1 KiB
React
167 lines
7.1 KiB
React
import React, { useEffect, useState } from 'react';
|
||
import { request } from '../api/client.js';
|
||
import { showSnackbar } from '../lib/utils.js';
|
||
|
||
const THEMES = [
|
||
{ color: '#6750a4', name: 'default', label: '默认' },
|
||
{ color: '#0288d1', name: 'ocean', label: '海洋' },
|
||
{ color: '#2e7d32', name: 'nature', label: '自然' },
|
||
{ color: '#e65100', name: 'sunset', label: '日落' },
|
||
];
|
||
|
||
/**
|
||
* 初始化向导(迁移自 setup.html + routes/setup.js 三步流程):
|
||
* 1. 管理员密码(≥6 位、两次一致、不能等于 admin123)→ 2. 网站名称 + 主题色 → 3. 确认并完成。
|
||
* 提交 POST /api/setup/complete;已完成初始化时提示并跳首页。
|
||
*/
|
||
export default function Setup() {
|
||
const [status, setStatus] = useState(null); // null=检查中
|
||
const [step, setStep] = useState(1);
|
||
const [adminPw, setAdminPw] = useState('');
|
||
const [adminPwConfirm, setAdminPwConfirm] = useState('');
|
||
const [siteName, setSiteName] = useState('RainWeb');
|
||
const [theme, setTheme] = useState({ color: '#6750a4', name: 'default', label: '默认' });
|
||
const [error, setError] = useState('');
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
useEffect(() => {
|
||
request('/setup/status')
|
||
.then((s) => setStatus(s || {}))
|
||
.catch(() => setStatus({ setup_complete: true })); // 接口异常不阻塞,视为已完成
|
||
}, []);
|
||
|
||
const next = (n) => {
|
||
setError('');
|
||
if (n === 2) {
|
||
if (adminPw.length < 6) { setError('密码至少6位'); return; }
|
||
if (adminPw !== adminPwConfirm) { setError('两次密码不一致'); return; }
|
||
if (adminPw === 'admin123') { setError('新密码不能与默认密码相同'); return; }
|
||
}
|
||
setStep(n);
|
||
};
|
||
|
||
const complete = async () => {
|
||
setBusy(true);
|
||
try {
|
||
await request('/setup/complete', {
|
||
method: 'POST',
|
||
body: {
|
||
password: adminPw,
|
||
site_name: siteName.trim() || 'RainWeb',
|
||
primary_color: theme.color,
|
||
theme_preset: theme.name,
|
||
},
|
||
});
|
||
showSnackbar('初始化完成!');
|
||
setTimeout(() => { window.location.href = '/'; }, 1000);
|
||
} catch (e) {
|
||
setError(e.message || '初始化失败');
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
if (status === null) {
|
||
return <div className="loading"><div className="spinner"></div></div>;
|
||
}
|
||
|
||
// 已完成初始化:提示并跳首页
|
||
if (status.setup_complete) {
|
||
return (
|
||
<div style={{ minHeight: '70vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
|
||
<div className="card" style={{ width: '100%', maxWidth: 420, padding: '40px 32px', textAlign: 'center' }}>
|
||
<h1 style={{ fontSize: 28, fontWeight: 500, marginBottom: 8 }}>✅ 已完成初始化</h1>
|
||
<p className="text-muted" style={{ marginBottom: 24, fontSize: 14 }}>RainWeb 已初始化完成,可正常使用。</p>
|
||
<a href="/" className="btn btn-filled w-full">进入首页</a>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const stepDot = (n) => ({
|
||
width: n === step ? 28 : 10,
|
||
height: 10,
|
||
borderRadius: n === step ? 5 : '50%',
|
||
background: n < step ? 'var(--md-ref-primary-container)' : (n === step ? 'var(--md-ref-primary)' : 'var(--md-ref-outline-variant)'),
|
||
transition: 'all 0.3s',
|
||
});
|
||
|
||
return (
|
||
<div style={{ minHeight: '70vh', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
|
||
<div className="card" style={{ width: '100%', maxWidth: 480, padding: '40px 32px' }}>
|
||
<h1 style={{ fontSize: 28, fontWeight: 500, textAlign: 'center', marginBottom: 4 }}>🌧️ RainWeb</h1>
|
||
<p style={{ textAlign: 'center', color: 'var(--md-ref-on-surface-variant)', marginBottom: 28, fontSize: 14 }}>
|
||
欢迎使用!请完成以下初始化设置
|
||
</p>
|
||
<div style={{ display: 'flex', gap: 8, justifyContent: 'center', marginBottom: 24 }}>
|
||
{[1, 2, 3].map((n) => <div key={n} style={stepDot(n)} />)}
|
||
</div>
|
||
|
||
{step === 1 && (
|
||
<div>
|
||
<h3 style={{ fontWeight: 500, marginBottom: 16 }}>设置管理员密码</h3>
|
||
<div className="form-group">
|
||
<label>新密码 *</label>
|
||
<input type="password" value={adminPw} onChange={(e) => setAdminPw(e.target.value)} placeholder="至少6位" autoComplete="new-password" />
|
||
</div>
|
||
<div className="form-group">
|
||
<label>确认密码 *</label>
|
||
<input type="password" value={adminPwConfirm} onChange={(e) => setAdminPwConfirm(e.target.value)} placeholder="再次输入" autoComplete="new-password" onKeyDown={(e) => { if (e.key === 'Enter') next(2); }} />
|
||
</div>
|
||
<button className="btn btn-filled w-full" onClick={() => next(2)}>下一步</button>
|
||
</div>
|
||
)}
|
||
|
||
{step === 2 && (
|
||
<div>
|
||
<h3 style={{ fontWeight: 500, marginBottom: 16 }}>网站设置</h3>
|
||
<div className="form-group">
|
||
<label>网站名称</label>
|
||
<input type="text" value={siteName} onChange={(e) => setSiteName(e.target.value)} placeholder="我的网站" />
|
||
</div>
|
||
<div className="form-group">
|
||
<label>选择主题色</label>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 12 }}>
|
||
{THEMES.map((t) => (
|
||
<div
|
||
key={t.name}
|
||
onClick={() => setTheme({ color: t.color, name: t.name, label: t.label })}
|
||
style={{
|
||
padding: 16,
|
||
borderRadius: 12,
|
||
border: '2px solid ' + (theme.name === t.name ? 'var(--md-ref-primary)' : 'var(--md-ref-outline-variant)'),
|
||
cursor: 'pointer',
|
||
textAlign: 'center',
|
||
background: theme.name === t.name ? 'var(--md-ref-primary-container)' : 'transparent',
|
||
transition: 'all 0.2s',
|
||
}}
|
||
>
|
||
<div style={{ width: 32, height: 32, borderRadius: '50%', margin: '0 auto 8px', background: t.color }} />
|
||
<div>{t.label}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<button className="btn btn-filled w-full" onClick={() => next(3)}>下一步</button>
|
||
</div>
|
||
)}
|
||
|
||
{step === 3 && (
|
||
<div>
|
||
<h3 style={{ fontWeight: 500, marginBottom: 16 }}>确认并完成</h3>
|
||
<div className="card" style={{ padding: 16, marginBottom: 16, fontSize: 14 }}>
|
||
<div style={{ marginBottom: 8 }}><strong>管理员密码</strong>: 已设置</div>
|
||
<div style={{ marginBottom: 8 }}><strong>网站名称</strong>: {siteName.trim() || 'RainWeb'}</div>
|
||
<div><strong>主题色</strong>: {theme.label}</div>
|
||
</div>
|
||
<button className="btn btn-filled w-full" onClick={complete} disabled={busy}>
|
||
{busy ? '处理中...' : '完成初始化'}
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
{error && <div style={{ color: 'var(--md-ref-error)', fontSize: 14, marginTop: 12 }}>{error}</div>}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|