11
This commit is contained in:
15
src/api/cardKeys.js
Normal file
15
src/api/cardKeys.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { apiRequest } from "./client.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @param {string} serialNumber
|
||||||
|
*/
|
||||||
|
export function activateCardKeyApi(token, serialNumber) {
|
||||||
|
return apiRequest("/card-keys/activate", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ serial_number: serialNumber }),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -93,6 +93,11 @@ const activeName = computed(() => {
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
<svg v-else-if="item.icon === 'card-activate'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||||
|
<circle cx="9" cy="12" r="2.5" />
|
||||||
|
<path d="M11.5 12h5M15 12v2M17 12v1.5" stroke-linecap="round" />
|
||||||
|
</svg>
|
||||||
<svg v-else-if="item.icon === 'help'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
<svg v-else-if="item.icon === 'help'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
<circle cx="12" cy="12" r="9" />
|
<circle cx="12" cy="12" r="9" />
|
||||||
<path d="M9.5 9a2.5 2.5 0 1 1 4.2 1.8c-.8.7-1.2 1.2-1.2 2.2M12 17h.01" stroke-linecap="round" />
|
<path d="M9.5 9a2.5 2.5 0 1 1 4.2 1.8c-.8.7-1.2 1.2-1.2 2.2M12 17h.01" stroke-linecap="round" />
|
||||||
|
|||||||
@@ -88,10 +88,9 @@ function setupHeartbeatLogoutListener() {
|
|||||||
if (!authStore.isAuthenticated) {
|
if (!authStore.isAuthenticated) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await authStore.logout();
|
authStore.currentUser.vip_end_time="2022-06-28T16:01:13";
|
||||||
if (router.currentRoute.value.name !== "login") {
|
await router.replace({ name: "card-activate" });
|
||||||
await router.replace({ name: "login" });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ function applyTokenResponse(store, res, fallbackMessage) {
|
|||||||
if (!data?.access_token || !data?.user) {
|
if (!data?.access_token || !data?.user) {
|
||||||
return { ok: false, message: "服务器返回数据异常" };
|
return { ok: false, message: "服务器返回数据异常" };
|
||||||
}
|
}
|
||||||
|
console.log(data.user);
|
||||||
store.setSession(data.access_token, {
|
store.setSession(data.access_token, {
|
||||||
id: data.user.id,
|
id: data.user.id,
|
||||||
username: data.user.username,
|
username: data.user.username,
|
||||||
role_id: data.user.role_id,
|
role_id: data.user.role_id,
|
||||||
vip_end_time: data.user.vip_end_time ?? null,
|
vip_end_time: data.user.vip_end_time ?? null,
|
||||||
|
points: data.user.points ?? 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ok: true, message: res.message || fallbackMessage };
|
return { ok: true, message: res.message || fallbackMessage };
|
||||||
@@ -133,6 +134,7 @@ export const useAuthStore = defineStore("auth", {
|
|||||||
username: u.username,
|
username: u.username,
|
||||||
role_id: u.role_id,
|
role_id: u.role_id,
|
||||||
vip_end_time: u.vip_end_time ?? null,
|
vip_end_time: u.vip_end_time ?? null,
|
||||||
|
points: u.points ?? 0,
|
||||||
};
|
};
|
||||||
localStorage.setItem(USER_KEY, JSON.stringify(this.currentUser));
|
localStorage.setItem(USER_KEY, JSON.stringify(this.currentUser));
|
||||||
syncAuthSessionToRust(this.token, this.currentUser);
|
syncAuthSessionToRust(this.token, this.currentUser);
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, ref } from "vue";
|
||||||
|
import { useAuthStore } from "../stores/auth.js";
|
||||||
|
import { activateCardKeyApi } from "../api/cardKeys.js";
|
||||||
|
|
||||||
|
const auth = useAuthStore();
|
||||||
|
|
||||||
|
const serialNumber = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
const feedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
|
const vipEndLabel = computed(() => {
|
||||||
|
const raw = auth.currentUser?.vip_end_time;
|
||||||
|
if (!raw) return "未开通";
|
||||||
|
const d = new Date(raw);
|
||||||
|
if (Number.isNaN(d.getTime())) return "—";
|
||||||
|
return d.toLocaleString("zh-CN", { hour12: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
const pointsLabel = computed(() => {
|
||||||
|
const p = auth.currentUser?.points;
|
||||||
|
if (p == null || Number.isNaN(Number(p))) return "0";
|
||||||
|
return String(p);
|
||||||
|
});
|
||||||
|
|
||||||
|
function showMessage(severity, message) {
|
||||||
|
feedback.value = { severity, message };
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatActivateDetail(data) {
|
||||||
|
if (!data) return "";
|
||||||
|
if (data.card_type === "时长" && data.added_days != null) {
|
||||||
|
return `会员已延长 ${data.added_days} 天`;
|
||||||
|
}
|
||||||
|
if (data.card_type === "点数" && data.added_points != null) {
|
||||||
|
return `已获得 ${data.added_points} 点数`;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSubmit() {
|
||||||
|
const serial = serialNumber.value.trim().toUpperCase();
|
||||||
|
if (!serial) {
|
||||||
|
showMessage("warn", "请输入卡密");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
feedback.value = { severity: "", message: "" };
|
||||||
|
|
||||||
|
const res = await activateCardKeyApi(auth.token, serial);
|
||||||
|
loading.value = false;
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
showMessage("error", res.message || "激活失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumber.value = "";
|
||||||
|
await auth.refreshProfile();
|
||||||
|
|
||||||
|
const detail = formatActivateDetail(res.data);
|
||||||
|
showMessage("success", detail ? `${res.message}(${detail})` : res.message || "激活成功");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card-activate-page">
|
||||||
|
<div class="card-activate-page__header">
|
||||||
|
<h1 class="gradient-text text-xl font-semibold">卡密激活</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-400">输入卡密序列号,兑换会员时长或点数</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-activate-page__panel">
|
||||||
|
<div class="card-activate-page__status">
|
||||||
|
<div class="card-activate-page__status-item">
|
||||||
|
<span class="text-xs uppercase text-slate-500">会员到期</span>
|
||||||
|
<span class="text-sm text-slate-200">{{ vipEndLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-activate-page__status-item">
|
||||||
|
<span class="text-xs uppercase text-slate-500">当前点数</span>
|
||||||
|
<span class="text-sm text-slate-200">{{ pointsLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Message
|
||||||
|
v-if="feedback.message"
|
||||||
|
:severity="feedback.severity"
|
||||||
|
:closable="true"
|
||||||
|
class="w-full"
|
||||||
|
@close="feedback.message = ''"
|
||||||
|
>
|
||||||
|
{{ feedback.message }}
|
||||||
|
</Message>
|
||||||
|
|
||||||
|
<form class="flex flex-col gap-4" @submit.prevent="onSubmit">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="card-serial" class="text-xs uppercase text-slate-500">卡密序列号</label>
|
||||||
|
<InputText
|
||||||
|
id="card-serial"
|
||||||
|
v-model="serialNumber"
|
||||||
|
placeholder="请输入卡密"
|
||||||
|
class="w-full font-mono uppercase tracking-wider"
|
||||||
|
autocomplete="off"
|
||||||
|
:disabled="loading"
|
||||||
|
@keyup.enter="onSubmit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit" label="立即激活" :loading="loading" class="w-full" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card-activate-page {
|
||||||
|
max-width: 28rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.5rem 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-activate-page__header {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-activate-page__panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.25rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
border: 1px solid rgb(255 255 255 / 0.1);
|
||||||
|
background: rgb(255 255 255 / 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-activate-page__status {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-activate-page__status-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
background: rgb(0 0 0 / 0.2);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user