104 lines
5.2 KiB
TypeScript
104 lines
5.2 KiB
TypeScript
import { useCallback, useEffect, useState, type FormEvent } from 'react';
|
||
import { getMindSpaceAdminConfig, getRybbitSsoUrl, getUmamiSsoUrl, updateMindSpaceAdminConfig } from '../../api/client';
|
||
|
||
const initial = {
|
||
enabled: false,
|
||
websiteId: '',
|
||
analyticsUrl: 'http://127.0.0.1:3100',
|
||
domains: '127.0.0.1,localhost',
|
||
idSecretConfigured: false,
|
||
};
|
||
|
||
export function AnalyticsConfigPage() {
|
||
const [config, setConfig] = useState(initial);
|
||
const [secret, setSecret] = useState('');
|
||
const [busy, setBusy] = useState(false);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [message, setMessage] = useState<string | null>(null);
|
||
const [openingUmami, setOpeningUmami] = useState(false);
|
||
const [openingRybbit, setOpeningRybbit] = useState(false);
|
||
|
||
const load = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const result = await getMindSpaceAdminConfig();
|
||
setConfig({ ...initial, ...result.analytics });
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '加载 Analytics 配置失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => { void load(); }, [load]);
|
||
|
||
const save = async (event: FormEvent) => {
|
||
event.preventDefault();
|
||
setBusy(true);
|
||
setError(null);
|
||
setMessage(null);
|
||
try {
|
||
const result = await updateMindSpaceAdminConfig({
|
||
analytics: { enabled: config.enabled, websiteId: config.websiteId, analyticsUrl: config.analyticsUrl, domains: config.domains, ...(secret.trim() ? { idSecret: secret.trim() } : {}) },
|
||
});
|
||
setConfig({ ...initial, ...result.analytics });
|
||
setSecret('');
|
||
setMessage('Analytics 配置已保存;密钥不会在后台回显。');
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '保存 Analytics 配置失败');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
const openUmami = async () => {
|
||
setOpeningUmami(true);
|
||
setError(null);
|
||
try {
|
||
window.location.assign(await getUmamiSsoUrl());
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '无法打开 Umami');
|
||
setOpeningUmami(false);
|
||
}
|
||
};
|
||
|
||
const openRybbit = async () => {
|
||
setOpeningRybbit(true);
|
||
setError(null);
|
||
try {
|
||
window.location.assign(await getRybbitSsoUrl());
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : '无法打开 Rybbit');
|
||
setOpeningRybbit(false);
|
||
}
|
||
};
|
||
|
||
return <div className="admin-page">
|
||
<div className="admin-page-head"><h2>Analytics 配置</h2><p className="muted">进入统计后台,或配置 Memind 生成页面的 Umami 统计。</p></div>
|
||
{error && <p className="banner banner-error">{error}</p>}
|
||
{message && <p className="banner banner-info">{message}</p>}
|
||
<section className="admin-card">
|
||
<h2>Rybbit 行为分析</h2>
|
||
<p className="muted">查看页面访问、用户会话、停留时间、点击和会话回放等行为明细。</p>
|
||
<div className="admin-actions">
|
||
<button type="button" className="send-btn" onClick={() => void openRybbit()} disabled={openingRybbit}>
|
||
{openingRybbit ? '正在进入 Rybbit...' : '打开 Rybbit 分析后台'}
|
||
</button>
|
||
</div>
|
||
</section>
|
||
<section className="admin-card">
|
||
<h2>Umami</h2>
|
||
<div className="admin-actions" style={{ marginBottom: 16 }}><button type="button" className="send-btn" onClick={() => void openUmami()} disabled={openingUmami || loading}>{openingUmami ? '正在进入 Umami...' : '打开 Umami 分析后台'}</button></div>
|
||
<form className="admin-form" onSubmit={save}>
|
||
<label className="admin-form-row"><span>启用页面统计</span><input type="checkbox" checked={config.enabled} disabled={loading || busy} onChange={(e) => setConfig({ ...config, enabled: e.target.checked })} /></label>
|
||
<label className="admin-form-row"><span>Website ID</span><input value={config.websiteId} disabled={loading || busy} onChange={(e) => setConfig({ ...config, websiteId: e.target.value })} placeholder="Umami Website ID" required={config.enabled} /></label>
|
||
<label className="admin-form-row"><span>Umami 地址</span><input value={config.analyticsUrl} disabled={loading || busy} onChange={(e) => setConfig({ ...config, analyticsUrl: e.target.value })} placeholder="http://127.0.0.1:3100" /></label>
|
||
<label className="admin-form-row"><span>统计域名</span><input value={config.domains} disabled={loading || busy} onChange={(e) => setConfig({ ...config, domains: e.target.value })} placeholder="127.0.0.1,localhost" /></label>
|
||
<label className="admin-form-row"><span>匿名化密钥 {config.idSecretConfigured ? '(已配置,留空保持不变)' : ''}</span><input type="password" value={secret} disabled={loading || busy} onChange={(e) => setSecret(e.target.value)} placeholder={config.idSecretConfigured ? '留空保持当前密钥' : '输入本地随机密钥'} autoComplete="new-password" /></label>
|
||
<div className="admin-actions"><button type="submit" className="send-btn" disabled={busy || loading}>{busy ? '保存中...' : '保存配置'}</button><button type="button" className="ghost-btn" onClick={() => void load()} disabled={busy}>重新加载</button></div>
|
||
</form>
|
||
</section>
|
||
</div>;
|
||
}
|