Files
new-api-key-usage/apps/web/src/components/admin/platform-subscription-picker.vue
T

265 lines
6.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 订阅选择弹层:登录(仅一次/标签页)+ 列表选用,与登记表单解耦
*/
import { ref, watch } from "vue";
import {
listPlatformSubscriptions,
loginPlatform,
type PlatformSubscription,
} from "@/api/admin/platform.ts";
import type { ApiError } from "@/api/http.ts";
import { formatDateTime } from "@/utils/time.ts";
const props = defineProps<{
open: boolean;
/** 已登记的 subscription_id——灰显不可选用 */
registeredIds?: number[];
}>();
const emit = defineEmits<{
(e: "update:open", v: boolean): void;
(e: "select", sub: PlatformSubscription): void;
}>();
const TOKEN_KEY = "naku_platform_token";
const username = ref("");
const password = ref("");
const error = ref("");
const loading = ref(false);
/** 打开后检查登录并拉取中——避免闪现登录表单 */
const booting = ref(false);
const list = ref<PlatformSubscription[]>([]);
function readToken(): string {
try {
return sessionStorage.getItem(TOKEN_KEY) ?? "";
} catch {
return "";
}
}
function writeToken(token: string) {
try {
if (token) sessionStorage.setItem(TOKEN_KEY, token);
else sessionStorage.removeItem(TOKEN_KEY);
} catch {
/* ignore */
}
}
watch(
() => props.open,
(v) => {
if (v) {
// 有 token:先显示「检查中」,拉完再进列表/登录,不闪登录表单
if (readToken()) {
booting.value = true;
void load(true).finally(() => {
booting.value = false;
});
}
} else {
list.value = [];
error.value = "";
password.value = "";
booting.value = false;
}
},
);
async function load(skipLoginCheck = false) {
if (loading.value) return;
if (!skipLoginCheck && (!username.value.trim() || !password.value)) return;
error.value = "";
loading.value = true;
try {
let token = readToken();
if (!token) {
const login = await loginPlatform({
username: username.value.trim(),
password: password.value,
});
token = login.access_token;
writeToken(token);
}
password.value = "";
list.value = await listPlatformSubscriptions(token);
if (list.value.length === 0) {
error.value = "账号下没有订阅记录";
}
} catch (e) {
const err = e as ApiError;
if (err.status === 401 || err.code === "platform_token_invalid") {
writeToken("");
error.value = "平台登录已失效——请重新输入密码";
} else {
error.value = err.message || "拉取失败";
}
list.value = [];
password.value = "";
} finally {
loading.value = false;
}
}
function isRegistered(s: PlatformSubscription): boolean {
return (props.registeredIds ?? []).includes(s.id);
}
function onUse(s: PlatformSubscription) {
if (isRegistered(s)) return;
emit("select", s);
emit("update:open", false);
}
</script>
<template>
<a-modal
:open="open"
title="从平台选择订阅"
:footer="null"
:width="520"
centered
:styles="{ body: { maxHeight: '70vh', overflowY: 'auto' } }"
destroy-on-hidden
@update:open="emit('update:open', $event)"
>
<div v-if="booting || (loading && list.length === 0)" class="plat-pick__boot">
<a-spin />
<span class="plat-pick__boot-text">正在检查登录并拉取…</span>
</div>
<template v-else-if="list.length === 0">
<p class="plat-pick__hint">
使用 NewAPI 控制台账号登录后拉取订阅,仅用于预填登记;密码不保存。
</p>
<a-form layout="vertical" @finish="load">
<a-form-item label="账号">
<a-input
v-model:value="username"
autocomplete="username"
@press-enter="load"
/>
</a-form-item>
<a-form-item label="密码">
<a-input-password
v-model:value="password"
autocomplete="current-password"
@press-enter="load"
/>
</a-form-item>
<a-button
type="primary"
html-type="submit"
block
:loading="loading"
:disabled="!username.trim() || !password"
>
登录并拉取
</a-button>
</a-form>
<a-alert
v-if="error"
type="warning"
show-icon
:message="error"
style="margin-top: 12px"
/>
</template>
<template v-else>
<div class="plat-pick__list">
<div
v-for="s in list"
:key="s.id"
class="plat-pick__item"
:class="{ 'is-registered': isRegistered(s) }"
@click="onUse(s)"
>
<div class="plat-pick__head">
<strong>订阅 #{{ s.id }}</strong>
<span class="plat-pick__status">
{{ isRegistered(s) ? "已登记" : s.status }}
</span>
</div>
<div class="plat-pick__meta">
{{ formatDateTime(s.start_time) }} ~
{{ s.end_time ? formatDateTime(s.end_time) : "进行中" }}
· 额度 {{ s.amount_total }}
</div>
</div>
</div>
</template>
</a-modal>
</template>
<style scoped>
.plat-pick__boot {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 48px 0;
}
.plat-pick__boot-text {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.plat-pick__hint {
margin: 0 0 16px;
font-size: 12px;
line-height: 20px;
color: rgba(0, 0, 0, 0.45);
}
.plat-pick__list {
max-height: 360px;
overflow-y: auto;
border: 1px solid #f0f0f0;
border-radius: 8px;
}
.plat-pick__item {
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
}
.plat-pick__item:hover {
background: #fafafa;
}
.plat-pick__item.is-registered {
opacity: 0.45;
cursor: not-allowed;
}
.plat-pick__item.is-registered:hover {
background: transparent;
}
.plat-pick__item:last-child {
border-bottom: none;
}
.plat-pick__head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 4px;
}
.plat-pick__status {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
}
.plat-pick__meta {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
font-variant-numeric: tabular-nums;
}
</style>