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>
这个提交包含在:
@@ -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: ")}
|
||||
|
||||
在新工单中引用
屏蔽一个用户