Skip to content

Commit 8f31c8f

Browse files
authored
Merge pull request #68 from tbphp/feat-generate-key
feat: 代理密钥优化-自动生成随机密钥和复制
2 parents 2fcd9b8 + ed2d9f0 commit 8f31c8f

File tree

5 files changed

+293
-9
lines changed

5 files changed

+293
-9
lines changed
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<script setup lang="ts">
2+
import { copy } from "@/utils/clipboard";
3+
import { Copy, Key } from "@vicons/ionicons5";
4+
import { NButton, NIcon, NInput, NInputNumber, NModal, NSpace, useMessage } from "naive-ui";
5+
import { ref } from "vue";
6+
7+
interface Props {
8+
modelValue: string;
9+
placeholder?: string;
10+
size?: "small" | "medium" | "large";
11+
}
12+
13+
interface Emits {
14+
(e: "update:modelValue", value: string): void;
15+
}
16+
17+
const props = withDefaults(defineProps<Props>(), {
18+
placeholder: "多个密钥请用英文逗号 , 分隔",
19+
size: "small",
20+
});
21+
22+
const emit = defineEmits<Emits>();
23+
24+
const message = useMessage();
25+
26+
// 密钥生成弹窗相关
27+
const showKeyGeneratorModal = ref(false);
28+
const keyCount = ref(1);
29+
const isGenerating = ref(false);
30+
31+
// 生成随机字符串
32+
function generateRandomString(length: number): string {
33+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
34+
let result = "";
35+
for (let i = 0; i < length; i++) {
36+
result += chars.charAt(Math.floor(Math.random() * chars.length));
37+
}
38+
return result;
39+
}
40+
41+
// 生成密钥
42+
function generateKeys(): string[] {
43+
const keys: string[] = [];
44+
for (let i = 0; i < keyCount.value; i++) {
45+
keys.push(`sk-${generateRandomString(48)}`);
46+
}
47+
return keys;
48+
}
49+
50+
// 打开密钥生成器弹窗
51+
function openKeyGenerator() {
52+
showKeyGeneratorModal.value = true;
53+
keyCount.value = 1;
54+
}
55+
56+
// 确认生成密钥
57+
function confirmGenerateKeys() {
58+
if (isGenerating.value) {
59+
return;
60+
}
61+
62+
try {
63+
isGenerating.value = true;
64+
const newKeys = generateKeys();
65+
const currentValue = props.modelValue || "";
66+
67+
let updatedValue = currentValue.trim();
68+
69+
// 处理逗号兼容情况
70+
if (updatedValue && !updatedValue.endsWith(",")) {
71+
updatedValue += ",";
72+
}
73+
74+
// 添加新生成的密钥
75+
if (updatedValue) {
76+
updatedValue += newKeys.join(",");
77+
} else {
78+
updatedValue = newKeys.join(",");
79+
}
80+
81+
emit("update:modelValue", updatedValue);
82+
showKeyGeneratorModal.value = false;
83+
84+
message.success(`成功生成 ${keyCount.value} 个密钥`);
85+
} finally {
86+
isGenerating.value = false;
87+
}
88+
}
89+
90+
// 复制代理密钥
91+
async function copyProxyKeys() {
92+
const proxyKeys = props.modelValue || "";
93+
if (!proxyKeys.trim()) {
94+
message.warning("暂无密钥可复制");
95+
return;
96+
}
97+
98+
// 将逗号分隔的密钥转换为换行分隔
99+
const formattedKeys = proxyKeys
100+
.split(",")
101+
.map(key => key.trim())
102+
.filter(key => key.length > 0)
103+
.join("\n");
104+
105+
const success = await copy(formattedKeys);
106+
if (success) {
107+
message.success("密钥已复制到剪贴板");
108+
} else {
109+
message.error("复制失败,请手动复制");
110+
}
111+
}
112+
113+
// 处理输入框值变化
114+
function handleInput(value: string) {
115+
emit("update:modelValue", value);
116+
}
117+
</script>
118+
119+
<template>
120+
<div class="proxy-keys-input">
121+
<n-input
122+
:value="modelValue"
123+
:placeholder="placeholder"
124+
clearable
125+
:size="size"
126+
@update:value="handleInput"
127+
>
128+
<template #suffix>
129+
<n-space :size="4" :wrap-item="false">
130+
<n-button text type="primary" :size="size" @click="openKeyGenerator">
131+
<template #icon>
132+
<n-icon :component="Key" />
133+
</template>
134+
生成
135+
</n-button>
136+
<n-button text type="tertiary" :size="size" @click="copyProxyKeys" style="opacity: 0.7">
137+
<template #icon>
138+
<n-icon :component="Copy" />
139+
</template>
140+
复制
141+
</n-button>
142+
</n-space>
143+
</template>
144+
</n-input>
145+
146+
<!-- 密钥生成器弹窗 -->
147+
<n-modal
148+
v-model:show="showKeyGeneratorModal"
149+
preset="dialog"
150+
title="生成代理密钥"
151+
positive-text="确认生成"
152+
negative-text="取消"
153+
:positive-button-props="{ loading: isGenerating }"
154+
@positive-click="confirmGenerateKeys"
155+
>
156+
<n-space vertical :size="16">
157+
<div>
158+
<p style="margin: 0 0 8px 0; color: #666; font-size: 14px">
159+
请输入要生成的密钥数量(最大100个):
160+
</p>
161+
<n-input-number
162+
v-model:value="keyCount"
163+
:min="1"
164+
:max="100"
165+
placeholder="请输入数量"
166+
style="width: 100%"
167+
:disabled="isGenerating"
168+
/>
169+
</div>
170+
<div style="color: #999; font-size: 12px; line-height: 1.4">
171+
<p>生成的密钥将会插入到当前输入框内容的后面,以逗号分隔</p>
172+
</div>
173+
</n-space>
174+
</n-modal>
175+
</div>
176+
</template>
177+
178+
<style scoped>
179+
.proxy-keys-input {
180+
width: 100%;
181+
}
182+
</style>

