Files
jiang13-bbs/frontend/components/DeleteCommentModal.tsx

173 lines
5.1 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>
);
}