93 lines
3.6 KiB
TypeScript
93 lines
3.6 KiB
TypeScript
import { useState } from 'react';
|
||
import { Copy, Plus, RefreshCw } from 'lucide-react';
|
||
import { api, errorText } from '../api';
|
||
import type { School } from '../types';
|
||
|
||
export function Schools({ schools, onChanged, onMessage }: {
|
||
schools: School[];
|
||
onChanged: () => void;
|
||
onMessage: (text: string) => void;
|
||
}) {
|
||
const [schoolId, setSchoolId] = useState('');
|
||
const [name, setName] = useState('');
|
||
const [issued, setIssued] = useState<{ school_id: string; token: string } | null>(null);
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
async function register(id: string, label: string): Promise<void> {
|
||
if (!id.trim()) return;
|
||
setBusy(true);
|
||
try {
|
||
const result = await api.post<{ school_id: string; token: string }>('/api/schools', {
|
||
school_id: id.trim(), name: label.trim(),
|
||
});
|
||
setIssued(result);
|
||
setSchoolId('');
|
||
setName('');
|
||
onChanged();
|
||
} catch (error) {
|
||
onMessage(errorText(error));
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<section className="panel">
|
||
<h2>学校与上报凭证</h2>
|
||
<p className="muted">
|
||
每所学校的桌面端用自己的凭证上报草稿、拉取已发布的经验。凭证只在生成时显示一次,丢了就重新生成。
|
||
</p>
|
||
|
||
<form className="school-form" onSubmit={event => { event.preventDefault(); void register(schoolId, name); }}>
|
||
<input value={schoolId} placeholder="学校标识,如 demo_school" onChange={event => setSchoolId(event.target.value)} />
|
||
<input value={name} placeholder="学校名称(可选)" onChange={event => setName(event.target.value)} />
|
||
<button type="submit" className="primary-button small" disabled={busy || !schoolId.trim()}>
|
||
<Plus size={14} />生成凭证
|
||
</button>
|
||
</form>
|
||
|
||
{issued && (
|
||
<div className="token-box">
|
||
<div className="token-head">
|
||
<strong>{issued.school_id} 的上报凭证</strong>
|
||
<button type="button" className="text-action" onClick={() => {
|
||
void navigator.clipboard.writeText(issued.token).then(() => onMessage('已复制到剪贴板'));
|
||
}}>
|
||
<Copy size={14} />复制
|
||
</button>
|
||
</div>
|
||
<code>{issued.token}</code>
|
||
<p className="muted">
|
||
写进那台机器的 .env:<code>HUB_URL=http://本机地址:8801</code> 和 <code>HUB_TOKEN=上面这串</code>,重启桌面端生效。
|
||
这串只显示这一次。
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<table className="school-table">
|
||
<thead>
|
||
<tr><th>学校</th><th>已报条目</th><th>最近上报</th><th>最近拉取</th><th /></tr>
|
||
</thead>
|
||
<tbody>
|
||
{schools.length === 0 && <tr><td colSpan={5} className="muted">还没有学校注册</td></tr>}
|
||
{schools.map(school => (
|
||
<tr key={school.id}>
|
||
<td><strong>{school.name || school.id}</strong><br /><span className="muted">{school.id}</span></td>
|
||
<td>{school.reported_entries}</td>
|
||
<td className="muted">{school.last_report_at ?? '—'}</td>
|
||
<td className="muted">{school.last_pull_at ?? '—'}</td>
|
||
<td>
|
||
<button type="button" className="text-action" disabled={busy}
|
||
onClick={() => { void register(school.id, school.name); }}
|
||
title="生成一串新凭证,旧的立即失效">
|
||
<RefreshCw size={14} />重置凭证
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
);
|
||
}
|