Files
lislgosms/src/components/ui/DeleteRiskAction.tsx
T

87 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, type ReactNode } from 'react';
import { AlertTriangle, ShieldCheck, Trash2 } from 'lucide-react';
import { adminApi, clientApi, type DeletionPreflight, type DeletionResult, type DeletionTargetType } from '@/api/adminApi';
import { createUuid } from '@/utils/randomId';
import { Button } from './Button';
import { Modal } from './Modal';
import { Textarea } from './Textarea';
export function DeleteRiskAction({ portal, targetType, targetId, children = '删除', icon = <Trash2 size={15} />, disabled, onCompleted }: {
portal: 'admin' | 'client';
targetType: DeletionTargetType;
targetId: string;
children?: ReactNode;
icon?: ReactNode;
disabled?: boolean;
onCompleted?: (result: DeletionResult) => void;
}) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [preflight, setPreflight] = useState<DeletionPreflight | null>(null);
const [result, setResult] = useState<DeletionResult | null>(null);
const [reason, setReason] = useState('');
const [error, setError] = useState('');
const [idempotencyKey, setIdempotencyKey] = useState('');
async function begin() {
const key = `delete:${targetType}:${targetId}:${createUuid()}`;
setOpen(true); setLoading(true); setPreflight(null); setResult(null); setReason(''); setError(''); setIdempotencyKey(key);
try {
const data = portal === 'admin'
? await adminApi.getDeletionPreflight(targetType, targetId)
: await clientApi.getDeletionPreflight(targetType as Exclude<DeletionTargetType, 'channel'>, targetId);
setPreflight(data);
} catch (failure) {
setError(failure instanceof Error ? failure.message : '删除资格预检失败');
} finally { setLoading(false); }
}
async function confirm() {
if (!preflight?.allowedActions.includes('delete') || reason.trim().length < 4) return;
setSubmitting(true); setError('');
try {
const body = { expectedUpdatedAt: preflight.expectedUpdatedAt, idempotencyKey, reason: reason.trim() };
const completed = portal === 'admin'
? await adminApi.deleteGovernedTarget(targetType, targetId, body)
: await clientApi.deleteGovernedTarget(targetType as Exclude<DeletionTargetType, 'channel'>, targetId, body);
setResult(completed); onCompleted?.(completed);
} catch (failure) {
setError(failure instanceof Error ? failure.message : '删除失败,请重新检查依赖后重试');
} finally { setSubmitting(false); }
}
function close() { if (!submitting) setOpen(false); }
const blocked = Boolean(preflight && !preflight.allowedActions.includes('delete'));
const footer = (requestClose: () => void) => result ? <Button onClick={close}>关闭</Button> : <>
<Button disabled={submitting} onClick={requestClose} variant="ghost">取消</Button>
<Button disabled={loading || submitting || blocked || !preflight || reason.trim().length < 4} icon={<Trash2 size={15} />} onClick={() => void confirm()} variant="danger">
{submitting ? '删除处理中…' : '确认删除'}
</Button>
</>;
return <>
<Button disabled={disabled} icon={icon} onClick={() => void begin()} size="sm" variant="danger">{children}</Button>
<Modal dirty={!result && reason.trim().length > 0} footer={({ requestClose }) => footer(requestClose)} onClose={close} open={open} title="删除资格与影响确认">
<div className="risk-action-content delete-risk-action">
{loading ? <p role="status">正在从后台检查引用关系与当前状态…</p> : null}
{preflight ? <>
<div className="risk-action-identity">
{Object.entries(preflight.identity).map(([key, value]) => <div key={key}><span>{identityLabels[key] ?? key}</span><strong>{value}</strong></div>)}
</div>
<section><h3>依赖与资格检查</h3>{preflight.dependencies.length ? <ul className="delete-risk-dependencies">{preflight.dependencies.map((item) => <li key={item.kind}><strong>{item.label}</strong><span>{item.count} </span>{item.items.length ? <small>{item.items.join('')}</small> : <small>无活动引用</small>}</li>)}</ul> : null}</section>
{preflight.blockedReasons.length
? <ul className="risk-action-blockers">{preflight.blockedReasons.map((item) => <li key={item}><AlertTriangle size={15} />{item}</li>)}</ul>
: <p className="risk-action-passed"><ShieldCheck size={16} />资格检查通过,可以执行逻辑删除</p>}
<section><h3>影响范围</h3><ul>{preflight.impacts.map((item) => <li key={item}>{item}</li>)}</ul><p className="muted">{preflight.recoverability.description}</p></section>
{!blocked ? <Textarea label="删除原因" onChange={(event) => setReason(event.target.value)} placeholder="至少填写 4 个字符,原因将写入审计记录" required value={reason} /> : null}
</> : null}
{result ? <div className="risk-action-result" role="status"><ShieldCheck size={20} /><div><strong>删除已完成</strong><span>操作单号:{result.operationId}{result.replayed ? '(幂等重放)' : ''}</span></div></div> : null}
{error ? <p className="form-error" role="alert">{error}</p> : null}
</div>
</Modal>
</>;
}
const identityLabels: Record<string, string> = { name: '对象名称', id: '唯一标识', code: '通道编码', tenant: '所属企业', application: '短信应用', signature: '关联签名' };