test key
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import type { APIKey, Group, GroupConfigOption, TaskInfo } from "@/types/models";
|
import type { APIKey, Group, GroupConfigOption, KeyStatus, TaskInfo } from "@/types/models";
|
||||||
import http from "@/utils/http";
|
import http from "@/utils/http";
|
||||||
|
|
||||||
export const keysApi = {
|
export const keysApi = {
|
||||||
@@ -43,11 +43,12 @@ export const keysApi = {
|
|||||||
page: number;
|
page: number;
|
||||||
page_size: number;
|
page_size: number;
|
||||||
key?: string;
|
key?: string;
|
||||||
status?: "active" | "inactive";
|
status?: KeyStatus;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
items: APIKey[];
|
items: APIKey[];
|
||||||
pagination: {
|
pagination: {
|
||||||
total_items: number;
|
total_items: number;
|
||||||
|
total_pages: number;
|
||||||
};
|
};
|
||||||
}> {
|
}> {
|
||||||
const res = await http.get("/keys", { params });
|
const res = await http.get("/keys", { params });
|
||||||
@@ -70,7 +71,7 @@ export const keysApi = {
|
|||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 测试单个密钥
|
// 测试密钥
|
||||||
async testKeys(
|
async testKeys(
|
||||||
group_id: number,
|
group_id: number,
|
||||||
keys_text: string
|
keys_text: string
|
||||||
|
@@ -345,7 +345,7 @@ function copyUrl(url: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-cards-container:deep(.n-card) {
|
.status-cards-container:deep(.n-card) {
|
||||||
width: 160px;
|
max-width: 160px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.status-card-failure .n-card-header__main) {
|
:deep(.status-card-failure .n-card-header__main) {
|
||||||
|
@@ -1,9 +1,19 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { keysApi } from "@/api/keys";
|
import { keysApi } from "@/api/keys";
|
||||||
import type { APIKey, Group } from "@/types/models";
|
import type { APIKey, Group, KeyStatus } from "@/types/models";
|
||||||
import { AddCircleOutline, RemoveCircleOutline } from "@vicons/ionicons5";
|
import {
|
||||||
|
AddCircleOutline,
|
||||||
|
CopyOutline,
|
||||||
|
EyeOffOutline,
|
||||||
|
EyeOutline,
|
||||||
|
RemoveCircleOutline,
|
||||||
|
} from "@vicons/ionicons5";
|
||||||
import { NButton, NDropdown, NEmpty, NIcon, NInput, NSelect, NSpace, NSpin } from "naive-ui";
|
import { NButton, NDropdown, NEmpty, NIcon, NInput, NSelect, NSpace, NSpin } from "naive-ui";
|
||||||
import { computed, ref, watch } from "vue";
|
import { ref, watch } from "vue";
|
||||||
|
|
||||||
|
interface KeyRow extends APIKey {
|
||||||
|
is_visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedGroup: Group | null;
|
selectedGroup: Group | null;
|
||||||
@@ -11,21 +21,20 @@ interface Props {
|
|||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
const keys = ref<APIKey[]>([]);
|
const keys = ref<KeyRow[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
const statusFilter = ref<"all" | "valid" | "invalid">("all");
|
const statusFilter = ref<"all" | "valid" | "invalid">("all");
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
const pageSize = ref(20);
|
const pageSize = ref(9);
|
||||||
const totalKeys = ref(0);
|
const total = ref(0);
|
||||||
|
const totalPages = ref(0);
|
||||||
const totalPages = computed(() => Math.ceil(totalKeys.value / pageSize.value));
|
|
||||||
|
|
||||||
// 状态过滤选项
|
// 状态过滤选项
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ label: "全部", value: "all" },
|
{ label: "全部", value: "all" },
|
||||||
{ label: "有效", value: "valid" },
|
{ label: "有效", value: "active" },
|
||||||
{ label: "无效", value: "invalid" },
|
{ label: "无效", value: "inactive" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 更多操作下拉菜单选项
|
// 更多操作下拉菜单选项
|
||||||
@@ -40,6 +49,9 @@ const moreOptions = [
|
|||||||
{ label: "清空所有无效 Key", key: "clearInvalid", props: { style: { color: "#d03050" } } },
|
{ label: "清空所有无效 Key", key: "clearInvalid", props: { style: { color: "#d03050" } } },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 防抖定时器
|
||||||
|
let searchTimer: number | undefined = undefined;
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.selectedGroup,
|
() => props.selectedGroup,
|
||||||
async newGroup => {
|
async newGroup => {
|
||||||
@@ -51,10 +63,23 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
watch([currentPage, pageSize, statusFilter, searchText], async () => {
|
watch([currentPage, pageSize, statusFilter], async () => {
|
||||||
await loadKeys();
|
await loadKeys();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 处理搜索输入的防抖
|
||||||
|
function handleSearchInput() {
|
||||||
|
// 清除之前的定时器
|
||||||
|
if (searchTimer) {
|
||||||
|
clearTimeout(searchTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchTimer = setTimeout(async () => {
|
||||||
|
currentPage.value = 1; // 搜索时重置到第一页
|
||||||
|
await loadKeys();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
// 处理更多操作菜单
|
// 处理更多操作菜单
|
||||||
function handleMoreAction(key: string) {
|
function handleMoreAction(key: string) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
@@ -80,20 +105,22 @@ function handleMoreAction(key: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadKeys() {
|
async function loadKeys() {
|
||||||
if (!props.selectedGroup) {
|
if (!props.selectedGroup?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const result = await keysApi.getGroupKeys(
|
const result = await keysApi.getGroupKeys({
|
||||||
props.selectedGroup.id,
|
group_id: props.selectedGroup.id,
|
||||||
currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize.value,
|
page_size: pageSize.value,
|
||||||
statusFilter.value === "all" ? undefined : statusFilter.value
|
status: statusFilter.value === "all" ? undefined : (statusFilter.value as KeyStatus),
|
||||||
);
|
key: searchText.value.trim() || undefined,
|
||||||
keys.value = result.data;
|
});
|
||||||
totalKeys.value = result.total;
|
keys.value = result.items as KeyRow[];
|
||||||
|
total.value = result.pagination.total_items;
|
||||||
|
totalPages.value = result.pagination.total_pages;
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
window.$message.error("加载密钥失败");
|
window.$message.error("加载密钥失败");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -108,7 +135,7 @@ function maskKey(key: string): string {
|
|||||||
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
|
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyKey(key: APIKey) {
|
function copyKey(key: KeyRow) {
|
||||||
navigator.clipboard
|
navigator.clipboard
|
||||||
.writeText(key.key_value)
|
.writeText(key.key_value)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -119,26 +146,35 @@ function copyKey(key: APIKey) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function testKey(_key: APIKey) {
|
async function testKey(_key: KeyRow) {
|
||||||
|
if (!props.selectedGroup?.id || !_key.key_value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingMsg = window.$message.info("正在测试密钥...", {
|
||||||
|
duration: 0,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.$message.info("正在测试密钥...");
|
const res = await keysApi.testKeys(props.selectedGroup.id, _key.key_value);
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
const curValid = res?.[0] || {};
|
||||||
const success = Math.random() > 0.3;
|
if (curValid.is_valid) {
|
||||||
if (success) {
|
|
||||||
window.$message.success("密钥测试成功");
|
window.$message.success("密钥测试成功");
|
||||||
} else {
|
} else {
|
||||||
window.$message.error("密钥测试失败: 无效的API密钥");
|
window.$message.error(curValid.error || "密钥测试失败: 无效的API密钥");
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
window.$message.error("测试失败");
|
console.error("测试失败");
|
||||||
|
} finally {
|
||||||
|
loadingMsg.destroy();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleKeyVisibility(key: APIKey) {
|
function toggleKeyVisibility(key: KeyRow) {
|
||||||
window.$message.info(`切换密钥"${maskKey(key.key_value)}"显示状态功能开发中`);
|
key.is_visible = !key.is_visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreKey(key: APIKey) {
|
async function restoreKey(key: KeyRow) {
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
const confirmed = window.confirm(`确定要恢复密钥"${maskKey(key.key_value)}"吗?`);
|
const confirmed = window.confirm(`确定要恢复密钥"${maskKey(key.key_value)}"吗?`);
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
@@ -154,7 +190,7 @@ async function restoreKey(key: APIKey) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteKey(key: APIKey) {
|
async function deleteKey(key: KeyRow) {
|
||||||
// eslint-disable-next-line no-alert
|
// eslint-disable-next-line no-alert
|
||||||
const confirmed = window.confirm(`确定要删除密钥"${maskKey(key.key_value)}"吗?`);
|
const confirmed = window.confirm(`确定要删除密钥"${maskKey(key.key_value)}"吗?`);
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
@@ -186,7 +222,7 @@ function formatRelativeTime(date: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusClass(status: "active" | "inactive") {
|
function getStatusClass(status: KeyStatus): string {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "active":
|
case "active":
|
||||||
return "status-valid";
|
return "status-valid";
|
||||||
@@ -362,6 +398,8 @@ function changePageSize(size: number) {
|
|||||||
placeholder="Key 模糊查询"
|
placeholder="Key 模糊查询"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 180px"
|
style="width: 180px"
|
||||||
|
clearable
|
||||||
|
@input="handleSearchInput"
|
||||||
/>
|
/>
|
||||||
<n-dropdown :options="moreOptions" trigger="click" @select="handleMoreAction">
|
<n-dropdown :options="moreOptions" trigger="click" @select="handleMoreAction">
|
||||||
<n-button size="small" secondary>
|
<n-button size="small" secondary>
|
||||||
@@ -392,16 +430,21 @@ function changePageSize(size: number) {
|
|||||||
<div class="key-section">
|
<div class="key-section">
|
||||||
<n-tag v-if="key.status === 'active'" type="info">有效</n-tag>
|
<n-tag v-if="key.status === 'active'" type="info">有效</n-tag>
|
||||||
<n-tag v-else>无效</n-tag>
|
<n-tag v-else>无效</n-tag>
|
||||||
<span class="key-text" :title="key.key_value">{{ maskKey(key.key_value) }}</span>
|
<n-input
|
||||||
|
class="key-text"
|
||||||
|
:value="key.is_visible ? key.key_value : maskKey(key.key_value)"
|
||||||
|
readonly
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
<div class="quick-actions">
|
<div class="quick-actions">
|
||||||
<n-button size="tiny" text @click="toggleKeyVisibility(key)" title="显示/隐藏">
|
<n-button size="tiny" text @click="toggleKeyVisibility(key)" title="显示/隐藏">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 12px">👁️</span>
|
<n-icon :component="key.is_visible ? EyeOffOutline : EyeOutline" />
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button size="tiny" text @click="copyKey(key)" title="复制">
|
<n-button size="tiny" text @click="copyKey(key)" title="复制">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<span style="font-size: 12px">📋</span>
|
<n-icon :component="CopyOutline" />
|
||||||
</template>
|
</template>
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -420,15 +463,23 @@ function changePageSize(size: number) {
|
|||||||
<strong>{{ key.failure_count }}</strong>
|
<strong>{{ key.failure_count }}</strong>
|
||||||
</span>
|
</span>
|
||||||
<span class="stat-item">
|
<span class="stat-item">
|
||||||
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : "从未使用" }}
|
{{ key.last_used_at ? formatRelativeTime(key.last_used_at) : "未使用" }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="key-actions">
|
<n-button-group class="key-actions">
|
||||||
<n-button type="info" size="tiny" @click="testKey(key)" title="测试密钥">
|
<n-button
|
||||||
|
round
|
||||||
|
tertiary
|
||||||
|
type="info"
|
||||||
|
size="tiny"
|
||||||
|
@click="testKey(key)"
|
||||||
|
title="测试密钥"
|
||||||
|
>
|
||||||
测试
|
测试
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
v-if="key.status !== 'active'"
|
v-if="key.status !== 'active'"
|
||||||
|
tertiary
|
||||||
size="tiny"
|
size="tiny"
|
||||||
@click="restoreKey(key)"
|
@click="restoreKey(key)"
|
||||||
title="恢复密钥"
|
title="恢复密钥"
|
||||||
@@ -436,10 +487,17 @@ function changePageSize(size: number) {
|
|||||||
>
|
>
|
||||||
恢复
|
恢复
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button size="tiny" type="error" @click="deleteKey(key)" title="删除密钥">
|
<n-button
|
||||||
|
round
|
||||||
|
tertiary
|
||||||
|
size="tiny"
|
||||||
|
type="error"
|
||||||
|
@click="deleteKey(key)"
|
||||||
|
title="删除密钥"
|
||||||
|
>
|
||||||
删除
|
删除
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</n-button-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -449,14 +507,14 @@ function changePageSize(size: number) {
|
|||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-container">
|
<div class="pagination-container">
|
||||||
<div class="pagination-info">
|
<div class="pagination-info">
|
||||||
<span>共 {{ totalKeys }} 条记录</span>
|
<span>共 {{ total }} 条记录</span>
|
||||||
<n-select
|
<n-select
|
||||||
v-model:value="pageSize"
|
v-model:value="pageSize"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '10条/页', value: 10 },
|
{ label: '9条/页', value: 9 },
|
||||||
{ label: '20条/页', value: 20 },
|
{ label: '18条/页', value: 18 },
|
||||||
{ label: '50条/页', value: 50 },
|
{ label: '36条/页', value: 36 },
|
||||||
{ label: '100条/页', value: 100 },
|
{ label: '72条/页', value: 72 },
|
||||||
]"
|
]"
|
||||||
size="small"
|
size="small"
|
||||||
style="width: 100px; margin-left: 12px"
|
style="width: 100px; margin-left: 12px"
|
||||||
@@ -717,23 +775,21 @@ function changePageSize(size: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.key-actions {
|
.key-actions {
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
&:deep(.n-button) {
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-text {
|
.key-text {
|
||||||
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #495057;
|
color: #495057;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,9 +1,12 @@
|
|||||||
|
// 密钥状态
|
||||||
|
export type KeyStatus = "active" | "inactive" | undefined;
|
||||||
|
|
||||||
// 数据模型定义
|
// 数据模型定义
|
||||||
export interface APIKey {
|
export interface APIKey {
|
||||||
id: number;
|
id: number;
|
||||||
group_id: number;
|
group_id: number;
|
||||||
key_value: string;
|
key_value: string;
|
||||||
status: "active" | "inactive";
|
status: KeyStatus;
|
||||||
request_count: number;
|
request_count: number;
|
||||||
failure_count: number;
|
failure_count: number;
|
||||||
last_used_at?: string;
|
last_used_at?: string;
|
||||||
|
@@ -5,10 +5,13 @@ import GroupList from "@/components/keys/GroupList.vue";
|
|||||||
import KeyTable from "@/components/keys/KeyTable.vue";
|
import KeyTable from "@/components/keys/KeyTable.vue";
|
||||||
import type { Group } from "@/types/models";
|
import type { Group } from "@/types/models";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted, ref } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
|
||||||
const groups = ref<Group[]>([]);
|
const groups = ref<Group[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const selectedGroup = ref<Group | null>(null);
|
const selectedGroup = ref<Group | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadGroups();
|
await loadGroups();
|
||||||
@@ -18,24 +21,33 @@ async function loadGroups() {
|
|||||||
try {
|
try {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
groups.value = await keysApi.getGroups();
|
groups.value = await keysApi.getGroups();
|
||||||
// 默认选择第一个分组
|
// 选择默认分组
|
||||||
if (groups.value.length > 0 && !selectedGroup.value) {
|
if (groups.value.length > 0 && !selectedGroup.value) {
|
||||||
selectedGroup.value = groups.value[0];
|
const groupId = route.query.groupId;
|
||||||
|
const found = groups.value.find(g => String(g.id) === String(groupId));
|
||||||
|
if (found) {
|
||||||
|
selectedGroup.value = found;
|
||||||
|
} else {
|
||||||
|
handleGroupSelect(groups.value[0]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGroupSelect(group: Group) {
|
function handleGroupSelect(group: Group | null) {
|
||||||
selectedGroup.value = group;
|
selectedGroup.value = group || null;
|
||||||
|
if (String(group?.id) !== String(route.query.groupId)) {
|
||||||
|
router.push({ name: "keys", query: { groupId: group?.id || "" } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGroupRefresh() {
|
async function handleGroupRefresh() {
|
||||||
await loadGroups();
|
await loadGroups();
|
||||||
if (selectedGroup.value) {
|
if (selectedGroup.value) {
|
||||||
// 重新加载当前选中的分组信息
|
// 重新加载当前选中的分组信息
|
||||||
selectedGroup.value = groups.value.find(g => g.id === selectedGroup.value?.id) || null;
|
handleGroupSelect(groups.value.find(g => g.id === selectedGroup.value?.id) || null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +57,7 @@ function handleGroupDelete(deletedGroup: Group) {
|
|||||||
|
|
||||||
// 如果删除的是当前选中的分组,则切换到第一个分组
|
// 如果删除的是当前选中的分组,则切换到第一个分组
|
||||||
if (selectedGroup.value?.id === deletedGroup.id) {
|
if (selectedGroup.value?.id === deletedGroup.id) {
|
||||||
selectedGroup.value = groups.value.length > 0 ? groups.value[0] : null;
|
handleGroupSelect(groups.value.length > 0 ? groups.value[0] : null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
Reference in New Issue
Block a user