diff options
Diffstat (limited to 'frontend/src/pages/AdminPage.svelte')
| -rw-r--r-- | frontend/src/pages/AdminPage.svelte | 74 |
1 files changed, 39 insertions, 35 deletions
diff --git a/frontend/src/pages/AdminPage.svelte b/frontend/src/pages/AdminPage.svelte index d9bea83..2a10266 100644 --- a/frontend/src/pages/AdminPage.svelte +++ b/frontend/src/pages/AdminPage.svelte @@ -17,6 +17,7 @@ import { Alert } from '$lib/components/ui/alert'; import { Badge } from '$lib/components/ui/badge'; import { Label } from '$lib/components/ui/label'; + import * as Select from '$lib/components/ui/select'; import * as Tabs from '$lib/components/ui/tabs'; import * as Card from '$lib/components/ui/card'; import * as Table from '$lib/components/ui/table'; @@ -185,12 +186,12 @@ </Tabs.Root> {/snippet} - <div class="stack"> + <div class="flex flex-col gap-3"> <Status busy={false} error={adminOpsErr} empty={null} /> {#if tab === 'users'} - <Card.Root size="sm" class="stack-sec"> - <h2>用户</h2> + <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <h2 class="text-lg font-semibold">用户</h2> <Status busy={usersBusy} error={usersErr} busyText="载入用户…" empty={null} /> {#if !usersBusy && !usersErr && users.length > 0} <Table.Root> @@ -211,26 +212,29 @@ <Table.Cell>{u.email}</Table.Cell> <Table.Cell>{u.name}</Table.Cell> <Table.Cell> - <select + <Select.Root + type="single" value={u.role} - aria-label={`角色 ${u.email}`} - onchange={(ev) => void patchUser(u, { role: (ev.target as HTMLSelectElement).value as 'admin' | 'member' })} + onValueChange={(v) => void patchUser(u, { role: v as 'admin' | 'member' })} > - <option value="member" selected={u.role === 'member'}>成员</option> - <option value="admin" selected={u.role === 'admin'}>管理员</option> - </select> + <Select.Trigger data-testid="role-select" class="w-32">{u.role === 'admin' ? '管理员' : '成员'}</Select.Trigger> + <Select.Content> + <Select.Item value="member">成员</Select.Item> + <Select.Item value="admin">管理员</Select.Item> + </Select.Content> + </Select.Root> </Table.Cell> <Table.Cell> <Badge variant={u.active ? 'secondary' : 'destructive'}>{u.active ? '启用' : '停用'}</Badge> </Table.Cell> <Table.Cell> - <label class="checkline"> + <label class="inline-flex items-center gap-1"> <input type="checkbox" checked={u.ai_enabled ?? false} onchange={(e) => void patchUser(u, { ai_enabled: (e.target as HTMLInputElement).checked })} /> </label> </Table.Cell> <Table.Cell> <Input - class="num w-[90px]" + class="tabular-nums w-24" type="number" min="0" max="200" @@ -239,7 +243,7 @@ /> </Table.Cell> <Table.Cell> - <div class="row"> + <div class="flex items-center gap-2 flex-wrap"> <Button variant={u.active ? 'destructive' : 'outline'} size="sm" onclick={() => void patchUser(u, { active: !u.active })}> {u.active ? '停用' : '启用'} </Button> @@ -251,17 +255,17 @@ </Table.Body> </Table.Root> {/if} - <p class="hint">停用账户会立即吊销其会话并阻止其回测与 AI 请求。角色不能作用于他人项目,私阅策略代码不提供。</p> + <p class="text-sm text-muted-foreground">停用账户会立即吊销其会话并阻止其回测与 AI 请求。角色不能作用于他人项目,私阅策略代码不提供。</p> </Card.Root> {:else if tab === 'invites'} - <Card.Root size="sm" class="stack-sec"> - <h2>签发邀请</h2> - <form onsubmit={makeInvite} class="row"> - <div style="flex:1 1 220px"> + <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <h2 class="text-lg font-semibold">签发邀请</h2> + <form onsubmit={makeInvite} class="flex flex-wrap items-end gap-3"> + <div class="flex-[1_1_220px]"> <Label for="inv-em">邮箱(可选备注)</Label> <Input id="inv-em" bind:value={inviteEmail} placeholder="📬 将线下告知受邀人,用于区分" /> </div> - <div style="flex:0 0 130px"> + <div class="w-[130px]"> <Label for="inv-h">有效小时</Label> <Input id="inv-h" type="number" bind:value={inviteHours} min="1" max="720" /> </div> @@ -269,20 +273,20 @@ </form> <Status busy={false} error={inviteErr} empty={null} /> {#if newInvite} - <Alert variant="default" class="bg-[var(--brand-soft)] border-[#bfe9db]"> + <Alert variant="default"> <div> - <div class="cov-caption">邀请令牌(只显示这一次,请立即复制):</div> - <div class="mono-invite">{newInvite.token}</div> - <div class="hint">到期:{fmtDateTime(newInvite.expires_at)} · 单次使用 · 注册页链接:</div> - <div class="mono-invite" title="复制该完整链接给对方">{inviteUrl}</div> + <div class="text-sm">邀请令牌(只显示这一次,请立即复制):</div> + <div class="font-mono text-sm break-all">{newInvite.token}</div> + <div class="text-sm text-muted-foreground">到期:{fmtDateTime(newInvite.expires_at)} · 单次使用 · 注册页链接:</div> + <div class="font-mono text-sm break-all" title="复制该完整链接给对方">{inviteUrl}</div> <Button variant="outline" size="sm" class="mt-1" onclick={() => void copy(inviteUrl)}>{inviteUrl && inviteCopied ? '已复制' : '复制链接'}</Button> </div> </Alert> {/if} </Card.Root> - <Card.Root size="sm" class="stack-sec"> - <h2>历史邀请</h2> + <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <h2 class="text-lg font-semibold">历史邀请</h2> <Status busy={inviteBusy} error={inviteErr} busyText="载入邀请…" empty={null} /> {#if !inviteBusy && !inviteErr && invites.length === 0} <Status busy={false} empty="还没有邀请记录" /> @@ -323,11 +327,11 @@ </Table.Body> </Table.Root> {/if} - <p class="hint">恢复链接由“生成恢复”按钮产生,仅出现一次;不会自动发邮件。</p> + <p class="text-sm text-muted-foreground">恢复链接由“生成恢复”按钮产生,仅出现一次;不会自动发邮件。</p> </Card.Root> {:else} - <Card.Root size="sm" class="stack-sec"> - <h2>安全审计(收尾尾部)</h2> + <Card.Root size="sm" class="p-4 flex flex-col gap-3"> + <h2 class="text-lg font-semibold">安全审计(收尾尾部)</h2> <Status busy={auditBusy} error={auditErr} busyText="载入审计记录…" empty={null} /> {#if !auditBusy && !auditErr && audit.length === 0} <Status busy={false} empty="暂无审计事件" /> @@ -349,29 +353,29 @@ <Table.Cell>{fmtDateTime(e.time)}</Table.Cell> <Table.Cell>{e.actor ?? '—'}</Table.Cell> <Table.Cell>{e.action}</Table.Cell> - <Table.Cell class="num">{e.target ?? '—'}</Table.Cell> + <Table.Cell class="tabular-nums">{e.target ?? '—'}</Table.Cell> <Table.Cell><Badge variant={e.status === 'ok' || e.status === 'success' ? 'secondary' : 'destructive'}>{e.status}</Badge></Table.Cell> </Table.Row> {/each} </Table.Body> </Table.Root> {/if} - <p class="hint">审计仅记录操作行为与结果,不含密码/密钥/策略代码内容。</p> + <p class="text-sm text-muted-foreground">审计仅记录操作行为与结果,不含密码/密钥/策略代码内容。</p> </Card.Root> {/if} </div> {#if resetFor} <Modal title={`为 ${resetFor.email} 生成密码恢复`} onClose={() => (resetFor = null)}> - <div class="stack"> - <Alert variant="default" class="bg-[var(--brand-soft)] border-[#bfe9db]"> + <div class="flex flex-col gap-3"> + <Alert variant="default"> <div> <div>恢复链接(管理员线下告知对方,只出现一次):</div> - <div class="mono-invite">{resetUrl}</div> - <div class="hint">令牌 {resetToken.slice(0, 8)}… 会即刻失效前提示时间(见链接有效期)。</div> + <div class="font-mono text-sm break-all">{resetUrl}</div> + <div class="text-sm text-muted-foreground">令牌 {resetToken.slice(0, 8)}… 会即刻失效前提示时间(见链接有效期)。</div> </div> </Alert> - <p class="hint">单次有效、限时;重置会吊销对方的全部历史会话。</p> + <p class="text-sm text-muted-foreground">单次有效、限时;重置会吊销对方的全部历史会话。</p> </div> </Modal> {/if} |
