From 6f04bbf1c970fd40ce1987e051937bcfb6846d15 Mon Sep 17 00:00:00 2001 From: Gsh <15170702455@163.com> Date: Sat, 14 Feb 2026 22:40:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=89=B9=E9=87=8F=E5=88=9B=E5=BB=BAapik?= =?UTF-8?q?ey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/APIKeyManagement.vue | 38 + .../components/TokenFormDialog.vue | 655 +++++++++++++++++- 2 files changed, 691 insertions(+), 2 deletions(-) diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue index 965c6cbe..b32f0ad4 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/APIKeyManagement.vue @@ -46,6 +46,9 @@ const currentFormData = ref({ }); const router = useRouter(); +// TokenFormDialog 组件引用 +const tokenFormDialogRef = ref | null>(null); + // 移动端检测 const isMobile = ref(false); @@ -165,6 +168,8 @@ async function handleFormSubmit(data: TokenFormData) { try { loading.value = true; + + // 单个创建或编辑 const submitData = { id: data.id, name: data.name, @@ -193,6 +198,37 @@ async function handleFormSubmit(data: TokenFormData) { } } +// 处理批量创建 +async function handleBatchCreate( + items: TokenFormData[], + onProgress: (index: number, status: 'pending' | 'creating' | 'success' | 'failed', error?: string) => void +) { + // 并发创建所有 token + const promises = items.map(async (item, index) => { + try { + onProgress(index, 'creating'); + await createToken({ + name: item.name, + expireTime: item.expireTime || null, + premiumQuotaLimit: item.premiumQuotaLimit || null, + }); + onProgress(index, 'success'); + } catch (error: any) { + console.error(`创建 "${item.name}" 失败:`, error); + onProgress(index, 'failed', error?.message || '创建失败'); + throw error; + } + }); + + await Promise.allSettled(promises); + + // 创建完成后刷新列表 + await fetchTokenList(); + + // 调用子组件的完成方法 + tokenFormDialogRef.value?.completeBatchCreate(); +} + // 删除Token(带防抖) async function handleDelete(row: TokenItem) { if (operatingTokenId.value === row.id) @@ -752,10 +788,12 @@ onUnmounted(() => { diff --git a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue index 4c10e2cc..df3999a7 100644 --- a/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue +++ b/Yi.Ai.Vue3/src/components/userPersonalCenter/components/TokenFormDialog.vue @@ -2,6 +2,18 @@ import { ElMessage } from 'element-plus'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; +// 最大批量创建数量 +const MAX_BATCH_CREATE_COUNT = 10; + +// 创建状态类型 +type CreateStatus = 'pending' | 'creating' | 'success' | 'failed'; + +interface CreateItemStatus { + name: string; + status: CreateStatus; + error?: string; +} + interface TokenFormData { id?: string; name: string; @@ -30,7 +42,8 @@ const props = withDefaults(defineProps(), { const emit = defineEmits<{ 'update:visible': [value: boolean]; - 'confirm': [data: TokenFormData]; + 'confirm': [data: TokenFormData | TokenFormData[]]; + 'batchCreate': [items: TokenFormData[], onProgress: (index: number, status: CreateStatus, error?: string) => void]; }>(); const localFormData = ref({ @@ -45,6 +58,84 @@ const neverExpire = ref(false); // 永不过期开关 const unlimitedQuota = ref(false); // 无限制额度开关 const isEnableLog = ref(false); // 是否启用请求日志(只读) +// 批量创建相关 +const batchCount = ref(1); // 批量创建数量,默认1 +const batchNamesText = ref(''); // 批量创建的名称文本(换行分隔) +const showPreviewDialog = ref(false); // 是否显示预览对话框 +const previewNames = ref([]); // 预览的名称列表 +const createStatusList = ref([]); // 创建状态列表 +const isCreating = ref(false); // 是否正在创建 + +// 解析批量名称文本,返回有效的名称数组 +function parseBatchNames(): string[] { + return batchNamesText.value + .split('\n') + .map(line => line.trim()) + .filter(name => name.length > 0); +} + +// 生成默认名称列表 +function generateDefaultNames(count: number): string[] { + const baseName = localFormData.value.name.trim() || 'API密钥'; + const names: string[] = []; + for (let i = 1; i <= count; i++) { + names.push(`${baseName}${i}`); + } + return names; +} + +// 构建最终的名称列表(以用户选择的数量为准) +function buildFinalNames(): string[] { + const targetCount = batchCount.value; + const parsedNames = parseBatchNames(); + const result: string[] = []; + + // 先使用用户粘贴的名称 + for (let i = 0; i < Math.min(parsedNames.length, targetCount); i++) { + result.push(parsedNames[i]); + } + + // 如果不够,用默认规则补齐 + if (result.length < targetCount) { + const baseName = localFormData.value.name.trim() || 'API密钥'; + for (let i = result.length + 1; i <= targetCount; i++) { + result.push(`${baseName}${i}`); + } + } + + return result; +} + +// 监听 batchCount 变化,自动生成名称 +watch(batchCount, (count) => { + if (count > 1 && props.mode === 'create') { + // 如果当前文本框的名称数量小于新数量,自动补齐 + const parsedNames = parseBatchNames(); + if (parsedNames.length < count) { + const baseName = localFormData.value.name.trim() || 'API密钥'; + const names: string[] = [...parsedNames]; + for (let i = names.length + 1; i <= count; i++) { + names.push(`${baseName}${i}`); + } + batchNamesText.value = names.join('\n'); + } + } else { + batchNamesText.value = ''; + } +}); + +// 监听 baseName 变化,当批量创建时自动更新名称 +watch(() => localFormData.value.name, (newName) => { + if (batchCount.value > 1 && props.mode === 'create') { + const baseName = newName.trim() || 'API密钥'; + const names: string[] = []; + for (let i = 1; i <= batchCount.value; i++) { + names.push(`${baseName}${i}`); + } + batchNamesText.value = names.join('\n'); + } +}); + // 移动端检测 const isMobile = ref(false); @@ -117,6 +208,8 @@ watch(() => props.visible, (newVal) => { premiumQuotaLimit: displayValue, quotaUnit: unit, }; + // 编辑模式禁用批量创建 + batchCount.value = 1; } else { // 新增模式:重置表单 @@ -128,6 +221,7 @@ watch(() => props.visible, (newVal) => { }; neverExpire.value = false; unlimitedQuota.value = false; + batchCount.value = 1; } submitting.value = false; } @@ -156,6 +250,40 @@ function handleClose() { // 确认提交 async function handleConfirm() { + // 批量创建模式 + if (props.mode === 'create' && batchCount.value > 1) { + if (!neverExpire.value && !localFormData.value.expireTime) { + ElMessage.warning('请选择过期时间'); + return; + } + + if (!unlimitedQuota.value && localFormData.value.premiumQuotaLimit <= 0) { + ElMessage.warning('请输入有效的配额限制'); + return; + } + + // 构建最终的名称列表 + const finalNames = buildFinalNames(); + + // 检查是否有重复名称 + const uniqueNames = new Set(finalNames); + if (uniqueNames.size !== finalNames.length) { + ElMessage.warning('存在重复的API密钥名称,请检查'); + return; + } + + // 显示预览对话框 + previewNames.value = finalNames; + // 初始化状态列表为 pending,显示在预览对话框中 + createStatusList.value = finalNames.map(name => ({ + name, + status: 'pending' as CreateStatus, + })); + showPreviewDialog.value = true; + return; + } + + // 单个创建或编辑模式 if (!localFormData.value.name.trim()) { ElMessage.warning('请输入API密钥名称'); return; @@ -194,7 +322,115 @@ async function handleConfirm() { } } +// 预览对话框确认提交 +async function handlePreviewConfirm() { + if (!neverExpire.value && !localFormData.value.expireTime) { + ElMessage.warning('请选择过期时间'); + return; + } + + if (!unlimitedQuota.value && localFormData.value.premiumQuotaLimit <= 0) { + ElMessage.warning('请输入有效的配额限制'); + return; + } + + // 先初始化创建状态为 pending + createStatusList.value = previewNames.value.map(name => ({ + name, + status: 'pending' as CreateStatus, + })); + + // 然后设置为创建中状态,触发 UI 更新显示状态列 + isCreating.value = true; + + // 将展示值转换为实际值 + let actualQuota = null; + if (!unlimitedQuota.value) { + const unit = quotaUnitOptions.find(u => u.value === localFormData.value.quotaUnit); + actualQuota = localFormData.value.premiumQuotaLimit * (unit?.multiplier || 1); + } + + // 生成多个提交数据 + const submitDataList: TokenFormData[] = previewNames.value.map(name => ({ + ...localFormData.value, + name, + expireTime: neverExpire.value ? '' : localFormData.value.expireTime, + premiumQuotaLimit: actualQuota, + })); + + // 进度回调函数 + const onProgress = (index: number, status: CreateStatus, error?: string) => { + if (createStatusList.value[index]) { + createStatusList.value[index].status = status; + if (error) { + createStatusList.value[index].error = error; + } + } + }; + + emit('batchCreate', submitDataList, onProgress); +} + +// 预览对话框取消 +function handlePreviewCancel() { + if (isCreating.value) return; + showPreviewDialog.value = false; + previewNames.value = []; + createStatusList.value = []; + // 同时关闭主表单对话框 + emit('update:visible', false); +} + +// 完成批量创建(由父组件调用) +function completeBatchCreate() { + isCreating.value = false; + showPreviewDialog.value = false; + previewNames.value = []; + createStatusList.value = []; + submitting.value = false; +} + +// 暴露给父组件的方法 +defineExpose({ + completeBatchCreate, +}); + +// 获取创建进度统计 +const createProgress = computed(() => { + const total = createStatusList.value.length; + const success = createStatusList.value.filter(s => s.status === 'success').length; + const failed = createStatusList.value.filter(s => s.status === 'failed').length; + const pending = createStatusList.value.filter(s => s.status === 'pending' || s.status === 'creating').length; + return { total, success, failed, pending }; +}); + +// 获取预览对话框标题 +const previewDialogTitle = computed(() => { + if (isCreating.value) { + const { success, failed, pending } = createProgress.value; + if (pending > 0) { + return `创建中... (${success}/${createProgress.value.total})`; + } + return `创建完成`; + } + return '批量创建预览'; +}); + const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥' : '编辑 API密钥'); + +// 格式化日期时间 +function formatDateTime(dateStr: string | null | undefined) { + if (!dateStr) + return '-'; + const date = new Date(dateStr); + return date.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); +} + + + +
+ +
+
+ + + + + + + + +
+ +
+ + +
+
+ {{ index + 1 }} + {{ item.name }} + + + + + + + + + + + + +
+
+ + + +
+ + +