Merge pull request #68 from tbphp/feat-generate-key
feat: 代理密钥优化-自动生成随机密钥和复制
This commit is contained in:
182
web/src/components/common/ProxyKeysInput.vue
Normal file
182
web/src/components/common/ProxyKeysInput.vue
Normal file
@@ -0,0 +1,182 @@
|
||||
<script setup lang="ts">
|
||||
import { copy } from "@/utils/clipboard";
|
||||
import { Copy, Key } from "@vicons/ionicons5";
|
||||
import { NButton, NIcon, NInput, NInputNumber, NModal, NSpace, useMessage } from "naive-ui";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
placeholder?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "update:modelValue", value: string): void;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
placeholder: "多个密钥请用英文逗号 , 分隔",
|
||||
size: "small",
|
||||
});
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
// 密钥生成弹窗相关
|
||||
const showKeyGeneratorModal = ref(false);
|
||||
const keyCount = ref(1);
|
||||
const isGenerating = ref(false);
|
||||
|
||||
// 生成随机字符串
|
||||
function generateRandomString(length: number): string {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
let result = "";
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// 生成密钥
|
||||
function generateKeys(): string[] {
|
||||
const keys: string[] = [];
|
||||
for (let i = 0; i < keyCount.value; i++) {
|
||||
keys.push(`sk-${generateRandomString(48)}`);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// 打开密钥生成器弹窗
|
||||
function openKeyGenerator() {
|
||||
showKeyGeneratorModal.value = true;
|
||||
keyCount.value = 1;
|
||||
}
|
||||
|
||||
// 确认生成密钥
|
||||
function confirmGenerateKeys() {
|
||||
if (isGenerating.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isGenerating.value = true;
|
||||
const newKeys = generateKeys();
|
||||
const currentValue = props.modelValue || "";
|
||||
|
||||
let updatedValue = currentValue.trim();
|
||||
|
||||
// 处理逗号兼容情况
|
||||
if (updatedValue && !updatedValue.endsWith(",")) {
|
||||
updatedValue += ",";
|
||||
}
|
||||
|
||||
// 添加新生成的密钥
|
||||
if (updatedValue) {
|
||||
updatedValue += newKeys.join(",");
|
||||
} else {
|
||||
updatedValue = newKeys.join(",");
|
||||
}
|
||||
|
||||
emit("update:modelValue", updatedValue);
|
||||
showKeyGeneratorModal.value = false;
|
||||
|
||||
message.success(`成功生成 ${keyCount.value} 个密钥`);
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制代理密钥
|
||||
async function copyProxyKeys() {
|
||||
const proxyKeys = props.modelValue || "";
|
||||
if (!proxyKeys.trim()) {
|
||||
message.warning("暂无密钥可复制");
|
||||
return;
|
||||
}
|
||||
|
||||
// 将逗号分隔的密钥转换为换行分隔
|
||||
const formattedKeys = proxyKeys
|
||||
.split(",")
|
||||
.map(key => key.trim())
|
||||
.filter(key => key.length > 0)
|
||||
.join("\n");
|
||||
|
||||
const success = await copy(formattedKeys);
|
||||
if (success) {
|
||||
message.success("密钥已复制到剪贴板");
|
||||
} else {
|
||||
message.error("复制失败,请手动复制");
|
||||
}
|
||||
}
|
||||
|
||||
// 处理输入框值变化
|
||||
function handleInput(value: string) {
|
||||
emit("update:modelValue", value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="proxy-keys-input">
|
||||
<n-input
|
||||
:value="modelValue"
|
||||
:placeholder="placeholder"
|
||||
clearable
|
||||
:size="size"
|
||||
@update:value="handleInput"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-space :size="4" :wrap-item="false">
|
||||
<n-button text type="primary" :size="size" @click="openKeyGenerator">
|
||||
<template #icon>
|
||||
<n-icon :component="Key" />
|
||||
</template>
|
||||
生成
|
||||
</n-button>
|
||||
<n-button text type="tertiary" :size="size" @click="copyProxyKeys" style="opacity: 0.7">
|
||||
<template #icon>
|
||||
<n-icon :component="Copy" />
|
||||
</template>
|
||||
复制
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<!-- 密钥生成器弹窗 -->
|
||||
<n-modal
|
||||
v-model:show="showKeyGeneratorModal"
|
||||
preset="dialog"
|
||||
title="生成代理密钥"
|
||||
positive-text="确认生成"
|
||||
negative-text="取消"
|
||||
:positive-button-props="{ loading: isGenerating }"
|
||||
@positive-click="confirmGenerateKeys"
|
||||
>
|
||||
<n-space vertical :size="16">
|
||||
<div>
|
||||
<p style="margin: 0 0 8px 0; color: #666; font-size: 14px">
|
||||
请输入要生成的密钥数量(最大100个):
|
||||
</p>
|
||||
<n-input-number
|
||||
v-model:value="keyCount"
|
||||
:min="1"
|
||||
:max="100"
|
||||
placeholder="请输入数量"
|
||||
style="width: 100%"
|
||||
:disabled="isGenerating"
|
||||
/>
|
||||
</div>
|
||||
<div style="color: #999; font-size: 12px; line-height: 1.4">
|
||||
<p>生成的密钥将会插入到当前输入框内容的后面,以逗号分隔</p>
|
||||
</div>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.proxy-keys-input {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { keysApi } from "@/api/keys";
|
||||
import { settingsApi } from "@/api/settings";
|
||||
import ProxyKeysInput from "@/components/common/ProxyKeysInput.vue";
|
||||
import type { Group, GroupConfigOption, UpstreamInfo } from "@/types/models";
|
||||
import { Add, Close, HelpCircleOutline, Remove } from "@vicons/ionicons5";
|
||||
import {
|
||||
@@ -610,9 +611,10 @@ async function handleSubmit() {
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<n-input
|
||||
v-model:value="formData.proxy_keys"
|
||||
<proxy-keys-input
|
||||
v-model="formData.proxy_keys"
|
||||
placeholder="多个密钥请用英文逗号 , 分隔"
|
||||
size="medium"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
|
@@ -3,10 +3,11 @@ import { keysApi } from "@/api/keys";
|
||||
import type { Group, GroupConfigOption, GroupStatsResponse } from "@/types/models";
|
||||
import { appState } from "@/utils/app-state";
|
||||
import { copy } from "@/utils/clipboard";
|
||||
import { getGroupDisplayName } from "@/utils/display";
|
||||
import { Pencil, Trash } from "@vicons/ionicons5";
|
||||
import { getGroupDisplayName, maskProxyKeys } from "@/utils/display";
|
||||
import { CopyOutline, EyeOffOutline, EyeOutline, Pencil, Trash } from "@vicons/ionicons5";
|
||||
import {
|
||||
NButton,
|
||||
NButtonGroup,
|
||||
NCard,
|
||||
NCollapse,
|
||||
NCollapseItem,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
NTooltip,
|
||||
useDialog,
|
||||
} from "naive-ui";
|
||||
import { onMounted, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import GroupFormModal from "./GroupFormModal.vue";
|
||||
|
||||
interface Props {
|
||||
@@ -43,6 +44,30 @@ const showEditModal = ref(false);
|
||||
const delLoading = ref(false);
|
||||
const expandedName = ref<string[]>([]);
|
||||
const configOptions = ref<GroupConfigOption[]>([]);
|
||||
const showProxyKeys = ref(false);
|
||||
|
||||
const proxyKeysDisplay = computed(() => {
|
||||
if (!props.group?.proxy_keys) {
|
||||
return "-";
|
||||
}
|
||||
if (showProxyKeys.value) {
|
||||
return props.group.proxy_keys.replace(/,/g, "\n");
|
||||
}
|
||||
return maskProxyKeys(props.group.proxy_keys);
|
||||
});
|
||||
|
||||
async function copyProxyKeys() {
|
||||
if (!props.group?.proxy_keys) {
|
||||
return;
|
||||
}
|
||||
const keysToCopy = props.group.proxy_keys.replace(/,/g, "\n");
|
||||
const success = await copy(keysToCopy);
|
||||
if (success) {
|
||||
window.$message.success("代理密钥已复制到剪贴板");
|
||||
} else {
|
||||
window.$message.error("复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStats();
|
||||
@@ -385,10 +410,41 @@ function resetPage() {
|
||||
{{ group?.validation_endpoint }}
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item>
|
||||
<n-grid-item :span="2">
|
||||
<n-form-item label="代理密钥:">
|
||||
<div class="proxy-keys-content">
|
||||
<span class="key-text">{{ proxyKeysDisplay }}</span>
|
||||
<n-button-group size="small" class="key-actions" v-if="group?.proxy_keys">
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle @click="showProxyKeys = !showProxyKeys">
|
||||
<template #icon>
|
||||
<n-icon
|
||||
:component="showProxyKeys ? EyeOffOutline : EyeOutline"
|
||||
/>
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
{{ showProxyKeys ? "隐藏密钥" : "显示密钥" }}
|
||||
</n-tooltip>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<n-button quaternary circle @click="copyProxyKeys">
|
||||
<template #icon>
|
||||
<n-icon :component="CopyOutline" />
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
复制密钥
|
||||
</n-tooltip>
|
||||
</n-button-group>
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
<n-grid-item :span="2">
|
||||
<n-form-item label="描述:">
|
||||
<div class="description-content">
|
||||
{{ group?.description }}
|
||||
{{ group?.description || "-" }}
|
||||
</div>
|
||||
</n-form-item>
|
||||
</n-grid-item>
|
||||
@@ -613,6 +669,28 @@ function resetPage() {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.proxy-keys-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.key-text {
|
||||
flex-grow: 1;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
padding-top: 4px; /* Align with buttons */
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.key-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 配置项tooltip样式 */
|
||||
.config-label {
|
||||
display: inline-flex;
|
||||
|
@@ -49,3 +49,18 @@ export function maskKey(key: string): string {
|
||||
}
|
||||
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Masks a comma-separated string of keys.
|
||||
* @param keys The comma-separated keys string.
|
||||
* @returns The masked keys string.
|
||||
*/
|
||||
export function maskProxyKeys(keys: string): string {
|
||||
if (!keys) {
|
||||
return "";
|
||||
}
|
||||
return keys
|
||||
.split(",")
|
||||
.map(key => maskKey(key.trim()))
|
||||
.join(", ");
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { settingsApi, type SettingCategory } from "@/api/settings";
|
||||
import ProxyKeysInput from "@/components/common/ProxyKeysInput.vue";
|
||||
import { HelpCircle, Save } from "@vicons/ionicons5";
|
||||
import {
|
||||
NButton,
|
||||
@@ -72,11 +73,11 @@ async function handleSubmit() {
|
||||
hoverable
|
||||
bordered
|
||||
>
|
||||
<n-grid :x-gap="24" :y-gap="0" responsive="screen" cols="1 s:2 m:2 l:3 xl:4">
|
||||
<n-grid :x-gap="36" :y-gap="0" responsive="screen" cols="1 s:2 m:2 l:3 xl:3">
|
||||
<n-grid-item
|
||||
v-for="item in category.settings"
|
||||
:key="item.key"
|
||||
:span="item.key === 'proxy_keys' ? 4 : 1"
|
||||
:span="item.key === 'proxy_keys' ? 3 : 1"
|
||||
>
|
||||
<n-form-item
|
||||
:path="item.key"
|
||||
@@ -109,6 +110,12 @@ async function handleSubmit() {
|
||||
style="width: 100%"
|
||||
size="small"
|
||||
/>
|
||||
<proxy-keys-input
|
||||
v-else-if="item.key === 'proxy_keys'"
|
||||
v-model="form[item.key] as string"
|
||||
placeholder="请输入内容"
|
||||
size="small"
|
||||
/>
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="form[item.key] as string"
|
||||
|
Reference in New Issue
Block a user