web/src/components/keys/GroupFormModal.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { keysApi } from "@/api/keys";
33
import { settingsApi } from "@/api/settings";
4+
import ProxyKeysInput from "@/components/common/ProxyKeysInput.vue";
45
import type { Group, GroupConfigOption, UpstreamInfo } from "@/types/models";
56
import { Add, Close, HelpCircleOutline, Remove } from "@vicons/ionicons5";
67
import {
@@ -610,9 +611,10 @@ async function handleSubmit() {
610611
</n-tooltip>
611612
</div>
612613
</template>
613-
<n-input
614-
v-model:value="formData.proxy_keys"
614+
<proxy-keys-input
615+
v-model="formData.proxy_keys"
615616
placeholder="多个密钥请用英文逗号 , 分隔"
617+
size="medium"
616618
/>
617619
</n-form-item>
618620

web/src/components/keys/GroupInfoCard.vue

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { keysApi } from "@/api/keys";
33
import type { Group, GroupConfigOption, GroupStatsResponse } from "@/types/models";
44
import { appState } from "@/utils/app-state";
55
import { copy } from "@/utils/clipboard";
6-
import { getGroupDisplayName } from "@/utils/display";
7-
import { Pencil, Trash } from "@vicons/ionicons5";
6+
import { getGroupDisplayName, maskProxyKeys } from "@/utils/display";
7+
import { CopyOutline, EyeOffOutline, EyeOutline, Pencil, Trash } from "@vicons/ionicons5";
88
import {
99
NButton,
10+
NButtonGroup,
1011
NCard,
1112
NCollapse,
1213
NCollapseItem,
@@ -20,7 +21,7 @@ import {
2021
NTooltip,
2122
useDialog,
2223
} from "naive-ui";
23-
import { onMounted, ref, watch } from "vue";
24+
import { computed, onMounted, ref, watch } from "vue";
2425
import GroupFormModal from "./GroupFormModal.vue";
2526
2627
interface Props {
@@ -43,6 +44,30 @@ const showEditModal = ref(false);
4344
const delLoading = ref(false);
4445
const expandedName = ref<string[]>([]);
4546
const configOptions = ref<GroupConfigOption[]>([]);
47+
const showProxyKeys = ref(false);
48+
49+
const proxyKeysDisplay = computed(() => {
50+
if (!props.group?.proxy_keys) {
51+
return "-";
52+
}
53+
if (showProxyKeys.value) {
54+
return props.group.proxy_keys.replace(/,/g, "\n");
55+
}
56+
return maskProxyKeys(props.group.proxy_keys);
57+
});
58+
59+
async function copyProxyKeys() {
60+
if (!props.group?.proxy_keys) {
61+
return;
62+
}
63+
const keysToCopy = props.group.proxy_keys.replace(/,/g, "\n");
64+
const success = await copy(keysToCopy);
65+
if (success) {
66+
window.$message.success("代理密钥已复制到剪贴板");
67+
} else {
68+
window.$message.error("复制失败");
69+
}
70+
}
4671
4772
onMounted(() => {
4873
loadStats();
@@ -385,10 +410,41 @@ function resetPage() {
385410
{{ group?.validation_endpoint }}
386411
</n-form-item>
387412
</n-grid-item>
388-
<n-grid-item>
413+
<n-grid-item :span="2">
414+
<n-form-item label="代理密钥:">
415+
<div class="proxy-keys-content">
416+
<span class="key-text">{{ proxyKeysDisplay }}</span>
417+
<n-button-group size="small" class="key-actions" v-if="group?.proxy_keys">
418+
<n-tooltip trigger="hover">
419+
<template #trigger>
420+
<n-button quaternary circle @click="showProxyKeys = !showProxyKeys">
421+
<template #icon>
422+
<n-icon
423+
:component="showProxyKeys ? EyeOffOutline : EyeOutline"
424+
/>
425+
</template>
426+
</n-button>
427+
</template>
428+
{{ showProxyKeys ? "隐藏密钥" : "显示密钥" }}
429+
</n-tooltip>
430+
<n-tooltip trigger="hover">
431+
<template #trigger>
432+
<n-button quaternary circle @click="copyProxyKeys">
433+
<template #icon>
434+
<n-icon :component="CopyOutline" />
435+
</template>
436+
</n-button>
437+
</template>
438+
复制密钥
439+
</n-tooltip>
440+
</n-button-group>
441+
</div>
442+
</n-form-item>
443+
</n-grid-item>
444+
<n-grid-item :span="2">
389445
<n-form-item label="描述:">
390446
<div class="description-content">
391-
{{ group?.description }}
447+
{{ group?.description || "-" }}
392448
</div>
393449
</n-form-item>
394450
</n-grid-item>
@@ -613,6 +669,28 @@ function resetPage() {
613669
color: #374151;
614670
}
615671
672+
.proxy-keys-content {
673+
display: flex;
674+
align-items: flex-start;
675+
justify-content: space-between;
676+
width: 100%;
677+
gap: 8px;
678+
}
679+
680+
.key-text {
681+
flex-grow: 1;
682+
font-family: monospace;
683+
white-space: pre-wrap;
684+
word-break: break-all;
685+
line-height: 1.5;
686+
padding-top: 4px; /* Align with buttons */
687+
color: #374151;
688+
}
689+
690+
.key-actions {
691+
flex-shrink: 0;
692+
}
693+
616694
/* 配置项tooltip样式 */
617695
.config-label {
618696
display: inline-flex;

web/src/utils/display.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,18 @@ export function maskKey(key: string): string {
4949
}
5050
return `${key.substring(0, 4)}...${key.substring(key.length - 4)}`;
5151
}
52+
53+
/**
54+
* Masks a comma-separated string of keys.
55+
* @param keys The comma-separated keys string.
56+
* @returns The masked keys string.
57+
*/
58+
export function maskProxyKeys(keys: string): string {
59+
if (!keys) {
60+
return "";
61+
}
62+
return keys
63+
.split(",")
64+
.map(key => maskKey(key.trim()))
65+
.join(", ");
66+
}

web/src/views/Settings.vue

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { settingsApi, type SettingCategory } from "@/api/settings";
3+
import ProxyKeysInput from "@/components/common/ProxyKeysInput.vue";
34
import { HelpCircle, Save } from "@vicons/ionicons5";
45
import {
56
NButton,
@@ -72,11 +73,11 @@ async function handleSubmit() {
7273
hoverable
7374
bordered
7475
>
75-
<n-grid :x-gap="24" :y-gap="0" responsive="screen" cols="1 s:2 m:2 l:3 xl:4">
76+
<n-grid :x-gap="36" :y-gap="0" responsive="screen" cols="1 s:2 m:2 l:3 xl:3">
7677
<n-grid-item
7778
v-for="item in category.settings"
7879
:key="item.key"
79-
:span="item.key === 'proxy_keys' ? 4 : 1"
80+
:span="item.key === 'proxy_keys' ? 3 : 1"
8081
>
8182
<n-form-item
8283
:path="item.key"
@@ -109,6 +110,12 @@ async function handleSubmit() {
109110
style="width: 100%"
110111
size="small"
111112
/>
113+
<proxy-keys-input
114+
v-else-if="item.key === 'proxy_keys'"
115+
v-model="form[item.key] as string"
116+
placeholder="请输入内容"
117+
size="small"
118+
/>
112119
<n-input
113120
v-else
114121
v-model:value="form[item.key] as string"

0 commit comments

Comments
 (0)