/* global React, Button, SealIcon, SensitiveNotice, loadDrafts, saveDraft, deleteDraft, loadAutoDraft, saveAutoDraft, clearAutoDraft, DraftRecover */ const { useState: useStateForm, useEffect: useEffForm } = React; function LoginModal({ open, onClose, onSubmit, onRules }) { const [busy, setBusy] = useStateForm(null); // 'douyin' | 'wechat' | null const [authModes, setAuthModes] = useStateForm({ password: false, oauth: ['douyin', 'wechat'] }); // legacy password fallback (only used when backend explicitly enables it for dev/staging) const [legacyOpen, setLegacyOpen] = useStateForm(false); const [legacyId, setLegacyId] = useStateForm(''); const [legacyPw, setLegacyPw] = useStateForm(''); const [legacyErr, setLegacyErr] = useStateForm(''); const [legacyBusy, setLegacyBusy] = useStateForm(false); // Probe backend capabilities once when modal opens useEffForm(() => { if (!open || !window.bxApi) return; fetch((window.BX_API_BASE || '') + '/api/user/auth_modes', { credentials: 'include' }) .then(r => r.json()).then(setAuthModes).catch(() => {}); setBusy(null); setLegacyOpen(false); setLegacyErr(''); }, [open]); async function startOAuth(provider) { if (!window.bxApi) { window.showToast && window.showToast({ type: 'warning', msg: '第三方登录需在云端环境使用' }); return; } setBusy(provider); try { const r = await window.bxApi.oauthStart(provider, 'login'); // Full page redirect to the provider's authorize URL. The provider page // displays the QR code (desktop) or deep-links into its app (mobile). // After grant, provider redirects to /api/user/oauth/:provider/callback, // which sets a session cookie and 302's back to /mailbox/. window.location.href = r.authorize_url; } catch (e) { setBusy(null); if (e.error === 'provider_not_configured') { window.showToast && window.showToast({ type: 'warning', msg: '此授权尚未配置,请联系陛下管理员' }); } else { window.showToast && window.showToast({ type: 'error', msg: (e && e.message) || '授权请求失败' }); } } } async function legacyLogin() { if (legacyBusy) return; if (!legacyId.trim() || !legacyPw) { setLegacyErr('两栏皆不可空'); return; } setLegacyBusy(true); setLegacyErr(''); try { const u = await window.bxApi.login({ douyin_id: legacyId.trim(), password: legacyPw }); onSubmit(u); } catch (e) { setLegacyErr((e && e.message) || '登录失败'); } finally { setLegacyBusy(false); } } if (!open) return null; const oauthList = (authModes.oauth || []); const douyinAvail = oauthList.includes('douyin'); const wechatAvail = oauthList.includes('wechat'); return (
e.stopPropagation()}>

登入朝堂

本站只支持抖音扫码登录 · 朕通过抖音认你身份,无需另设口谕。
电脑访问 → 点下面按钮跳到抖音二维码页 → 用抖音 App 扫码
手机访问 → 点下面按钮自动跳到抖音 App 完成授权

