This commit is contained in:
fengchuanhn@gmail.com
2026-05-23 10:33:26 +08:00
parent 6ad45ed60c
commit defeaf7434
5 changed files with 161 additions and 82 deletions

View File

@@ -1,15 +1,14 @@
import { apiRequest } from "./client.js"; import { apiRequest } from "./client.js";
/** /**
* @param {string} token * @param {{ username: string, serialNumber: string }} payload
* @param {string} serialNumber
*/ */
export function activateCardKeyApi(token, serialNumber) { export function activateCardKeyApi({ username, serialNumber }) {
return apiRequest("/card-keys/activate", { return apiRequest("/card-keys/activate", {
method: "POST", method: "POST",
headers: { body: JSON.stringify({
Authorization: `Bearer ${token}`, username: username.trim(),
}, serial_number: serialNumber.trim().toUpperCase(),
body: JSON.stringify({ serial_number: serialNumber }), }),
}); });
} }

View File

@@ -88,12 +88,19 @@ function setupHeartbeatLogoutListener() {
if (!authStore.isAuthenticated) { if (!authStore.isAuthenticated) {
return; return;
} }
authStore.currentUser.vip_end_time="2022-06-28T16:01:13"; authStore.redirectToCardActivate(router);
await router.replace({ name: "card-activate" }); });
}
function setupVipWatch() {
authStore.$subscribe(() => {
if (!authStore.needsVipActivation) return;
if (router.currentRoute.value.name === "card-activate") return;
authStore.redirectToCardActivate(router);
}); });
} }
setupHeartbeatLogoutListener(); setupHeartbeatLogoutListener();
setupVipWatch();
app.mount("#app"); app.mount("#app");

View File

