feat(web): 成员管理——清单表、开户令牌明文、重发与启停

This commit is contained in:
2026-10-08 00:22:27 +08:00
parent adbb6adb93
commit b16ddb1acb
5 changed files with 370 additions and 9 deletions
@@ -0,0 +1,122 @@
<script setup lang="ts">
/**
* 令牌明文对话框:仅签发/重发后出现一次(交互-成员管理 §3)
*/
import { ref, watch } from "vue";
import { message } from "antdv-next";
import { CopyOutlined, WarningOutlined } from "@antdv-next/icons";
const props = defineProps<{
open: boolean;
token: string;
memberName?: string;
}>();
const emit = defineEmits<{
(e: "update:open", v: boolean): void;
}>();
const confirmClose = ref(false);
watch(
() => props.open,
(v) => {
if (v) {
confirmClose.value = false;
}
},
);
async function onCopy() {
try {
await navigator.clipboard.writeText(props.token);
message.success("已复制");
} catch {
message.error("复制失败,请手动选中复制");
}
}
function tryClose() {
if (!confirmClose.value) {
confirmClose.value = true;
return;
}
emit("update:open", false);
}
</script>
<template>
<a-modal
:open="open"
:title="memberName ? `访问令牌 · ${memberName}` : '访问令牌'"
:mask-closable="false"
:closable="false"
:keyboard="false"
:footer="null"
>
<a-alert
type="warning"
show-icon
style="margin-bottom: 16px"
message="关闭后将无法再次查看——请立即复制并通过你信任的渠道发给成员"
/>
<div class="token-plain">
<code class="token-plain__text">{{ token }}</code>
<a-button @click="onCopy">
<CopyOutlined />
复制
</a-button>
</div>
<div class="token-plain__footer">
<template v-if="!confirmClose">
<a-button @click="tryClose">关闭</a-button>
</template>
<template v-else>
<span class="token-plain__confirm">
<WarningOutlined style="color: #faad14" />
确认已保存并发送?
</span>
<a-space>
<a-button @click="confirmClose = false">再看看</a-button>
<a-button type="primary" @click="tryClose">我已保存并发送</a-button>
</a-space>
</template>
</div>
</a-modal>
</template>
<style scoped>
.token-plain {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border: 1px solid #f0f0f0;
border-radius: 8px;
background: #fafafa;
}
.token-plain__text {
flex: 1;
word-break: break-all;
font-size: 13px;
line-height: 22px;
color: rgba(0, 0, 0, 0.88);
user-select: all;
}
.token-plain__footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
min-height: 32px;
}
.token-plain__confirm {
margin-right: auto;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
}
</style>
@@ -6,6 +6,7 @@ import { computed, reactive, ref, watch } from "vue";
import type { MemberItem } from "@new-api-key-usage/core";
import type { ApiError } from "@/api/http.ts";
import { useCreateMember, useRegisterKey } from "@/queries/admin.ts";
import MemberTokenModal from "@/components/admin/member-token-modal.vue";
const props = defineProps<{
open: boolean;
@@ -30,6 +31,9 @@ const resultMsg = ref<{ type: "success" | "error" | "warning"; text: string } |
const showNewMember = ref(false);
const newMemberName = ref("");
const newMemberRemark = ref("");
const tokenOpen = ref(false);
const plainToken = ref("");
const tokenMemberName = ref("");
watch(
() => props.open,
@@ -106,15 +110,22 @@ async function onCreateMember() {
const name = newMemberName.value.trim();
if (!name) return;
try {
const m = await createMember.mutateAsync({
const res = await createMember.mutateAsync({
name,
remark: newMemberRemark.value.trim(),
});
form.member_id = m.id;
form.member_id = res.member.id;
showNewMember.value = false;
newMemberName.value = "";
newMemberRemark.value = "";
resultMsg.value = { type: "success", text: `已新建成员「${m.name}」并选为归属` };
// POST /members 必回明文令牌——立刻展示,避免遗失
plainToken.value = res.token;
tokenMemberName.value = res.member.name;
tokenOpen.value = true;
resultMsg.value = {
type: "success",
text: `已新建成员「${res.member.name}」并选为归属,请保存下方令牌`,
};
} catch (e) {
const err = e as ApiError;
resultMsg.value = { type: "error", text: err.message || "新建成员失败" };
@@ -187,6 +198,12 @@ async function onCreateMember() {
:message="resultMsg.text"
/>
</a-form>
<MemberTokenModal
v-model:open="tokenOpen"
:token="plainToken"
:member-name="tokenMemberName"
@update:open="(v: boolean) => { if (!v) { plainToken = ''; tokenMemberName = ''; } }"
/>
</a-modal>
</template>