179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
import type { FormEvent, ReactNode } from 'react';
|
|
import { CreditCard, KeyRound, ListChecks, UserRound } from 'lucide-react';
|
|
import type { ConsoleData } from '../app-state';
|
|
import { EntityTable } from '../components/EntityTable';
|
|
import { PageHeader } from '../components/PageHeader';
|
|
import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Input, Label, Tabs } from '../components/ui';
|
|
import type { ApiKeyForm, LoadState, WorkspaceSection } from '../types';
|
|
|
|
const tabs = [
|
|
{ value: 'overview', label: '个人总览', icon: <UserRound size={15} /> },
|
|
{ value: 'billing', label: '余额充值', icon: <CreditCard size={15} /> },
|
|
{ value: 'apiKeys', label: 'API Key', icon: <KeyRound size={15} /> },
|
|
{ value: 'tasks', label: '任务记录', icon: <ListChecks size={15} /> },
|
|
] satisfies Array<{ value: WorkspaceSection; label: string; icon: ReactNode }>;
|
|
|
|
export function WorkspacePage(props: {
|
|
apiKeyForm: ApiKeyForm;
|
|
apiKeySecret: string;
|
|
data: ConsoleData;
|
|
section: WorkspaceSection;
|
|
state: LoadState;
|
|
onApiKeyFormChange: (value: ApiKeyForm) => void;
|
|
onSectionChange: (value: WorkspaceSection) => void;
|
|
onSubmitApiKey: (event: FormEvent<HTMLFormElement>) => void;
|
|
}) {
|
|
return (
|
|
<div className="pageStack">
|
|
<PageHeader eyebrow="Workspace" title="用户工作台" description="个人资产、API Key 和任务记录。" />
|
|
<div className="subPageLayout">
|
|
<Tabs className="sideTabs" value={props.section} tabs={tabs} onValueChange={props.onSectionChange} />
|
|
<div className="subPageContent">
|
|
{props.section === 'overview' && <WorkspaceOverview data={props.data} />}
|
|
{props.section === 'billing' && <BillingPanel />}
|
|
{props.section === 'apiKeys' && <ApiKeyPanel {...props} />}
|
|
{props.section === 'tasks' && <TaskPanel data={props.data} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function WorkspaceOverview(props: { data: ConsoleData }) {
|
|
const owner = props.data.users[0];
|
|
return (
|
|
<section className="contentGrid two">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>个人中心总览</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="profileGrid">
|
|
<InfoItem label="账号" value={owner?.username ?? '-'} />
|
|
<InfoItem label="租户" value={owner?.tenantKey ?? 'default'} />
|
|
<InfoItem label="身份源" value={owner?.source ?? 'gateway'} />
|
|
<InfoItem label="API Key" value={String(props.data.apiKeys.length)} />
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>用户组策略</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<EntityTable
|
|
columns={['用户组', '优先级', '状态', '来源']}
|
|
empty="暂无用户组"
|
|
rows={props.data.userGroups.map((item) => [item.groupKey, item.priority, item.status, item.source])}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function BillingPanel() {
|
|
return (
|
|
<section className="contentGrid two">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>余额</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="balanceCard">
|
|
<span>resource</span>
|
|
<strong>0.00</strong>
|
|
<Badge variant="secondary">local</Badge>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>充值</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="formGrid">
|
|
<Label>
|
|
金额
|
|
<Input value="100" readOnly />
|
|
</Label>
|
|
<Button type="button" variant="secondary">创建充值订单</Button>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function ApiKeyPanel(props: {
|
|
apiKeyForm: ApiKeyForm;
|
|
apiKeySecret: string;
|
|
data: ConsoleData;
|
|
state: LoadState;
|
|
onApiKeyFormChange: (value: ApiKeyForm) => void;
|
|
onSubmitApiKey: (event: FormEvent<HTMLFormElement>) => void;
|
|
}) {
|
|
return (
|
|
<section className="contentGrid two">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>创建 API Key</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form className="formGrid" onSubmit={props.onSubmitApiKey}>
|
|
<Label>
|
|
名称
|
|
<Input value={props.apiKeyForm.name} onChange={(event) => props.onApiKeyFormChange({ name: event.target.value })} />
|
|
</Label>
|
|
<Button type="submit" disabled={props.state === 'loading'}>
|
|
<KeyRound size={15} />
|
|
创建
|
|
</Button>
|
|
{props.apiKeySecret && <code className="secretBox">{props.apiKeySecret}</code>}
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>API Key 列表</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<EntityTable
|
|
columns={['名称', '前缀', '状态', '创建时间']}
|
|
empty="暂无 API Key"
|
|
rows={props.data.apiKeys.map((item) => [item.name, item.keyPrefix, item.status, new Date(item.createdAt).toLocaleString()])}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function TaskPanel(props: { data: ConsoleData }) {
|
|
const task = props.data.taskResult;
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>任务记录</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{task ? (
|
|
<div className="taskPreview">
|
|
<Badge variant={task.status === 'succeeded' ? 'success' : 'secondary'}>{task.status}</Badge>
|
|
<strong>{task.kind}</strong>
|
|
<span>{task.model}</span>
|
|
<pre>{JSON.stringify(task.result ?? {}, null, 2)}</pre>
|
|
</div>
|
|
) : (
|
|
<div className="emptyState">
|
|
<strong>暂无任务</strong>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
function InfoItem(props: { label: string; value: string }) {
|
|
return (
|
|
<div className="infoItem">
|
|
<span>{props.label}</span>
|
|
<strong>{props.value}</strong>
|
|
</div>
|
|
);
|
|
}
|