feat(web): Key 管理——清单表、行内归属启停、登记四态与停滞显形
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 登记 Key:sk-key 明文在 web 的唯一露面处(交互-key管理 §2)
|
||||
*/
|
||||
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";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
members: MemberItem[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", v: boolean): void;
|
||||
(e: "registered", keyId: number): void;
|
||||
}>();
|
||||
|
||||
const register = useRegisterKey();
|
||||
const createMember = useCreateMember();
|
||||
|
||||
const form = reactive({
|
||||
sk_key: "",
|
||||
member_id: null as number | "unassigned" | null,
|
||||
});
|
||||
const resultMsg = ref<{ type: "success" | "error" | "warning"; text: string } | null>(
|
||||
null,
|
||||
);
|
||||
const showNewMember = ref(false);
|
||||
const newMemberName = ref("");
|
||||
const newMemberRemark = ref("");
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(v) => {
|
||||
if (!v) {
|
||||
form.sk_key = "";
|
||||
form.member_id = null;
|
||||
resultMsg.value = null;
|
||||
showNewMember.value = false;
|
||||
newMemberName.value = "";
|
||||
newMemberRemark.value = "";
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const memberOptions = computed(() => [
|
||||
{ value: "unassigned", label: "未归属" },
|
||||
...props.members.map((m) => ({ value: m.id, label: m.name })),
|
||||
]);
|
||||
|
||||
async function onSubmit() {
|
||||
resultMsg.value = null;
|
||||
if (!form.sk_key.trim()) {
|
||||
resultMsg.value = { type: "warning", text: "请粘贴 sk-key" };
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const key = await register.mutateAsync({
|
||||
sk_key: form.sk_key.trim(),
|
||||
member_id:
|
||||
form.member_id === "unassigned" || form.member_id == null
|
||||
? null
|
||||
: form.member_id,
|
||||
});
|
||||
resultMsg.value = {
|
||||
type: "success",
|
||||
text: "已登记,首轮同步进行中(约 5 分钟内完成)",
|
||||
};
|
||||
emit("registered", key.id);
|
||||
} catch (e) {
|
||||
const err = e as ApiError;
|
||||
if (err.status === 409) {
|
||||
resultMsg.value = {
|
||||
type: "warning",
|
||||
text: "该 key 已在清单中",
|
||||
};
|
||||
if (err.keyId != null) emit("registered", err.keyId);
|
||||
} else if (err.status === 422) {
|
||||
// 上游拒绝 vs 本地表单校验(如 sk- 前缀)文案不同
|
||||
const fieldMsg = err.fields?.sk_key;
|
||||
resultMsg.value = {
|
||||
type: "error",
|
||||
text:
|
||||
fieldMsg ??
|
||||
(err.code === "validation_failed"
|
||||
? err.message || "请检查填写内容"
|
||||
: "上游拒绝了这枚 key——可能不存在或已被禁用"),
|
||||
};
|
||||
} else if (err.status === 502 || err.status === 0) {
|
||||
resultMsg.value = {
|
||||
type: "error",
|
||||
text: "上游站点暂时不可达,请稍后重试",
|
||||
};
|
||||
} else {
|
||||
resultMsg.value = {
|
||||
type: "error",
|
||||
text: err.message || "登记失败,请稍后重试",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onCreateMember() {
|
||||
const name = newMemberName.value.trim();
|
||||
if (!name) return;
|
||||
try {
|
||||
const m = await createMember.mutateAsync({
|
||||
name,
|
||||
remark: newMemberRemark.value.trim(),
|
||||
});
|
||||
form.member_id = m.id;
|
||||
showNewMember.value = false;
|
||||
newMemberName.value = "";
|
||||
newMemberRemark.value = "";
|
||||
resultMsg.value = { type: "success", text: `已新建成员「${m.name}」并选为归属` };
|
||||
} catch (e) {
|
||||
const err = e as ApiError;
|
||||
resultMsg.value = { type: "error", text: err.message || "新建成员失败" };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
title="登记 Key"
|
||||
:confirm-loading="register.isPending.value"
|
||||
ok-text="登记"
|
||||
cancel-text="取消"
|
||||
@update:open="emit('update:open', $event)"
|
||||
@ok="onSubmit"
|
||||
>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="sk-key" required>
|
||||
<a-textarea
|
||||
v-model:value="form.sk_key"
|
||||
:rows="4"
|
||||
placeholder="粘贴你在 NewAPI 创建的 sk-…"
|
||||
/>
|
||||
<div class="register-key__note">
|
||||
粘贴后立即加密存储,界面不再回显明文
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="归属成员">
|
||||
<a-select
|
||||
v-model:value="form.member_id"
|
||||
:options="memberOptions"
|
||||
placeholder="未归属"
|
||||
allow-clear
|
||||
/>
|
||||
<a-button
|
||||
type="link"
|
||||
size="small"
|
||||
style="padding-left: 0"
|
||||
@click="showNewMember = !showNewMember"
|
||||
>
|
||||
+ 新建成员
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="showNewMember" label="新成员名称">
|
||||
<a-input
|
||||
v-model:value="newMemberName"
|
||||
placeholder="成员名称"
|
||||
@press-enter="onCreateMember"
|
||||
/>
|
||||
<a-input
|
||||
v-model:value="newMemberRemark"
|
||||
placeholder="备注(可选)"
|
||||
style="margin-top: 8px"
|
||||
/>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
style="margin-top: 8px"
|
||||
:loading="createMember.isPending.value"
|
||||
@click="onCreateMember"
|
||||
>
|
||||
创建并选用
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
<a-alert
|
||||
v-if="resultMsg"
|
||||
:type="resultMsg.type"
|
||||
show-icon
|
||||
:message="resultMsg.text"
|
||||
/>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.register-key__note {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user