- root deno.json workspace (nodeModulesDir auto), npm versions registered once in imports, deno.lock; remove pnpm-lock/pnpm-workspace/root package.json; members keep minimal package.json (name/type/exports) - tasks: dev, typecheck (tsc 7.0.2 per member + deno check), lint/fmt (biome, useImportExtensions), test (vitest via deno), check:tokens, build (web + deno compile), ci, db:migrate/reset, gen:types - tsconfig base moved to config/ (no root tsconfig.json); deno types -> types/deno.d.ts; web drops baseUrl - server: Deno.serve entry with graceful shutdown, --role migrate|preflight|probe, WEB_ROOT static SPA hosting; drop @hono/node-server, tsx, esbuild build.mjs - logger: redact child() bindings and serialize err by message/stack; sync stdout destination; tests - relative imports get explicit extensions (sites/mp-sync paths left for follow-up) - crash-recovery tests spawn `deno run -A`; tasks tests use unique per-file schemas - single-container Dockerfile + entrypoint (migrate -> preflight -> api+worker watchdog), docker-compose.yml - docs: README, runbook, ADR-001 superseded, plan §1 validation row Co-Authored-By: Claude Fable 5.1 <[email protected]>
81 lines
2.2 KiB
TypeScript
81 lines
2.2 KiB
TypeScript
import { KeyRound } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { toast } from "sonner";
|
|
import { Button } from "~/components/ui/button.tsx";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "~/components/ui/dialog.tsx";
|
|
import { Input } from "~/components/ui/input.tsx";
|
|
import { clearAuth, getToken, setToken } from "~/lib/api.ts";
|
|
|
|
/** 设置本机使用的 API Key(存 localStorage,首版单管理员) */
|
|
export function ApiKeyDialog() {
|
|
const [open, setOpen] = useState(false);
|
|
const [value, setValue] = useState("");
|
|
const hasKey = typeof window !== "undefined" && !!getToken();
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(o) => {
|
|
setOpen(o);
|
|
if (o) setValue("");
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant="outline"
|
|
size="icon-sm"
|
|
aria-label="设置 API Key"
|
|
title={hasKey ? "已设置 API Key" : "设置 API Key"}
|
|
>
|
|
<KeyRound className="h-4 w-4" />
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>API Key</DialogTitle>
|
|
<DialogDescription>控制台用它访问服务端接口,只保存在本浏览器。</DialogDescription>
|
|
</DialogHeader>
|
|
<Input
|
|
type="password"
|
|
autoComplete="off"
|
|
placeholder={hasKey ? "已设置,输入新值以替换" : "粘贴 API Key"}
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
/>
|
|
<DialogFooter>
|
|
{hasKey ? (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => {
|
|
clearAuth();
|
|
setOpen(false);
|
|
toast.success("已清除 API Key");
|
|
}}
|
|
>
|
|
清除
|
|
</Button>
|
|
) : null}
|
|
<Button
|
|
disabled={!value.trim()}
|
|
onClick={() => {
|
|
setToken(value.trim());
|
|
setOpen(false);
|
|
toast.success("已保存 API Key");
|
|
}}
|
|
>
|
|
保存
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|