feat: 支付登记从平台选择订阅——登录拉取预填,密码不落库
This commit is contained in:
@@ -7,8 +7,15 @@ import type { Dayjs } from "dayjs";
|
||||
import dayjs from "dayjs";
|
||||
import type { PaymentItem } from "@new-api-key-usage/core";
|
||||
import type { ApiError } from "@/api/http.ts";
|
||||
import { useCreatePayment, useQpuLatest, useUpdatePayment } from "@/queries/admin.ts";
|
||||
import { unixToBeijingWall, wallAsBeijingIso } from "@/utils/time.ts";
|
||||
import {
|
||||
useCreatePayment,
|
||||
usePayments,
|
||||
useQpuLatest,
|
||||
useUpdatePayment,
|
||||
} from "@/queries/admin.ts";
|
||||
import PlatformSubscriptionPicker from "@/components/admin/platform-subscription-picker.vue";
|
||||
import type { PlatformSubscription } from "@/api/admin/platform.ts";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
@@ -23,6 +30,7 @@ const emit = defineEmits<{
|
||||
const createPay = useCreatePayment();
|
||||
const updatePay = useUpdatePayment();
|
||||
const qpuQuery = useQpuLatest();
|
||||
const paymentsQuery = usePayments();
|
||||
const qpu = computed(() => qpuQuery.data.value?.qpu ?? null);
|
||||
|
||||
const form = reactive({
|
||||
@@ -38,6 +46,17 @@ const form = reactive({
|
||||
quota_usd_input: null as number | null,
|
||||
});
|
||||
const error = ref("");
|
||||
const pickerOpen = ref(false);
|
||||
|
||||
function onPickSubscription(s: PlatformSubscription) {
|
||||
form.kind = "subscription";
|
||||
form.subscription_id = s.id;
|
||||
form.period_start = unixToBeijingWall(s.start_time);
|
||||
form.period_end = s.end_time > 0 ? unixToBeijingWall(s.end_time) : null;
|
||||
form.quotaMode = "quota";
|
||||
form.quota_granted = s.amount_total;
|
||||
form.quota_usd_input = null;
|
||||
}
|
||||
|
||||
const isEdit = computed(() => props.editing != null);
|
||||
const saving = computed(() => createPay.isPending.value || updatePay.isPending.value);
|
||||
@@ -122,6 +141,16 @@ async function onSubmit() {
|
||||
error.value = "订阅必须填写订阅编号";
|
||||
return;
|
||||
}
|
||||
// 一笔订阅只应登记一条支付记录;改错走「编辑」
|
||||
const dup = (paymentsQuery.data.value ?? []).find(
|
||||
(p) =>
|
||||
p.subscription_id === form.subscription_id &&
|
||||
p.id !== (props.editing?.id ?? -1),
|
||||
);
|
||||
if (dup) {
|
||||
error.value = `订阅 #${form.subscription_id} 已有支付登记——请编辑原记录,不要重复登记`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const body = {
|
||||
kind: form.kind,
|
||||
@@ -172,6 +201,14 @@ async function onSubmit() {
|
||||
<a-radio value="topup">充值</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-button
|
||||
v-if="form.kind === 'subscription' && !isEdit"
|
||||
class="pay-form__pick"
|
||||
block
|
||||
@click="pickerOpen = true"
|
||||
>
|
||||
从平台选择订阅(自动填写编号、期间与额度)
|
||||
</a-button>
|
||||
<a-form-item label="金额(币种自定)" required>
|
||||
<a-input-number
|
||||
v-model:value="form.paid_amount"
|
||||
@@ -242,14 +279,24 @@ async function onSubmit() {
|
||||
</a-form-item>
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||
</a-form>
|
||||
<PlatformSubscriptionPicker
|
||||
v-model:open="pickerOpen"
|
||||
@select="onPickSubscription"
|
||||
/>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pay-form__pick {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.pay-form__note {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user