feat: Minecraft theme overhaul, fix points bug, add history
这个提交包含在:
@@ -14,6 +14,10 @@ type Me = {
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
// Use a distinct style for inputs/selects
|
||||
const inputClass = "w-full bg-[color:var(--mc-surface)] text-[color:var(--mc-plank-light)] border-2 border-[color:var(--mc-stone-dark)] px-3 py-2 text-base focus:border-[color:var(--mc-gold)] focus:outline-none transition-colors font-minecraft";
|
||||
const labelClass = "text-sm text-[color:var(--mc-stone)] mb-1 block";
|
||||
|
||||
type RedeemItem = {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -62,12 +66,21 @@ function fmtTs(v: number | null | undefined): string {
|
||||
return new Date(v * 1000).toLocaleString();
|
||||
}
|
||||
|
||||
function resolveRank(rating: number): { label: string; color: string; icon: string } {
|
||||
if (rating >= 2000) return { label: "Netherite", color: "text-[color:var(--mc-red)]", icon: "🔥" };
|
||||
if (rating >= 1500) return { label: "Diamond", color: "text-[color:var(--mc-diamond)]", icon: "💎" };
|
||||
if (rating >= 1200) return { label: "Iron", color: "text-zinc-200", icon: "⚔️" };
|
||||
if (rating >= 1000) return { label: "Stone", color: "text-[color:var(--mc-stone)]", icon: "🪨" };
|
||||
return { label: "Wood", color: "text-[color:var(--mc-wood)]", icon: "🪵" };
|
||||
}
|
||||
|
||||
export default function MePage() {
|
||||
const { isZh, tx } = useI18nText();
|
||||
const [token, setToken] = useState("");
|
||||
const [profile, setProfile] = useState<Me | null>(null);
|
||||
const [items, setItems] = useState<RedeemItem[]>([]);
|
||||
const [records, setRecords] = useState<RedeemRecord[]>([]);
|
||||
const [historyItems, setHistoryItems] = useState<RatingHistoryItem[]>([]);
|
||||
const [dailyTasks, setDailyTasks] = useState<DailyTaskItem[]>([]);
|
||||
const [dailyDayKey, setDailyDayKey] = useState("");
|
||||
const [dailyTotalReward, setDailyTotalReward] = useState(0);
|
||||
@@ -99,34 +112,17 @@ export default function MePage() {
|
||||
if (isZh) return task.title;
|
||||
if (task.code === "login_checkin") return "Daily Sign-in";
|
||||
if (task.code === "daily_submit") return "Daily Submission";
|
||||
if (task.code === "first_ac") return "Solve One Problem";
|
||||
if (task.code === "code_quality") return "Code Quality";
|
||||
if (task.code === "first_ac") return "First Blood";
|
||||
if (task.code === "code_quality") return "Craftsman";
|
||||
return task.title;
|
||||
};
|
||||
|
||||
const taskDesc = (task: DailyTaskItem): string => {
|
||||
if (isZh) return task.description;
|
||||
if (task.code === "login_checkin") return "Sign in once today to get 1 point.";
|
||||
if (task.code === "daily_submit") return "Submit once today to get 1 point.";
|
||||
if (task.code === "first_ac") return "Get AC once today to get 1 point.";
|
||||
if (task.code === "code_quality") return "Submit code longer than 10 lines once today to get 1 point.";
|
||||
return task.description;
|
||||
};
|
||||
|
||||
const itemName = (name: string): string => {
|
||||
if (isZh) return name;
|
||||
if (name === "私人玩游戏时间") return "Private Game Time";
|
||||
if (name === "私人玩游戏时间") return "Game Time pass";
|
||||
return name;
|
||||
};
|
||||
|
||||
const itemDesc = (text: string): string => {
|
||||
if (isZh) return text;
|
||||
if (text === "全局用户可兑换:假期 1 小时 5 Rating;学习日/非节假日 1 小时 25 Rating。") {
|
||||
return "Global redeem item: holiday 1 hour = 5 rating; study day/non-holiday 1 hour = 25 rating.";
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const loadAll = async () => {
|
||||
setLoading(true);
|
||||
setError("");
|
||||
@@ -136,15 +132,17 @@ export default function MePage() {
|
||||
setToken(tk);
|
||||
if (!tk) throw new Error(tx("请先登录", "Please sign in first"));
|
||||
|
||||
const [me, redeemItems, redeemRecords, daily] = await Promise.all([
|
||||
const [me, redeemItems, redeemRecords, daily, history] = await Promise.all([
|
||||
apiFetch<Me>("/api/v1/me", {}, tk),
|
||||
apiFetch<RedeemItem[]>("/api/v1/me/redeem/items", {}, tk),
|
||||
apiFetch<RedeemRecord[]>("/api/v1/me/redeem/records?limit=200", {}, tk),
|
||||
apiFetch<DailyTaskPayload>("/api/v1/me/daily-tasks", {}, tk),
|
||||
listRatingHistory(50),
|
||||
]);
|
||||
setProfile(me);
|
||||
setItems(redeemItems ?? []);
|
||||
setRecords(redeemRecords ?? []);
|
||||
setHistoryItems(history ?? []);
|
||||
setDailyTasks(daily?.tasks ?? []);
|
||||
setDailyDayKey(daily?.day_key ?? "");
|
||||
setDailyTotalReward(daily?.total_reward ?? 0);
|
||||
@@ -171,9 +169,9 @@ export default function MePage() {
|
||||
setMsg("");
|
||||
try {
|
||||
if (!token) throw new Error(tx("请先登录", "Please sign in first"));
|
||||
if (!selectedItemId) throw new Error(tx("请选择兑换物品", "Please select a redeem item"));
|
||||
if (!selectedItemId) throw new Error(tx("请选择交易物品", "Select trade item"));
|
||||
if (!Number.isFinite(quantity) || quantity <= 0) {
|
||||
throw new Error(tx("兑换数量必须大于 0", "Quantity must be greater than 0"));
|
||||
throw new Error(tx("数量必须大于 0", "Amount > 0"));
|
||||
}
|
||||
|
||||
const created = await apiFetch<RedeemCreateResp>(
|
||||
@@ -192,12 +190,8 @@ export default function MePage() {
|
||||
|
||||
setMsg(
|
||||
isZh
|
||||
? `兑换成功:${created.item_name} × ${created.quantity},扣除 ${created.total_cost} 积分${
|
||||
typeof created.rating_after === "number" ? `,当前 Rating ${created.rating_after}` : ""
|
||||
}。`
|
||||
: `Redeemed successfully: ${itemName(created.item_name)} × ${created.quantity}, cost ${created.total_cost} rating${
|
||||
typeof created.rating_after === "number" ? `, current rating ${created.rating_after}` : ""
|
||||
}.`
|
||||
? `交易成功:${created.item_name} × ${created.quantity},花费 ${created.total_cost} 绿宝石。`
|
||||
: `Trade successful: ${itemName(created.item_name)} × ${created.quantity}, cost ${created.total_cost} Emeralds.`
|
||||
);
|
||||
setNote("");
|
||||
await loadAll();
|
||||
@@ -208,176 +202,186 @@ export default function MePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const rank = resolveRank(profile?.rating ?? 0);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8">
|
||||
<h1 className="text-xl font-semibold max-[390px]:text-lg sm:text-2xl">
|
||||
{tx("我的信息与积分兑换", "My Profile & Redeem")}
|
||||
<main className="mx-auto max-w-6xl px-3 py-6 max-[390px]:px-2 sm:px-4 md:px-6 md:py-8 font-mono">
|
||||
<h1 className="text-xl font-bold max-[390px]:text-lg sm:text-2xl text-[color:var(--mc-diamond)] mc-text-shadow">
|
||||
{tx("冒险者档案 & 交易站", "Character Sheet & Trading Post")}
|
||||
</h1>
|
||||
{loading && <p className="mt-3 text-sm text-zinc-500">{tx("加载中...", "Loading...")}</p>}
|
||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||
{msg && <p className="mt-3 text-sm text-emerald-700">{msg}</p>}
|
||||
{loading && <p className="mt-3 text-sm text-[color:var(--mc-stone)]">{tx("读取存档中...", "Loading Save...")}</p>}
|
||||
{error && <p className="mt-3 text-sm text-[color:var(--mc-red)]">{error}</p>}
|
||||
{msg && <p className="mt-3 text-sm text-[color:var(--mc-green)]">{msg}</p>}
|
||||
|
||||
{profile && (
|
||||
<section className="mt-4 rounded-xl border bg-white p-4 text-sm">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<PixelAvatar
|
||||
seed={`${profile.username}-${profile.id}`}
|
||||
size={72}
|
||||
className="border-zinc-700"
|
||||
alt={`${profile.username} avatar`}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<p>ID: {profile.id}</p>
|
||||
<p>{tx("用户名", "Username")}: {profile.username}</p>
|
||||
<p>Rating: {profile.rating}</p>
|
||||
<p>{tx("创建时间", "Created At")}: {fmtTs(profile.created_at)}</p>
|
||||
<p className="text-xs text-zinc-500">
|
||||
{tx("默认像素头像按账号随机生成,可作为主题角色形象。", "Default pixel avatar is randomly generated by account as your theme character.")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-[1fr_2fr]">
|
||||
<section className="rounded-none border-[3px] border-black bg-[color:var(--mc-plank)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<div className="relative mb-4">
|
||||
<div className="absolute inset-0 bg-black opacity-20 translate-x-1 translate-y-1 rounded-none"></div>
|
||||
<div className="border-[4px] border-white p-1 bg-[color:var(--mc-stone-dark)]">
|
||||
<PixelAvatar
|
||||
seed={`${profile.username}-${profile.id}`}
|
||||
size={100}
|
||||
className="border-none"
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="mt-4 rounded-xl border bg-white p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="text-base font-semibold">{tx("每日任务", "Daily Tasks")}</h2>
|
||||
<p className="text-xs text-zinc-600">
|
||||
{dailyDayKey ? `${dailyDayKey} · ` : ""}
|
||||
{tx("已获", "Earned")} {dailyGainedReward}/{dailyTotalReward} {tx("分", "pts")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-3 divide-y">
|
||||
{dailyTasks.map((task) => (
|
||||
<article key={task.code} className="py-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="font-medium">
|
||||
{taskTitle(task)} · +{task.reward}
|
||||
</p>
|
||||
<span
|
||||
className={`rounded px-2 py-0.5 text-xs ${
|
||||
task.completed ? "bg-emerald-100 text-emerald-700" : "bg-zinc-100 text-zinc-600"
|
||||
}`}
|
||||
>
|
||||
{task.completed ? tx("已完成", "Completed") : tx("未完成", "Incomplete")}
|
||||
<h2 className="text-xl font-bold text-black mc-text-shadow-sm mb-1">{profile.username}</h2>
|
||||
<div className={`text-sm font-bold ${rank.color} mb-2`}>
|
||||
{rank.icon} {rank.label} Rank
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-black h-4 border border-white relative mb-1">
|
||||
<div
|
||||
className="h-full bg-[color:var(--mc-green)]"
|
||||
style={{ width: `${Math.min(100, (profile.rating % 100))}%` }}
|
||||
></div>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-[10px] text-white font-bold shadow-black drop-shadow-md">
|
||||
Level {Math.floor(profile.rating / 100)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-zinc-600">{taskDesc(task)}</p>
|
||||
{task.completed && (
|
||||
<p className="mt-1 text-xs text-zinc-500">
|
||||
{tx("完成时间:", "Completed At: ")}
|
||||
{fmtTs(task.completed_at)}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
{!loading && dailyTasks.length === 0 && (
|
||||
<p className="py-3 text-sm text-zinc-500">
|
||||
{tx("今日任务尚未初始化,请稍后刷新。", "Today's tasks are not initialized yet. Please refresh later.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<p className="text-xs text-[color:var(--mc-stone-dark)]">UID: {profile.id}</p>
|
||||
</div>
|
||||
|
||||
<section className="mt-4 rounded-xl border bg-white p-4">
|
||||
<h2 className="text-base font-semibold">{tx("积分兑换物品", "Redeem Items")}</h2>
|
||||
<p className="mt-1 text-xs text-zinc-600">
|
||||
{tx(
|
||||
"示例规则:私人玩游戏时间(假期 1 小时=5 积分;学习日/非节假日 1 小时=25 积分)",
|
||||
"Sample rule: Private Game Time (holiday 1h=5 points; study day/non-holiday 1h=25 points)"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||
{items.map((item) => (
|
||||
<article key={item.id} className="rounded border bg-zinc-50 p-3 text-sm">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<p className="font-medium">{itemName(item.name)}</p>
|
||||
<button
|
||||
className="rounded border px-2 py-1 text-xs hover:bg-zinc-100"
|
||||
onClick={() => setSelectedItemId(item.id)}
|
||||
>
|
||||
{tx("选中", "Select")}
|
||||
</button>
|
||||
<div className="mt-4 space-y-2 border-t border-black/20 pt-4">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-800">{tx("绿宝石 (Rating)", "Emeralds (Rating)")}</span>
|
||||
<span className="font-bold text-[color:var(--mc-green)] text-shadow-sm">{profile.rating}</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-zinc-600">{itemDesc(item.description) || "-"}</p>
|
||||
<p className="mt-1 text-xs text-zinc-700">
|
||||
{tx("假期", "Holiday")}: {item.holiday_cost} / {item.unit_label}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-700">
|
||||
{tx("学习日", "Study Day")}: {item.studyday_cost} / {item.unit_label}
|
||||
</p>
|
||||
</article>
|
||||
))}
|
||||
{!loading && items.length === 0 && (
|
||||
<p className="text-sm text-zinc-500">
|
||||
{tx("管理员尚未配置可兑换物品。", "No redeem items configured by admin yet.")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-zinc-800">{tx("加入时间", "Joined")}</span>
|
||||
<span className="text-zinc-600">{new Date(profile.created_at * 1000).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="mt-4 rounded-lg border p-3">
|
||||
<h3 className="text-sm font-medium">{tx("兑换表单", "Redeem Form")}</h3>
|
||||
<div className="mt-2 grid gap-2 md:grid-cols-2">
|
||||
<select
|
||||
className="rounded border px-3 py-2 text-sm"
|
||||
value={selectedItemId}
|
||||
onChange={(e) => setSelectedItemId(Number(e.target.value))}
|
||||
>
|
||||
<option value={0}>{tx("请选择兑换物品", "Please select an item")}</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{itemName(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Daily Tasks */}
|
||||
<div className="bg-[color:var(--mc-surface)] border-4 border-black p-4 relative">
|
||||
<h2 className="text-xl text-[color:var(--mc-dirt)] mb-4 flex justify-between items-center font-minecraft">
|
||||
<span>每日悬赏任务</span>
|
||||
<span className="text-xs text-[color:var(--mc-gold)]">进度: {dailyGainedReward} / {dailyTotalReward} XP</span>
|
||||
</h2>
|
||||
|
||||
<select
|
||||
className="rounded border px-3 py-2 text-sm"
|
||||
value={dayType}
|
||||
onChange={(e) => setDayType(e.target.value === "studyday" ? "studyday" : "holiday")}
|
||||
>
|
||||
<option value="holiday">{tx("假期时间(按假期单价)", "Holiday time (holiday price)")}</option>
|
||||
<option value="studyday">{tx("学习日/非节假日(按学习日单价)", "Study day/non-holiday (study-day price)")}</option>
|
||||
</select>
|
||||
<div className="space-y-3">
|
||||
{dailyTasks.map((task, idx) => (
|
||||
<div key={idx} className="bg-[color:var(--mc-surface-soft)] p-3 border-2 border-[color:var(--mc-stone-dark)] relative group hover:border-[color:var(--mc-stone)] transition-colors">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-5 h-5 border-2 border-[color:var(--mc-stone-dark)] flex items-center justify-center bg-black/30 mt-0.5 ${task.completed ? 'bg-[color:var(--mc-green)]/20' : ''}`}>
|
||||
{task.completed && <span className="text-[color:var(--mc-green)] text-sm">✓</span>}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<h3 className="text-[color:var(--mc-plank-light)] text-lg font-bold leading-tight">
|
||||
{task.title}
|
||||
<span className="ml-2 text-[color:var(--mc-gold)] text-base font-minecraft">+{task.reward} XP</span>
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-[color:var(--mc-stone)] text-base leading-snug">
|
||||
{task.description}
|
||||
{task.completed && <span className="ml-2 text-[color:var(--mc-stone-dark)] italic text-sm">({fmtTs(task.completed_at)})</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<section className="flex-1 rounded-none border-[3px] border-black bg-[color:var(--mc-stone)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)] text-white">
|
||||
<h2 className="text-xl text-[color:var(--mc-obsidian)] mb-6 flex items-center gap-2 border-b-2 border-[color:var(--mc-stone)]/30 pb-2 font-minecraft">
|
||||
<span className="text-2xl">💎</span>
|
||||
<span>村民交易站</span>
|
||||
<span className="ml-auto text-sm text-[color:var(--mc-stone-dark)]">消耗: RATING</span>
|
||||
</h2>
|
||||
|
||||
<input
|
||||
className="rounded border px-3 py-2 text-sm"
|
||||
type="number"
|
||||
min={1}
|
||||
max={24}
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, Number(e.target.value) || 1))}
|
||||
placeholder={tx("兑换时长(小时)", "Redeem duration (hours)")}
|
||||
/>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex gap-2 text-black">
|
||||
<select
|
||||
className="flex-1 rounded-none border-2 border-black bg-[color:var(--surface)] px-2 py-1 text-base font-bold"
|
||||
value={selectedItemId}
|
||||
onChange={(e) => setSelectedItemId(Number(e.target.value))}
|
||||
>
|
||||
<option value={0}>{tx("选择战利品...", "Select loot...")}</option>
|
||||
{items.map((item) => (
|
||||
<option key={item.id} value={item.id}>
|
||||
{itemName(item.name)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
className="w-20 rounded-none border-2 border-black bg-[color:var(--surface)] px-2 py-1 text-base font-bold text-center"
|
||||
type="number"
|
||||
min={1}
|
||||
max={64}
|
||||
value={quantity}
|
||||
onChange={(e) => setQuantity(Math.max(1, Number(e.target.value) || 1))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
className="rounded border px-3 py-2 text-sm"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
placeholder={tx("备注(可选)", "Note (optional)")}
|
||||
/>
|
||||
{selectedItem && (
|
||||
<div className="bg-[color:var(--mc-stone)]/20 p-3 border border-[color:var(--mc-stone)]/30 rounded-none text-base text-[color:var(--mc-obsidian)]">
|
||||
<p>{selectedItem.description}</p>
|
||||
<p className="mt-1 text-[color:var(--mc-wood-dark)]">
|
||||
单价: {dayType === 'holiday' ? selectedItem.holiday_cost : selectedItem.studyday_cost} Rating / {selectedItem.unit_label}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
className="flex-1 rounded-none border-2 border-black bg-[color:var(--stone-dark)] text-black px-2 py-1 text-base"
|
||||
value={dayType}
|
||||
onChange={(e) => setDayType(e.target.value === "studyday" ? "studyday" : "holiday")}
|
||||
>
|
||||
<option value="holiday">{tx("假期特惠", "Holiday Price")}</option>
|
||||
<option value="studyday">{tx("工作日价格", "Workday Price")}</option>
|
||||
</select>
|
||||
<button
|
||||
className="mc-btn mc-btn-success text-xs px-4"
|
||||
onClick={() => void redeem()}
|
||||
disabled={redeemLoading || !selectedItemId}
|
||||
>
|
||||
{tx("交易", "Trade")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-2 text-xs text-zinc-600">
|
||||
{tx("当前单价", "Current unit price")}: {unitCost} / {tx("小时", "hour")};{tx("预计扣分", "Estimated cost")}: {totalCost}
|
||||
</p>
|
||||
|
||||
<button
|
||||
className="mt-3 rounded bg-zinc-900 px-4 py-2 text-sm text-white disabled:opacity-50"
|
||||
onClick={() => void redeem()}
|
||||
disabled={redeemLoading || !selectedItemId}
|
||||
>
|
||||
{redeemLoading ? tx("兑换中...", "Redeeming...") : tx("确认兑换", "Confirm Redeem")}
|
||||
</button>
|
||||
{/* Rating History Section */}
|
||||
<section className="mt-4 rounded-none border-[3px] border-black bg-[color:var(--mc-surface)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
|
||||
<h2 className="text-base font-bold text-black mb-2">{tx("积分变动记录", "Rating History")}</h2>
|
||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
||||
{historyItems.map((item, idx) => (
|
||||
<div key={idx} className="flex justify-between text-xs text-zinc-800 border-b border-zinc-200 pb-1">
|
||||
<span>
|
||||
<span className={`font-bold ${item.change > 0 ? 'text-[color:var(--mc-green)]' : 'text-[color:var(--mc-red)]'}`}>
|
||||
{item.change > 0 ? `+${item.change}` : item.change}
|
||||
</span>
|
||||
<span className="ml-2">{item.note}</span>
|
||||
</span>
|
||||
<span className="text-[color:var(--mc-stone-dark)]">
|
||||
{new Date(item.created_at * 1000).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{!loading && historyItems.length === 0 && (
|
||||
<p className="text-xs text-zinc-500">{tx("暂无记录。", "No history.")}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="mt-4 rounded-xl border bg-white p-4">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="text-base font-semibold">{tx("兑换记录", "Redeem Records")}</h2>
|
||||
{/* Trades Section */}
|
||||
<section className="mt-4 rounded-none border-[3px] border-black bg-[color:var(--mc-surface)] p-4 shadow-[4px_4px_0_rgba(0,0,0,0.5)]">
|
||||
<div className="flex items-center justify-between gap-2 mb-2">
|
||||
<h2 className="text-base font-bold text-black">{tx("交易记录", "Trade History")}</h2>
|
||||
<button
|
||||
className="rounded border px-3 py-1 text-xs hover:bg-zinc-100"
|
||||
className="text-xs text-[color:var(--mc-stone-dark)] underline"
|
||||
onClick={() => void loadAll()}
|
||||
disabled={loading}
|
||||
>
|
||||
@@ -385,21 +389,19 @@ export default function MePage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 divide-y">
|
||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
||||
{records.map((row) => (
|
||||
<article key={row.id} className="py-2 text-sm">
|
||||
<p>
|
||||
#{row.id} · {itemName(row.item_name)} · {row.quantity} {tx("小时", "hour")} ·{" "}
|
||||
{row.day_type === "holiday" ? tx("假期", "Holiday") : tx("学习日", "Study Day")}
|
||||
</p>
|
||||
<p className="text-xs text-zinc-600">
|
||||
{tx("单价", "Unit cost")} {row.unit_cost},{tx("总扣分", "Total cost")} {row.total_cost} · {fmtTs(row.created_at)}
|
||||
</p>
|
||||
{row.note && <p className="text-xs text-zinc-500">{tx("备注:", "Note: ")}{row.note}</p>}
|
||||
</article>
|
||||
<div key={row.id} className="flex justify-between text-xs text-zinc-800 border-b border-zinc-200 pb-1">
|
||||
<span>
|
||||
{itemName(row.item_name)} × {row.quantity}
|
||||
</span>
|
||||
<span className="text-[color:var(--mc-stone-dark)]">
|
||||
-{row.total_cost} Gems · {new Date(row.created_at * 1000).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{!loading && records.length === 0 && (
|
||||
<p className="py-3 text-sm text-zinc-500">{tx("暂无兑换记录。", "No redeem records yet.")}</p>
|
||||
<p className="text-xs text-zinc-500">{tx("暂无交易。", "No trades.")}</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
在新工单中引用
屏蔽一个用户