feat: redeem items support optional duration (permanent/timed)

- Add duration_minutes column to redeem_items (0 = permanent)
- Backend: update RedeemItem/RedeemItemWrite structs, all CRUD SQL
- Backend: EnsureColumn migration for existing databases
- Frontend: add duration selector dropdown (永久/15min/30min/1h/2h/3h/custom)
- Frontend: show ♾️ Permanent or ⏱️ duration in item list

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
这个提交包含在:
cryptocommuniums-afk
2026-02-16 18:38:56 +08:00
父节点 9772ea6764
当前提交 bd300ac597
修改 5 个文件,包含 68 行新增20 行删除

查看文件

@@ -14,6 +14,7 @@ type RedeemItem = {
unit_label: string;
holiday_cost: number;
studyday_cost: number;
duration_minutes: number;
is_active: boolean;
is_global: boolean;
created_at: number;
@@ -40,6 +41,7 @@ type ItemForm = {
unit_label: string;
holiday_cost: number;
studyday_cost: number;
duration_minutes: number;
is_active: boolean;
is_global: boolean;
};
@@ -50,6 +52,7 @@ const DEFAULT_FORM: ItemForm = {
unit_label: "hour",
holiday_cost: 5,
studyday_cost: 25,
duration_minutes: 0,
is_active: true,
is_global: true,
};
@@ -148,6 +151,7 @@ export default function AdminRedeemPage() {
unit_label: item.unit_label,
holiday_cost: item.holiday_cost,
studyday_cost: item.studyday_cost,
duration_minutes: item.duration_minutes,
is_active: item.is_active,
is_global: item.is_global,
});
@@ -217,6 +221,34 @@ export default function AdminRedeemPage() {
setForm((prev) => ({ ...prev, studyday_cost: Math.max(0, Number(e.target.value) || 0) }))
}
/>
<div className="flex items-center gap-2">
<select
className="rounded border px-3 py-2 text-sm flex-1"
value={form.duration_minutes === 0 ? "0" : String(form.duration_minutes)}
onChange={(e) => setForm((prev) => ({ ...prev, duration_minutes: Number(e.target.value) }))}
>
<option value="0">{tx("永久有效 ♾️", "Permanent ♾️")}</option>
<option value="15">15 {tx("分钟", "min")}</option>
<option value="30">30 {tx("分钟", "min")}</option>
<option value="60">1 {tx("小时", "hour")}</option>
<option value="90">1.5 {tx("小时", "hours")}</option>
<option value="120">2 {tx("小时", "hours")}</option>
<option value="180">3 {tx("小时", "hours")}</option>
<option value="-1">{tx("自定义", "Custom")}</option>
</select>
{form.duration_minutes === -1 && (
<input
className="rounded border px-3 py-2 text-sm w-24"
type="number"
min={1}
placeholder={tx("分钟", "min")}
onChange={(e) => {
const v = Math.max(1, Number(e.target.value) || 1);
setForm((prev) => ({ ...prev, duration_minutes: v }));
}}
/>
)}
</div>
<textarea
className="rounded border px-3 py-2 text-sm md:col-span-2"
placeholder={tx("描述", "Description")}
@@ -291,6 +323,12 @@ export default function AdminRedeemPage() {
<p>
#{item.id} · {item.name} · {tx("假期", "Holiday")} {item.holiday_cost}/{item.unit_label} · {tx("学习日", "Study Day")} {item.studyday_cost}/
{item.unit_label}
{" · "}
{item.duration_minutes > 0
? (item.duration_minutes >= 60
? `⏱️ ${item.duration_minutes / 60}${tx("小时", "h")}`
: `⏱️ ${item.duration_minutes}${tx("分钟", "min")}`)
: `♾️ ${tx("永久", "Permanent")}`}
</p>
<p className="text-xs text-zinc-600">
{tx("状态:", "Status: ")}