11
This commit is contained in:
@@ -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 }),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/main.js
11
src/main.js
@@ -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");
|
||||||
|
|||||||
@@ -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" };
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user