Initial open source release

This commit is contained in:
joeseesun
2026-05-24 13:42:56 +08:00
commit 8abb29e13e
75 changed files with 13298 additions and 0 deletions
+143
View File
@@ -0,0 +1,143 @@
'use client';
import { useEffect, useState } from 'react';
import { CheckCircle2, Database, ShieldCheck, UserRound, Wrench } from 'lucide-react';
type SetupStatus = {
ok: boolean;
dataDir: string;
configured: boolean;
config: { myNicknames: string[]; demoMode: boolean; privacyConfirmed: boolean; defaultSyncDays: number };
checks: { wxInstalled: boolean; wxDaemonRunning: boolean; wxDaemonPid: number | null };
};
export default function SetupPage() {
const [status, setStatus] = useState<SetupStatus | null>(null);
const [names, setNames] = useState('');
const [demoMode, setDemoMode] = useState(false);
const [privacyConfirmed, setPrivacyConfirmed] = useState(false);
const [defaultSyncDays, setDefaultSyncDays] = useState(7);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
(async () => {
const res = await fetch('/api/setup', { cache: 'no-store' });
const json = (await res.json()) as SetupStatus;
setStatus(json);
setNames(json.config.myNicknames.join(', '));
setDemoMode(json.config.demoMode);
setPrivacyConfirmed(json.config.privacyConfirmed);
setDefaultSyncDays(json.config.defaultSyncDays ?? 7);
})();
}, []);
async function submit() {
setBusy(true);
setError(null);
try {
const res = await fetch('/api/setup', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
myNicknames: names.split(',').map((name) => name.trim()).filter(Boolean),
demoMode,
privacyConfirmed,
defaultSyncDays,
}),
});
const json = await res.json();
if (!res.ok || !json.ok) throw new Error(json.error ?? '保存失败');
window.location.href = '/';
} catch (e) {
setError(e instanceof Error ? e.message : '保存失败');
} finally {
setBusy(false);
}
}
return (
<main className="min-h-screen bg-[var(--bg)] px-6 py-8 text-[var(--text)]">
<div className="mx-auto max-w-4xl">
<div className="report-kicker">WeChat Radar Setup</div>
<h1 className="mt-2 text-[28px] font-semibold"></h1>
<p className="mt-2 text-[13px] leading-relaxed text-[var(--text-2)]">
使
</p>
<div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-2">
<section className="card p-5">
<SectionTitle icon={<Wrench size={15} />} title="环境检查" />
<CheckRow label="wx-cli" ok={status?.checks.wxInstalled ?? false} detail={status?.checks.wxInstalled ? '已安装' : '未检测到 wx 命令'} />
<CheckRow label="wx-daemon" ok={status?.checks.wxDaemonRunning ?? false} detail={status?.checks.wxDaemonRunning ? `运行中 PID ${status?.checks.wxDaemonPid ?? ''}` : '未运行,可先使用 demo 模式'} />
<CheckRow label="数据目录" ok detail={status?.dataDir ?? '加载中'} />
</section>
<section className="card p-5">
<SectionTitle icon={<UserRound size={15} />} title="你的微信名" />
<label className="mt-3 block text-[12px] text-[var(--text-3)]"></label>
<input
value={names}
onChange={(e) => setNames(e.target.value)}
placeholder="张三, San Zhang, zhangsan"
className="control-surface mt-2 w-full rounded-md px-3 py-2 text-[13px] outline-none"
/>
<p className="mt-2 text-[11px] text-[var(--text-3)]"> @我的</p>
</section>
<section className="card p-5">
<SectionTitle icon={<Database size={15} />} title="数据模式" />
<label className="mt-4 flex items-center gap-2 text-[13px]">
<input type="checkbox" checked={demoMode} onChange={(e) => setDemoMode(e.target.checked)} />
使
</label>
<label className="mt-4 block text-[12px] text-[var(--text-3)]"></label>
<select
value={defaultSyncDays}
onChange={(e) => setDefaultSyncDays(Number(e.target.value))}
className="control-surface mt-2 rounded-md px-3 py-2 text-[13px] outline-none"
>
<option value={1}> 1 </option>
<option value={7}> 7 </option>
<option value={30}> 30 </option>
<option value={365}> 365 </option>
</select>
</section>
<section className="card p-5">
<SectionTitle icon={<ShieldCheck size={15} />} title="隐私确认" />
<label className="mt-4 flex items-start gap-2 text-[13px] leading-relaxed">
<input className="mt-1" type="checkbox" checked={privacyConfirmed} onChange={(e) => setPrivacyConfirmed(e.target.checked)} />
<span> SQLite </span>
</label>
</section>
</div>
{error && <div className="mt-4 text-[13px] text-[var(--danger)]">{error}</div>}
<div className="mt-6 flex justify-end gap-2">
<button className="btn" onClick={() => window.location.href = '/'}></button>
<button className="btn btn-primary" disabled={busy || !privacyConfirmed} onClick={submit}>
{busy ? '保存中…' : '完成配置'}
</button>
</div>
</div>
</main>
);
}
function SectionTitle({ icon, title }: { icon: React.ReactNode; title: string }) {
return <div className="flex items-center gap-1.5 text-[14px] font-semibold text-[var(--text)]">{icon}{title}</div>;
}
function CheckRow({ label, ok, detail }: { label: string; ok: boolean; detail: string }) {
return (
<div className="mt-3 flex items-center justify-between gap-3 text-[13px]">
<span className="text-[var(--text-2)]">{label}</span>
<span className="flex min-w-0 items-center gap-1.5 text-right text-[12px] text-[var(--text-3)]">
<CheckCircle2 size={13} className={ok ? 'text-[var(--accent)]' : 'text-[var(--text-3)]'} />
<span className="truncate">{detail}</span>
</span>
</div>
);
}