import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react'; import { Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import { useAuth } from './auth'; import { useUI, PasswordInput } from './ui'; import { api, type InstanceWithStatus } from './api'; import InstanceView from './pages/Desktop'; import Admin from './pages/Admin'; const BUSY = ['downloading', 'extracting', 'installing']; // ---- 实例数据:侧栏 / 主页 / 实例视图共享,安装中轮询 ---- interface InstancesState { instances: InstanceWithStatus[]; loaded: boolean; reload: () => Promise; } const InstancesCtx = createContext({ instances: [], loaded: false, reload: async () => {} }); export const useInstances = () => useContext(InstancesCtx); function useInstancesLoader(): InstancesState { const [instances, setInstances] = useState([]); const [loaded, setLoaded] = useState(false); const timer = useRef(undefined); const reload = async () => { try { const { instances } = await api.listInstances(); setInstances(instances); } catch { /* 401 会被 api 层重定向到登录 */ } finally { setLoaded(true); } }; useEffect(() => { reload(); return () => window.clearTimeout(timer.current); }, []); useEffect(() => { window.clearTimeout(timer.current); if (instances.some((i) => BUSY.includes(i.wechat.phase))) timer.current = window.setTimeout(reload, 1500); return () => window.clearTimeout(timer.current); }, [instances]); return { instances, loaded, reload }; } // 实例状态点(颜色 + 文案) export function statusOf(inst: InstanceWithStatus): { cls: string; text: string } { const offline = inst.runtime !== 'running'; if (offline) return { cls: 'st-off', text: inst.runtime === 'missing' ? '未创建' : '已停止' }; if (BUSY.includes(inst.wechat.phase)) return { cls: 'st-busy', text: '处理中' }; if (inst.wechat.installed) return { cls: 'st-on', text: '在线' }; return { cls: 'st-warn', text: '待安装' }; } // ---- 图标 ---- const Icon = { home: ( ), gear: ( ), logout: ( ), collapse: ( ), menu: ( ), }; export default function AppShell() { const state = useInstancesLoader(); const { refresh } = useAuth(); const [collapsed, setCollapsed] = useState(() => localStorage.getItem('woc_sb_collapsed') === '1'); const [drawer, setDrawer] = useState(false); const [showPw, setShowPw] = useState(false); const [isDesktop, setIsDesktop] = useState(() => window.matchMedia('(min-width: 768px)').matches); const loc = useLocation(); useEffect(() => { const m = window.matchMedia('(min-width: 768px)'); const h = () => setIsDesktop(m.matches); m.addEventListener('change', h); return () => m.removeEventListener('change', h); }, []); useEffect(() => setDrawer(false), [loc.pathname]); // 路由变化关抽屉 // 移动端不收成窄栏(改用抽屉);折叠仅桌面生效 const railed = collapsed && isDesktop; const toggleCollapsed = () => setCollapsed((c) => { localStorage.setItem('woc_sb_collapsed', c ? '0' : '1'); return !c; }); useEffect(() => { const onKey = (e: KeyboardEvent) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'b') { e.preventDefault(); toggleCollapsed(); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, []); const openMenu = () => setDrawer(true); const openChangePassword = () => setShowPw(true); return (
setDrawer(false)} />
} /> } /> } /> } />
{showPw && setShowPw(false)} onSaved={() => refresh()} />} ); } function Sidebar({ collapsed, onToggleCollapsed }: { collapsed: boolean; onToggleCollapsed: () => void }) { const { user, logout } = useAuth(); const { confirm } = useUI(); const { instances } = useInstances(); const nav = useNavigate(); const loc = useLocation(); const isAdmin = user?.role === 'admin'; const go = (p: string) => nav(p); return ( ); } function HomeView({ onOpenMenu, onChangePassword }: { onOpenMenu: () => void; onChangePassword: () => void }) { const { user } = useAuth(); const { instances, loaded } = useInstances(); const nav = useNavigate(); const isAdmin = user?.role === 'admin'; return (
主页
你好,{user?.username} {isAdmin && 管理员}
{user?.mustChangePassword && ( )}
我的微信实例 {isAdmin && ( )}
{loaded && instances.length === 0 ? (
还没有微信实例
{isAdmin ? '去「管理」新建一个微信实例' : '请联系管理员为你分配实例'}
) : (
{instances.map((inst) => { const st = statusOf(inst); return ( ); })}
)}
); } export function ChangePassword({ onClose, onSaved }: { onClose: () => void; onSaved?: () => void }) { const [oldPassword, setOld] = useState(''); const [newPassword, setNew] = useState(''); const [confirm, setConfirm] = useState(''); const [msg, setMsg] = useState(''); const [busy, setBusy] = useState(false); const mismatch = confirm.length > 0 && newPassword !== confirm; const canSubmit = !busy && !!oldPassword && newPassword.length >= 6 && newPassword === confirm; const submit = async (e: React.FormEvent) => { e.preventDefault(); setMsg(''); if (newPassword !== confirm) { setMsg('两次输入的新密码不一致'); return; } setBusy(true); try { await api.changePassword(oldPassword, newPassword); setMsg('修改成功'); onSaved?.(); setTimeout(onClose, 800); } catch (e: any) { setMsg(e.message || '修改失败'); } finally { setBusy(false); } }; return (
e.stopPropagation()} onSubmit={submit}>

修改密码

{mismatch &&
两次输入的新密码不一致
} {msg &&
{msg}
}
); }