Files
jiang13-bbs/frontend/app/admin/settings/EconomySettings.tsx
freefire 5270fee2b7 feat: 评论/帖子编辑历史与编辑标记,内容锁定
- 新增 smartDiff 差异计算与 EditHistoryModal 编辑历史弹窗,替换原 CommentEditHistoryModal
- 新增 PostEditedMark 编辑标记展示
- 新增内容锁定机制(content_lock)防止并发编辑冲突
- 审核与通知服务适配
2026-09-25 23:41:45 +08:00

238 lines
8.2 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 { 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>
);
}