feat: 支付登记从平台选择订阅——登录拉取预填,密码不落库

This commit is contained in:
2026-10-08 22:32:33 +08:00
parent 29e0317155
commit ed2ff0f950
8 changed files with 551 additions and 4 deletions
@@ -0,0 +1,203 @@
<script setup lang="ts">
/**
* 订阅选择弹层:登录(仅一次/标签页)+ 列表选用,与登记表单解耦
*/
import { ref } 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;
}>();
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 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 */
}
}
async function load() {
if (!username.value.trim() || !password.value || loading.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) {
writeToken("");
error.value = "平台登录已失效——请重新输入密码";
} else {
error.value = err.message || "拉取失败";
}
list.value = [];
password.value = "";
} finally {
loading.value = false;
}
}
function onUse(s: PlatformSubscription) {
emit("select", s);
emit("update:open", false);
}
</script>
<template>
<a-modal
:open="open"
title="从平台选择订阅"
:footer="null"
:width="520"
destroy-on-hidden
@update:open="emit('update:open', $event)"
>
<template v-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"
@click="onUse(s)"
>
<div class="plat-pick__head">
<strong>订阅 #{{ s.id }}</strong>
<span class="plat-pick__status">{{ 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__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: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>