{(douyinAvail || !window.bxApi) ? ( ) : (
抖音登录尚在配置中。请联系陛下管理员或稍后再试。
)} {wechatAvail && ( )}
点击即表示朕已阅 { e.preventDefault(); onRules && onRules(); }} style={{ color: 'var(--bronze)', cursor: 'pointer', textDecoration: 'underline', textUnderlineOffset: 3 }}>《投稿须知》。
朕仅获取你的抖音昵称与头像,不读你的私信、视频或粉丝。
{/* Legacy password form — only when backend explicitly enables PASSWORD_AUTH. */} {authModes.password && (
{!legacyOpen ? ( ) : (

测试通道,生产环境关闭

{legacyErr && {legacyErr}}
)}
)}
); } function ComposeForm({ onSubmit, draftToEdit }) { const [title, setTitle] = useStateForm(draftToEdit?.title || ''); const [body, setBody] = useStateForm(draftToEdit?.body || ''); const [cat, setCat] = useStateForm(draftToEdit?.cat || '情感困扰'); const [named, setNamed] = useStateForm(draftToEdit?.named || 'real'); const [clip, setClip] = useStateForm(draftToEdit?.clip || 'yes'); const [errs, setErrs] = useStateForm({}); const [recovered, setRecovered] = useStateForm(null); const [draftId, setDraftId] = useStateForm(draftToEdit?.id || null); const [images, setImages] = useStateForm(draftToEdit?.images || []); // [{id, url, name, size}] const [submitting, setSubmitting] = useStateForm(false); // double-click guard const [savingDraft, setSavingDraft] = useStateForm(false); const [sens, setSens] = useStateForm({ blocked: [], warnings: [] }); // 违规词检测结果 const fileInputRef = React.useRef(null); const checkTimer = React.useRef(null); const imagesRef = React.useRef(images); imagesRef.current = images; // 始终指向最新 images,供卸载时清理用 const MAX_IMAGES = 6; const MAX_IMAGE_MB = 5; // 组件卸载时(用户中途离开/不提交)兜底释放所有 blob: 预览 URL,避免内存泄漏。 useEffForm(() => () => { imagesRef.current.forEach(img => { if (img.url && img.url.startsWith('blob:')) URL.revokeObjectURL(img.url); }); }, []); function handleImageFiles(files) { const incoming = Array.from(files || []); if (incoming.length === 0) return; const room = MAX_IMAGES - images.length; if (room <= 0) { window.showToast && window.showToast({ type: 'warning', msg: `至多 ${MAX_IMAGES} 张图,不可再添` }); return; } const picked = incoming.slice(0, room); const added = []; picked.forEach(f => { if (!f.type.startsWith('image/')) { window.showToast && window.showToast({ type: 'error', msg: `「${f.name}」非图片,朕不收` }); return; } if (f.size > MAX_IMAGE_MB * 1024 * 1024) { window.showToast && window.showToast({ type: 'error', msg: `「${f.name}」超过 ${MAX_IMAGE_MB}MB` }); return; } added.push({ id: Date.now() + Math.random(), url: URL.createObjectURL(f), name: f.name, size: f.size, file: f }); }); if (added.length > 0) setImages(x => [...x, ...added]); if (incoming.length > room) window.showToast && window.showToast({ type: 'warning', msg: `只收了前 ${room} 张,余者请删一些再上` }); } function removeImage(id) { setImages(x => { const gone = x.find(i => i.id === id); if (gone && gone.url && gone.url.startsWith('blob:')) URL.revokeObjectURL(gone.url); return x.filter(i => i.id !== id); }); } function onDrop(e) { e.preventDefault(); handleImageFiles(e.dataTransfer.files); } function onDragOver(e) { e.preventDefault(); } // Draft recovery on mount — only if not already editing a draft useEffForm(() => { if (draftToEdit) return; const auto = loadAutoDraft(); if (auto && (auto.title || auto.body) && (auto.body || '').length > 10) { setRecovered(auto); } }, []); // Auto-save every 30s useEffForm(() => { const t = setInterval(() => { if (title.trim() || body.trim()) saveAutoDraft({ title, body, cat, named, clip }); }, 30000); return () => clearInterval(t); }, [title, body, cat, named, clip]); // 违规词实时检测:停手 400ms 后查一次(防抖)。命中 block 词则禁止呈递。 useEffForm(() => { if (!window.bxApi) return; // 离线 mock 模式不查 const text = (title + '\n' + body).trim(); if (!text) { setSens({ blocked: [], warnings: [] }); return; } clearTimeout(checkTimer.current); checkTimer.current = setTimeout(() => { window.bxApi.checkContent(text) .then(r => setSens({ blocked: r.blocked || [], warnings: r.warnings || [] })) .catch(() => {}); // 网络/401 静默,不打扰打字 }, 400); return () => clearTimeout(checkTimer.current); }, [title, body]); function restore() { setTitle(recovered.title || ''); setBody(recovered.body || ''); setCat(recovered.cat || '情感困扰'); setRecovered(null); } function discardRecovered() { clearAutoDraft(); setRecovered(null); } function validate() { const e = {}; if (!title.trim()) e.title = '题目不可空'; else if (title.length > 30) e.title = '题目不过 30 字'; if (!body.trim()) e.body = '正文不可空'; else if (body.trim().length < 20) e.body = '正文至少 20 字 · 朕需略知全貌'; setErrs(e); return Object.keys(e).length === 0; } async function doSubmit() { if (submitting) return; // double-click guard if (!validate()) { window.showToast && window.showToast({ type: 'error', msg: '奏折有漏,请爱卿检查' }); return; } if (sens.blocked.length) { // 违规词未改,不放行 window.showToast && window.showToast({ type: 'error', msg: '奏折含违规词,请先修改后再呈' }); return; } setSubmitting(true); try { if (window.bxApi) { try { const res = await window.bxApi.submitLetter({ title, body, category: cat, anonymous: named === 'anon', allow_clip: clip === 'yes', use_priority_credit: false, }); // Upload images sequentially after we have a letter id for (const img of images) { if (img.file) { try { await window.bxApi.uploadImage(res.id, img.file); } catch (e) { window.showToast && window.showToast({ type: 'warning', msg: `图片 ${img.name} 上传失败: ${e.message}` }); } } } // Delete the draft we just submitted — from the correct store if (draftId) { if (typeof draftId === 'number' && window.bxApi) { try { await window.bxApi.deleteDraft(draftId); } catch {} } else { deleteDraft(draftId); } } // Free blob: URL previews to avoid memory leak. for (const img of images) { if (img.url && img.url.startsWith('blob:')) URL.revokeObjectURL(img.url); } clearAutoDraft(); onSubmit({ id: res.id }); return; } catch (err) { // 后端违规词闸(绕过前端直发也会被拦)。把命中词回填,标红正文。 if (err && err.error === 'sensitive_content') { if (err.details) setSens({ blocked: err.details.blocked || [], warnings: err.details.warnings || [] }); if (err.fields) setErrs(err.fields); window.showToast && window.showToast({ type: 'error', msg: '奏折含违规词,请修改后再呈' }); return; } if (err && err.fields) { setErrs(err.fields); return; } window.showToast && window.showToast({ type: 'error', msg: (err && err.message) || '呈递失败,请稍后再试' }); return; } } // offline fallback if (draftId) deleteDraft(draftId); clearAutoDraft(); onSubmit({ title, body, cat, images }); } finally { setSubmitting(false); } } async function doSaveDraft() { if (savingDraft) return; if (!title.trim() && !body.trim() && images.length === 0) { window.showToast && window.showToast({ type: 'info', msg: '尚无内容可存' }); return; } setSavingDraft(true); try { // Try server draft first (persists across devices). Fall back to localStorage. if (window.bxApi) { try { const payload = { title, body, category: cat, anonymous: named === 'anon', allow_clip: clip === 'yes', }; const r = typeof draftId === 'number' ? await window.bxApi.updateDraft(draftId, payload) : await window.bxApi.createDraft(payload); setDraftId(r.id); clearAutoDraft(); window.showToast && window.showToast({ type: 'success', msg: '奏折已入草稿匣' }); return; } catch (e) { if (e.status === 401) { window.showToast && window.showToast({ type: 'warning', msg: '请先入朝方可存草稿' }); return; } // else fall through to local } } const draft = { id: draftId, title, body, cat, named, clip, images }; const list = saveDraft(draft); const saved = list[0]; setDraftId(saved.id); clearAutoDraft(); window.showToast && window.showToast({ type: 'success', msg: '奏折已入草稿匣(本地)' }); } finally { setSavingDraft(false); } } return (
COMPOSE · 上奏

呈递奏折

朕听着呢,爱卿请讲

{recovered && }