@@ -390,6 +390,10 @@ router.beforeEach((to) => {
const auth = useAuthStore(); const auth = useAuthStore();
if (to.name === "card-activate") {
return true;
}
const requiresAuth = to.matched.some((record) => record.meta.requiresAuth); const requiresAuth = to.matched.some((record) => record.meta.requiresAuth);
@@ -402,6 +406,17 @@ router.beforeEach((to) => {
if (auth.needsVipActivation) {
return {
name: "card-activate",
query: { username: auth.currentUser?.username ?? "" },
};
}
if ((to.name === "login" || to.name === "register") && auth.isAuthenticated) { if ((to.name === "login" || to.name === "register") && auth.isAuthenticated) {
return { name: "home" }; return { name: "home" };
@@ -417,8 +432,6 @@ router.beforeEach((to) => {
.find((role) => role != null); .find((role) => role != null);
console.log('requiredRole',requiredRole);
console.log('auth.roleId',auth.roleId);
if (requiredRole != null && auth.roleId !== requiredRole) { if (requiredRole != null && auth.roleId !== requiredRole) {
return { name: "home" }; return { name: "home" };

View File

@@ -5,11 +5,24 @@ import { fetchMeApi, loginApi, logoutApi, registerApi } from "../api/auth.js";
const TOKEN_KEY = "aiclient_token"; const TOKEN_KEY = "aiclient_token";
const USER_KEY = "aiclient_current_user"; const USER_KEY = "aiclient_current_user";
/** 普通用户 */
export const ROLE_USER = 0;
/** 管理员 */ /** 管理员 */
export const ROLE_ADMIN = 1; export const ROLE_ADMIN = 1;
/** 代理 */ /** 代理 */
export const ROLE_AGENT = 2; export const ROLE_AGENT = 2;
export const ROLE_OEM=3; export const ROLE_OEM = 3;
export function isVipExpired(vipEndTime) {
if (!vipEndTime) return true;
const d = new Date(vipEndTime);
if (Number.isNaN(d.getTime())) return true;
return d.getTime() <= Date.now();
}
function isStaffRole(roleId) {
return roleId === ROLE_ADMIN || roleId === ROLE_AGENT || roleId === ROLE_OEM;
}
async function syncAuthSessionToRust(token, user) { async function syncAuthSessionToRust(token, user) {
@@ -32,7 +45,6 @@ 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,
@@ -62,7 +74,13 @@ export const useAuthStore = defineStore("auth", {
roleId: (state) => state.currentUser?.role_id ?? null, roleId: (state) => state.currentUser?.role_id ?? null,
isAdmin: (state) => state.currentUser?.role_id === ROLE_ADMIN, isAdmin: (state) => state.currentUser?.role_id === ROLE_ADMIN,
isAgent: (state) => state.currentUser?.role_id === ROLE_AGENT, isAgent: (state) => state.currentUser?.role_id === ROLE_AGENT,
isOEM : (state) => state.currentUser?.role_id === ROLE_OEM, isOEM: (state) => state.currentUser?.role_id === ROLE_OEM,
isVipExpired: (state) => isVipExpired(state.currentUser?.vip_end_time),
needsVipActivation: (state) => {
if (!state.token || !state.currentUser) return false;
if (isStaffRole(state.currentUser.role_id ?? ROLE_USER)) return false;
return isVipExpired(state.currentUser.vip_end_time);
},
}, },
actions: { actions: {
@@ -87,6 +105,19 @@ export const useAuthStore = defineStore("auth", {
syncAuthSessionToRust(this.token, this.currentUser); syncAuthSessionToRust(this.token, this.currentUser);
}, },
cardActivateQuery() {
return {
username: this.currentUser?.username ?? "",
};
},
redirectToCardActivate(router) {
router.replace({
name: "card-activate",
query: this.cardActivateQuery(),
});
},
async register({ username, password, confirmPassword }) { async register({ username, password, confirmPassword }) {
const name = username?.trim(); const name = username?.trim();
if (!name || !password) { if (!name || !password) {

View File

@@ -1,16 +1,22 @@
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useAuthStore } from "../stores/auth.js"; import { useAuthStore } from "../stores/auth.js";
import { activateCardKeyApi } from "../api/cardKeys.js"; import { activateCardKeyApi } from "../api/cardKeys.js";
import AuthLayout from "../components/AuthLayout.vue";
const route = useRoute();
const router = useRouter();
const auth = useAuthStore(); const auth = useAuthStore();
const username = ref("");
const serialNumber = ref(""); const serialNumber = ref("");
const loading = ref(false); const loading = ref(false);
const feedback = ref({ severity: "", message: "" }); const feedback = ref({ severity: "", message: "" });
const resultSnapshot = ref(null);
const vipEndLabel = computed(() => { const vipEndLabel = computed(() => {
const raw = auth.currentUser?.vip_end_time; const raw = resultSnapshot.value?.vip_end_time ?? auth.currentUser?.vip_end_time;
if (!raw) return "未开通"; if (!raw) return "未开通";
const d = new Date(raw); const d = new Date(raw);
if (Number.isNaN(d.getTime())) return "—"; if (Number.isNaN(d.getTime())) return "—";
@@ -18,11 +24,25 @@ const vipEndLabel = computed(() => {
}); });
const pointsLabel = computed(() => { const pointsLabel = computed(() => {
const p = auth.currentUser?.points; const p = resultSnapshot.value?.points ?? auth.currentUser?.points;
if (p == null || Number.isNaN(Number(p))) return "0"; if (p == null || Number.isNaN(Number(p))) return "0";
return String(p); return String(p);
}); });
const showStatus = computed(() => auth.isAuthenticated || resultSnapshot.value != null);
watch(
() => route.query.username,
(value) => {
if (typeof value === "string" && value.trim()) {
username.value = value.trim();
} else if (auth.currentUser?.username) {
username.value = auth.currentUser.username;
}
},
{ immediate: true },
);
function showMessage(severity, message) { function showMessage(severity, message) {
feedback.value = { severity, message }; feedback.value = { severity, message };
} }
@@ -39,7 +59,12 @@ function formatActivateDetail(data) {
} }
async function onSubmit() { async function onSubmit() {
const name = username.value.trim();
const serial = serialNumber.value.trim().toUpperCase(); const serial = serialNumber.value.trim().toUpperCase();
if (!name) {
showMessage("warn", "请输入用户名");
return;
}
if (!serial) { if (!serial) {
showMessage("warn", "请输入卡密"); showMessage("warn", "请输入卡密");
return; return;
@@ -48,7 +73,7 @@ async function onSubmit() {
loading.value = true; loading.value = true;
feedback.value = { severity: "", message: "" }; feedback.value = { severity: "", message: "" };
const res = await activateCardKeyApi(auth.token, serial); const res = await activateCardKeyApi({ username: name, serialNumber: serial });
loading.value = false; loading.value = false;
if (!res.ok) { if (!res.ok) {
@@ -57,22 +82,38 @@ async function onSubmit() {
} }
serialNumber.value = ""; serialNumber.value = "";
resultSnapshot.value = res.data ?? null;
if (auth.isAuthenticated && auth.currentUser?.username === name) {
await auth.refreshProfile(); await auth.refreshProfile();
}
const detail = formatActivateDetail(res.data); const detail = formatActivateDetail(res.data);
showMessage("success", detail ? `${res.message}${detail}` : res.message || "激活成功"); showMessage("success", detail ? `${res.message}${detail}` : res.message || "激活成功");
if (auth.isAuthenticated && auth.currentUser?.username === name && !auth.isVipExpired) {
router.replace({ name: "home" });
}
}
function goLogin() {
router.push({ name: "login" });
} }
</script> </script>
<template> <template>
<div class="card-activate-page"> <AuthLayout title="卡密激活" subtitle="输入用户名与卡密序列号,兑换会员时长或点数">
<div class="card-activate-page__header"> <Message
<h1 class="gradient-text text-xl font-semibold">卡密激活</h1> v-if="feedback.message"
<p class="mt-1 text-sm text-slate-400">输入卡密序列号兑换会员时长或点数</p> :severity="feedback.severity"
</div> :closable="true"
class="mb-4 w-full"
@close="feedback.message = ''"
>
{{ feedback.message }}
</Message>
<div class="card-activate-page__panel"> <div v-if="showStatus" class="card-activate-page__status mb-4">
<div class="card-activate-page__status">
<div class="card-activate-page__status-item"> <div class="card-activate-page__status-item">
<span class="text-xs uppercase text-slate-500">会员到期</span> <span class="text-xs uppercase text-slate-500">会员到期</span>
<span class="text-sm text-slate-200">{{ vipEndLabel }}</span> <span class="text-sm text-slate-200">{{ vipEndLabel }}</span>
@@ -83,19 +124,20 @@ async function onSubmit() {
</div> </div>
</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"> <form class="flex flex-col gap-4" @submit.prevent="onSubmit">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label for="card-serial" class="text-xs uppercase text-slate-500">卡密序列号</label> <label for="card-username" class="font-mono text-xs text-text-muted uppercase">用户名</label>
<InputText
id="card-username"
v-model="username"
placeholder="请输入要激活的用户名"
autocomplete="username"
class="w-full"
:disabled="loading"
/>
</div>
<div class="flex flex-col gap-2">
<label for="card-serial" class="font-mono text-xs text-text-muted uppercase">卡密序列号</label>
<InputText <InputText
id="card-serial" id="card-serial"
v-model="serialNumber" v-model="serialNumber"
@@ -103,36 +145,23 @@ async function onSubmit() {
class="w-full font-mono uppercase tracking-wider" class="w-full font-mono uppercase tracking-wider"
autocomplete="off" autocomplete="off"
:disabled="loading" :disabled="loading"
@keyup.enter="onSubmit"
/> />
</div> </div>
<Button type="submit" label="立即激活" :loading="loading" class="w-full" /> <Button type="submit" label="立即激活" :loading="loading" class="w-full" />
<Button
v-if="!auth.isAuthenticated"
type="button"
label="已有账号,去登录"
severity="secondary"
text
class="w-full"
@click="goLogin"
/>
</form> </form>
</div> </AuthLayout>
</div>
</template> </template>
<style scoped> <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 { .card-activate-page__status {
display: grid; display: grid;
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;