Files
hunjing/frontend/src/components/admin/VermilionAction.tsx
T
2026-08-21 16:00:13 +08:00

112 lines
3.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { safetyApi } from "@/lib/api";
import type { PendingApproval } from "@/types/api";
import {
PERMISSION_POLICY_LABEL,
RISK_LEVEL_LABEL,
} from "@/lib/constants/labels";
import { cn } from "@/lib/utils/cn";
type Phase = "pending" | "loading" | "approved" | "denied";
/**
* 朱批裁定卡 —— 司命台动作签名(铜镜鉴心 v2)。
* 待裁定项以朱漆描边浮前:左缘 3px 朱漆 + 朱漆辐射底晕 + 鎏金"朱批待定"印文,
* 右缘 RISK mono 印鉴;裁定后卡片转为低饱和留档态。动效克制,仅在交互瞬间触发。
*/
export function VermilionAction({ approval }: { approval: PendingApproval }) {
const [phase, setPhase] = useState<Phase>("pending");
const [error, setError] = useState<string | null>(null);
async function decide(action: "approve" | "deny") {
setPhase("loading");
setError(null);
try {
if (action === "approve") {
await safetyApi.approveConfirmation(approval.confirmation_id);
setPhase("approved");
} else {
await safetyApi.denyConfirmation(approval.confirmation_id);
setPhase("denied");
}
} catch (e) {
setError(e instanceof Error ? e.message : "裁定失败,请重试。");
setPhase("pending");
}
}
const decided = phase === "approved" || phase === "denied";
return (
<div
className={cn(
"rounded-xl p-4 backdrop-blur-sm transition-opacity",
decided
? "border border-border-glass bg-glass opacity-60"
: "vermilion",
)}
>
<div className="flex flex-wrap items-center gap-2">
{!decided && (
<span className="font-serif text-[13px] font-bold tracking-wide text-cinnabar">
朱批待定
</span>
)}
<span className="font-serif text-sm font-semibold text-fg">
{approval.tool}
</span>
<span className="text-[10px] text-muted">
{PERMISSION_POLICY_LABEL[approval.permission_policy]}
</span>
<span className="ml-auto inline-flex items-center rounded border border-cinnabar/40 px-1.5 py-0.5 font-mono text-[9.5px] tracking-wide text-cinnabar">
{RISK_LEVEL_LABEL[approval.risk_level]}
</span>
</div>
<p className="mt-2 text-[13.5px] leading-relaxed text-fg">
{approval.action_summary}
</p>
{approval.possible_impact && (
<p className="mt-1 text-xs text-muted">
影响:{approval.possible_impact}
</p>
)}
{approval.reason && (
<p className="mt-1 text-xs text-muted">{approval.reason}</p>
)}
{error && <p className="mt-2 text-xs text-cinnabar">{error}</p>}
{phase === "approved" && (
<p className="mt-3 text-xs font-medium text-patina">已朱批放行</p>
)}
{phase === "denied" && (
<p className="mt-3 text-xs font-medium text-muted">已驳回</p>
)}
{phase === "pending" || phase === "loading" ? (
<div className="mt-3 flex gap-2">
<button
type="button"
onClick={() => decide("approve")}
disabled={phase === "loading"}
className="rounded-lg border border-cinnabar/40 bg-cinnabar/15 px-3 py-1.5 text-xs font-medium text-cinnabar transition-all hover:scale-[1.02] hover:bg-cinnabar/25 active:scale-95 disabled:opacity-50"
>
朱批放行
</button>
<button
type="button"
onClick={() => decide("deny")}
disabled={phase === "loading"}
className="rounded-lg border border-border-glass bg-glass px-3 py-1.5 text-xs font-medium text-muted transition-all hover:scale-[1.02] hover:bg-glass-strong hover:text-fg active:scale-95 disabled:opacity-50"
>
驳回
</button>
</div>
) : null}
</div>
);
}