Files
jiang13-bbs/frontend/components/DeleteCommentModal.tsx
freefire c6fdbb7193 feat(comments): 软删占位、理由与恢复,管理删仅对访客打码
自删/管理删差异文案与理由可见性,评论者与管理员免马赛克,并支持恢复与处理人链接。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 07:43:48 +08:00

173 lines
5.0 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.
"use client";
import { useEffect, useState } from "react";
import Modal from "@/components/Modal";
import { POST_DELETE_TYPES } from "@/lib/api";
export type DeleteCommentPayload = {
delete_type: string;
delete_reason: string;
};
/**
* 评论软删弹窗:
* - 评论者自删:必填理由(仅管理可见)
* - 管理删:必选类型;仅「其他」须填理由
*/
export default function DeleteCommentModal({
open,
mode,
busy,
onClose,
onConfirm,
}: {
open: boolean;
/** self=评论者自删;staff=版主/后台删除 */
mode: "self" | "staff";
busy?: boolean;
onClose: () => void;
onConfirm: (opts: DeleteCommentPayload) => void;
}) {
const [deleteType, setDeleteType] = useState("spam");
const [reason, setReason] = useState("");
const [err, setErr] = useState("");
useEffect(() => {
if (open) {
setDeleteType("spam");
setReason("");
setErr("");
}
}, [open]);
if (!open) return null;
const isStaff = mode === "staff";
const reasonRequired = !isStaff || deleteType === "other";
const submit = () => {
const r = reason.trim();
if (reasonRequired) {
if (!r) {
setErr("请填写删除理由");
return;
}
if ([...r].length > 200) {
setErr("删除理由不能超过 200 字");
return;
}
}
onConfirm({
delete_type: isStaff ? deleteType : "",
delete_reason: reasonRequired ? r : "",
});
};
return (
<Modal
open
title={isStaff ? "删除评论" : "删除我的评论"}
description={
isStaff
? "评论区保留马赛克占位;读者可见删除类型,「其他」时还会展示补充理由。子回复不受影响。"
: "评论区保留马赛克占位,读者仅见「评论者已删除此评论」。你填写的理由仅管理员可见。"
}
onClose={onClose}
busy={busy}
maxWidthClass="max-w-[440px]"
panelClassName="p-5"
ariaLabel={isStaff ? "删除评论" : "删除我的评论"}
footer={
<>
<button type="button" className="btn btn-line" onClick={onClose} disabled={busy}>
取消
</button>
<button type="button" className="btn btn-danger" onClick={submit} disabled={busy}>
{busy ? "删除中…" : "确认删除"}
</button>
</>
}
>
{isStaff ? (
<>
<p className="mb-2 text-[12.5px] font-medium" style={{ color: "var(--ink)" }}>
删除类型
</p>
<div
className="grid grid-cols-1 sm:grid-cols-2 gap-1.5 mb-1"
role="radiogroup"
aria-label="删除类型"
>
{POST_DELETE_TYPES.map((t) => {
const active = deleteType === t.value;
return (
<button
key={t.value}
type="button"
role="radio"
aria-checked={active}
onClick={() => {
setDeleteType(t.value);
setErr("");
if (t.value !== "other") setReason("");
}}
className="text-left rounded-lg px-3 py-2.5 text-[13px] transition-colors"
style={{
border: `1.5px solid ${active ? "var(--accent)" : "var(--line)"}`,
background: active ? "var(--accent-soft)" : "var(--panel-2)",
color: active ? "var(--accent)" : "var(--ink)",
}}
>
{t.label}
</button>
);
})}
</div>
<p className="meta text-[11.5px] mb-4 leading-relaxed">
选「其他」时需补充理由;其余类型直接按标签展示给读者。
</p>
</>
) : null}
{reasonRequired ? (
<>
<label
className="block mb-1.5 text-[12.5px] font-medium"
style={{ color: "var(--ink)" }}
htmlFor="cmt-delete-reason"
>
{isStaff ? "补充理由" : "删除理由"}{" "}
<span style={{ color: "var(--danger)" }}>*</span>
</label>
<textarea
id="cmt-delete-reason"
className="field"
rows={3}
value={reason}
onChange={(e) => {
setReason(e.target.value);
setErr("");
}}
placeholder={
isStaff
? "向读者说明删除原因(将展示在占位中)"
: "例如:发错了 / 不想保留(仅管理员可见)"
}
maxLength={200}
autoFocus={!isStaff}
/>
<p className="meta text-[11.5px] mt-1.5">
{isStaff ? "将公开显示在评论占位旁。" : "普通读者看不到这条理由。"}
</p>
</>
) : null}
{err ? (
<p className="mt-2 text-[12px]" style={{ color: "var(--danger)" }}>
{err}
</p>
) : null}
</Modal>
);
}