- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal - 新增 PostEditedMark 编辑标记展示 - 新增内容锁定机制(content_lock)防止并发编辑冲突 - 审核与通知服务适配
238 lines
8.2 KiB
TypeScript
238 lines
8.2 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import { CalendarCheck, Flame, MessageSquarePlus, PenLine, Star } from "lucide-react";
|
||
import { AdminSettingsGroup, AdminSettingsRow, AdminStepper } from "@/components/admin";
|
||
import ConfirmDialog from "@/components/ConfirmDialog";
|
||
import useUnsavedGuard from "@/hooks/useUnsavedGuard";
|
||
import { apiUpdateSiteSettings, type PublicSettings } from "@/lib/api";
|
||
import { toast } from "@/lib/toast";
|
||
|
||
/** 积分规则 8 键(0–100,后端按 0–100 裁剪) */
|
||
type PointsKeys = Pick<
|
||
PublicSettings,
|
||
| "points_checkin_base"
|
||
| "points_streak_every_days"
|
||
| "points_streak_bonus"
|
||
| "points_post_reward"
|
||
| "points_post_daily_cap"
|
||
| "points_reply_reward"
|
||
| "points_reply_daily_cap"
|
||
| "points_recommend_reward"
|
||
>;
|
||
|
||
const POINTS_KEYS: (keyof PointsKeys)[] = [
|
||
"points_checkin_base",
|
||
"points_streak_every_days",
|
||
"points_streak_bonus",
|
||
"points_post_reward",
|
||
"points_post_daily_cap",
|
||
"points_reply_reward",
|
||
"points_reply_daily_cap",
|
||
"points_recommend_reward",
|
||
];
|
||
|
||
/** 管理员可配置的积分增长规则:签到基础分 / 连续签到加成 / 发帖奖励 / 回复奖励(各带每日上限)/ 推荐奖励 */
|
||
export default function EconomySettings({ initial }: { initial: PublicSettings }) {
|
||
const pickInitial = (): PointsKeys =>
|
||
Object.fromEntries(POINTS_KEYS.map((k) => [k, initial[k]])) as PointsKeys;
|
||
|
||
const [form, setForm] = useState<PointsKeys>(pickInitial);
|
||
const [saved, setSaved] = useState<PointsKeys>(pickInitial);
|
||
const [saving, setSaving] = useState(false);
|
||
const dirty = useMemo(() => POINTS_KEYS.some((k) => form[k] !== saved[k]), [form, saved]);
|
||
const leave = useUnsavedGuard(dirty);
|
||
|
||
const set = (k: keyof PointsKeys, v: number) => setForm((f) => ({ ...f, [k]: v }));
|
||
|
||
const save = async () => {
|
||
if (saving || !dirty) return;
|
||
setSaving(true);
|
||
try {
|
||
const payload = Object.fromEntries(POINTS_KEYS.map((k) => [k, form[k]]));
|
||
const res = await apiUpdateSiteSettings(payload);
|
||
const next = Object.fromEntries(POINTS_KEYS.map((k) => [k, res[k]])) as PointsKeys;
|
||
setForm(next);
|
||
setSaved(next);
|
||
toast("积分规则已保存", "ok");
|
||
} catch (e) {
|
||
toast(e instanceof Error ? e.message : "保存失败");
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="admin-settings-stack">
|
||
<AdminSettingsGroup title="签到">
|
||
<AdminSettingsRow
|
||
label={
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<CalendarCheck size={15} /> 签到基础分
|
||
</span>
|
||
}
|
||
hintBelow="每日首次签到固定获得的积分(0=关闭,上限 100)。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_checkin_base}
|
||
onChange={(v) => set("points_checkin_base", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="分"
|
||
ariaLabel="签到基础分"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
<AdminSettingsRow
|
||
label={
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<Flame size={15} /> 连续签到触发天数
|
||
</span>
|
||
}
|
||
hintBelow="连续签到每满 N 天额外发放一次加成(0=关闭,上限 100)。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_streak_every_days}
|
||
onChange={(v) => set("points_streak_every_days", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="天"
|
||
ariaLabel="连续签到触发天数"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
<AdminSettingsRow
|
||
label={
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<Flame size={15} /> 连续签到加成
|
||
</span>
|
||
}
|
||
hintBelow="触发加成时额外获得的积分(0=关闭,上限 100);与触发天数配合使用。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_streak_bonus}
|
||
onChange={(v) => set("points_streak_bonus", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="分"
|
||
ariaLabel="连续签到加成分值"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
</AdminSettingsGroup>
|
||
|
||
<AdminSettingsGroup title="发帖与回复">
|
||
<AdminSettingsRow
|
||
label={
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<PenLine size={15} /> 发帖奖励
|
||
</span>
|
||
}
|
||
hintBelow="帖子发布成功后发放(0=关闭,上限 100);仅公开可见的帖子计奖,删除不回追。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_post_reward}
|
||
onChange={(v) => set("points_post_reward", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="分"
|
||
ariaLabel="发帖奖励分值"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
<AdminSettingsRow
|
||
label={<span className="inline-flex items-center gap-1.5">发帖奖励每日上限</span>}
|
||
hintBelow="每人每天最多计奖的帖子数(0=不限,上限 100),防止刷分。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_post_daily_cap}
|
||
onChange={(v) => set("points_post_daily_cap", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="帖"
|
||
ariaLabel="发帖奖励每日上限"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
<AdminSettingsRow
|
||
label={
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<MessageSquarePlus size={15} /> 回复奖励
|
||
</span>
|
||
}
|
||
hintBelow="评论发布成功后发放(0=关闭,上限 100);仅公开可见的评论计奖。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_reply_reward}
|
||
onChange={(v) => set("points_reply_reward", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="分"
|
||
ariaLabel="回复奖励分值"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
<AdminSettingsRow
|
||
label={<span className="inline-flex items-center gap-1.5">回复奖励每日上限</span>}
|
||
hintBelow="每人每天最多计奖的评论数(0=不限,上限 100),防止刷分。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_reply_daily_cap}
|
||
onChange={(v) => set("points_reply_daily_cap", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="条"
|
||
ariaLabel="回复奖励每日上限"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
</AdminSettingsGroup>
|
||
|
||
<AdminSettingsGroup title="帖子推荐">
|
||
<AdminSettingsRow
|
||
label={
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<Star size={15} /> 推荐奖励
|
||
</span>
|
||
}
|
||
hintBelow="帖子被管理员推荐(加精)时,作者一次性获得该积分(0=关闭,上限 100);推荐人为作者本人时不发放,同一帖子只发放一次。"
|
||
>
|
||
<AdminStepper
|
||
value={form.points_recommend_reward}
|
||
onChange={(v) => set("points_recommend_reward", v)}
|
||
min={0}
|
||
max={100}
|
||
unit="分"
|
||
ariaLabel="推荐奖励分值"
|
||
disabled={saving}
|
||
/>
|
||
</AdminSettingsRow>
|
||
</AdminSettingsGroup>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<button
|
||
type="button"
|
||
className="btn btn-primary btn-sm"
|
||
disabled={saving || !dirty}
|
||
onClick={() => void save()}
|
||
>
|
||
{saving ? "保存中…" : "保存积分规则"}
|
||
</button>
|
||
<span className="admin-settings-instant">
|
||
{dirty ? "有未保存的修改" : "已保存的规则对后续签到/发帖/回复/推荐生效"}
|
||
</span>
|
||
</div>
|
||
|
||
<ConfirmDialog
|
||
open={leave.leaveOpen}
|
||
title="有未保存的修改"
|
||
message="离开后,当前尚未保存的积分规则修改会丢失。确定离开?"
|
||
confirmLabel="离开"
|
||
danger
|
||
onCancel={leave.cancelLeave}
|
||
onConfirm={leave.confirmLeave}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|