Download web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue from hequ/cc: direct link, hf CLI and curl.
- Browser
- Download file 55.1 kB
-
https://huggingface.co/spaces/hequ/cc/resolve/main/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
- Command line
-
hf download hf://spaces/hequ/cc/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
-
curl -L -o CreateApiKeyModal.vue https://huggingface.co/spaces/hequ/cc/resolve/main/web/admin-spa/src/components/apikeys/CreateApiKeyModal.vue
55.1 kB
| <template> | |
| <Teleport to="body"> | |
| <div class="modal fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4"> | |
| <div class="modal-content mx-auto flex max-h-[90vh] w-full max-w-4xl flex-col p-4 sm:p-6"> | |
| <div class="mb-4 flex items-center justify-between"> | |
| <div class="flex items-center gap-2 sm:gap-3"> | |
| <div | |
| class="flex h-8 w-8 items-center justify-center rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 sm:h-10 sm:w-10 sm:rounded-xl" | |
| > | |
| <i class="fas fa-key text-sm text-white sm:text-base" /> | |
| </div> | |
| <h3 class="text-lg font-bold text-gray-900 dark:text-gray-100 sm:text-xl"> | |
| 创建新的 API Key | |
| </h3> | |
| </div> | |
| <button | |
| class="p-1 text-gray-400 transition-colors hover:text-gray-600 dark:text-gray-500 dark:hover:text-gray-300" | |
| @click="$emit('close')" | |
| > | |
| <i class="fas fa-times text-lg sm:text-xl" /> | |
| </button> | |
| </div> | |
| <form | |
| class="modal-scroll-content custom-scrollbar flex-1 space-y-4" | |
| @submit.prevent="createApiKey" | |
| > | |
| <!-- 创建类型选择 --> | |
| <div | |
| class="rounded-lg border border-blue-200 bg-gradient-to-r from-blue-50 to-indigo-50 p-3 dark:border-blue-700 dark:from-blue-900/20 dark:to-indigo-900/20 sm:p-4" | |
| > | |
| <div | |
| :class="[ | |
| 'flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between', | |
| form.createType === 'batch' ? 'mb-3' : '' | |
| ]" | |
| > | |
| <label | |
| class="flex h-full items-center text-xs font-semibold text-gray-700 dark:text-gray-300 sm:text-sm" | |
| >创建类型</label | |
| > | |
| <div class="flex items-center gap-3 sm:gap-4"> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.createType" | |
| class="mr-1.5 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 sm:mr-2" | |
| type="radio" | |
| value="single" | |
| /> | |
| <span | |
| class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm" | |
| > | |
| <i class="fas fa-key mr-1 text-xs" /> | |
| 单个创建 | |
| </span> | |
| </label> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.createType" | |
| class="mr-1.5 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 sm:mr-2" | |
| type="radio" | |
| value="batch" | |
| /> | |
| <span | |
| class="flex items-center text-xs text-gray-700 dark:text-gray-300 sm:text-sm" | |
| > | |
| <i class="fas fa-layer-group mr-1 text-xs" /> | |
| 批量创建 | |
| </span> | |
| </label> | |
| </div> | |
| </div> | |
| <!-- 批量创建数量输入 --> | |
| <div v-if="form.createType === 'batch'" class="mt-3"> | |
| <div class="flex items-center gap-4"> | |
| <div class="flex-1"> | |
| <label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400" | |
| >创建数量</label | |
| > | |
| <div class="flex items-center gap-2"> | |
| <input | |
| v-model.number="form.batchCount" | |
| class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| max="500" | |
| min="2" | |
| placeholder="输入数量 (2-500)" | |
| required | |
| type="number" | |
| /> | |
| <div class="whitespace-nowrap text-xs text-gray-500 dark:text-gray-400"> | |
| 最大支持 500 个 | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <p class="mt-2 flex items-start text-xs text-amber-600 dark:text-amber-400"> | |
| <i class="fas fa-info-circle mr-1 mt-0.5 flex-shrink-0" /> | |
| <span | |
| >批量创建时,每个 Key 的名称会自动添加序号后缀,例如:{{ | |
| form.name || 'MyKey' | |
| }}_1, {{ form.name || 'MyKey' }}_2 ...</span | |
| > | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <label | |
| class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-2 sm:text-sm" | |
| >名称 <span class="text-red-500">*</span></label | |
| > | |
| <div> | |
| <input | |
| v-model="form.name" | |
| class="form-input flex-1 border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| :class="{ 'border-red-500': errors.name }" | |
| :placeholder=" | |
| form.createType === 'batch' | |
| ? '输入基础名称(将自动添加序号)' | |
| : '为您的 API Key 取一个名称' | |
| " | |
| required | |
| type="text" | |
| @input="errors.name = ''" | |
| /> | |
| </div> | |
| <p v-if="errors.name" class="mt-1 text-xs text-red-500 dark:text-red-400"> | |
| {{ errors.name }} | |
| </p> | |
| </div> | |
| <!-- 标签 --> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >标签</label | |
| > | |
| <div class="space-y-4"> | |
| <!-- 已选择的标签 --> | |
| <div v-if="form.tags.length > 0"> | |
| <div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400"> | |
| 已选择的标签: | |
| </div> | |
| <div class="flex flex-wrap gap-2"> | |
| <span | |
| v-for="(tag, index) in form.tags" | |
| :key="'selected-' + index" | |
| class="inline-flex items-center gap-1 rounded-full bg-blue-100 px-3 py-1 text-sm text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" | |
| > | |
| {{ tag }} | |
| <button | |
| class="ml-1 hover:text-blue-900 dark:hover:text-blue-300" | |
| type="button" | |
| @click="removeTag(index)" | |
| > | |
| <i class="fas fa-times text-xs" /> | |
| </button> | |
| </span> | |
| </div> | |
| </div> | |
| <!-- 可选择的已有标签 --> | |
| <div v-if="unselectedTags.length > 0"> | |
| <div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400"> | |
| 点击选择已有标签: | |
| </div> | |
| <div class="flex flex-wrap gap-2"> | |
| <button | |
| v-for="tag in unselectedTags" | |
| :key="'available-' + tag" | |
| class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm text-gray-700 transition-colors hover:bg-blue-100 hover:text-blue-700 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-blue-900/30 dark:hover:text-blue-400" | |
| type="button" | |
| @click="selectTag(tag)" | |
| > | |
| <i class="fas fa-tag text-xs text-gray-500 dark:text-gray-400" /> | |
| {{ tag }} | |
| </button> | |
| </div> | |
| </div> | |
| <!-- 创建新标签 --> | |
| <div> | |
| <div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400"> | |
| 创建新标签: | |
| </div> | |
| <div class="flex gap-2"> | |
| <input | |
| v-model="newTag" | |
| class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| placeholder="输入新标签名称" | |
| type="text" | |
| @keypress.enter.prevent="addTag" | |
| /> | |
| <button | |
| class="rounded-lg bg-green-500 px-4 py-2 text-white transition-colors hover:bg-green-600" | |
| type="button" | |
| @click="addTag" | |
| > | |
| <i class="fas fa-plus" /> | |
| </button> | |
| </div> | |
| </div> | |
| <p class="text-xs text-gray-500 dark:text-gray-400"> | |
| 用于标记不同团队或用途,方便筛选管理 | |
| </p> | |
| </div> | |
| </div> | |
| <!-- 速率限制设置 --> | |
| <div | |
| class="rounded-lg border border-blue-200 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/20" | |
| > | |
| <div class="mb-2 flex items-center gap-2"> | |
| <div | |
| class="flex h-6 w-6 flex-shrink-0 items-center justify-center rounded bg-blue-500" | |
| > | |
| <i class="fas fa-tachometer-alt text-xs text-white" /> | |
| </div> | |
| <h4 class="text-sm font-semibold text-gray-800 dark:text-gray-200"> | |
| 速率限制设置 (可选) | |
| </h4> | |
| </div> | |
| <div class="space-y-2"> | |
| <div class="grid grid-cols-1 gap-2 lg:grid-cols-3"> | |
| <div> | |
| <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300" | |
| >时间窗口 (分钟)</label | |
| > | |
| <input | |
| v-model="form.rateLimitWindow" | |
| class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="1" | |
| placeholder="无限制" | |
| type="number" | |
| /> | |
| <p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">时间段单位</p> | |
| </div> | |
| <div> | |
| <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300" | |
| >请求次数限制</label | |
| > | |
| <input | |
| v-model="form.rateLimitRequests" | |
| class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="1" | |
| placeholder="无限制" | |
| type="number" | |
| /> | |
| <p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大请求</p> | |
| </div> | |
| <div> | |
| <label class="mb-1 block text-xs font-medium text-gray-700 dark:text-gray-300" | |
| >费用限制 (美元)</label | |
| > | |
| <input | |
| v-model="form.rateLimitCost" | |
| class="form-input w-full border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="0" | |
| placeholder="无限制" | |
| step="0.01" | |
| type="number" | |
| /> | |
| <p class="ml-2 mt-0.5 text-xs text-gray-500 dark:text-gray-400">窗口内最大费用</p> | |
| </div> | |
| </div> | |
| <!-- 示例说明 --> | |
| <div class="rounded-lg bg-blue-100 p-2 dark:bg-blue-900/30"> | |
| <h5 class="mb-1 text-xs font-semibold text-blue-800 dark:text-blue-400"> | |
| 💡 使用示例 | |
| </h5> | |
| <div class="space-y-0.5 text-xs text-blue-700 dark:text-blue-300"> | |
| <div> | |
| <strong>示例1:</strong> 时间窗口=60,请求次数=1000 → 每60分钟最多1000次请求 | |
| </div> | |
| <div><strong>示例2:</strong> 时间窗口=1,费用=0.1 → 每分钟最多$0.1费用</div> | |
| <div> | |
| <strong>示例3:</strong> 窗口=30,请求=50,费用=5 → 每30分钟50次请求且不超$5费用 | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >每日费用限制 (美元)</label | |
| > | |
| <div class="space-y-2"> | |
| <div class="flex gap-2"> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.dailyCostLimit = '50'" | |
| > | |
| $50 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.dailyCostLimit = '100'" | |
| > | |
| $100 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.dailyCostLimit = '200'" | |
| > | |
| $200 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.dailyCostLimit = ''" | |
| > | |
| 自定义 | |
| </button> | |
| </div> | |
| <input | |
| v-model="form.dailyCostLimit" | |
| class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="0" | |
| placeholder="0 表示无限制" | |
| step="0.01" | |
| type="number" | |
| /> | |
| <p class="dark:text灰-400 text-xs text-gray-500"> | |
| 设置此 API Key 每日的费用限制,超过限制将拒绝请求,0 或留空表示无限制 | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >总费用限制 (美元)</label | |
| > | |
| <div class="space-y-2"> | |
| <div class="flex gap-2"> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.totalCostLimit = '100'" | |
| > | |
| $100 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.totalCostLimit = '500'" | |
| > | |
| $500 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.totalCostLimit = '1000'" | |
| > | |
| $1000 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.totalCostLimit = ''" | |
| > | |
| 自定义 | |
| </button> | |
| </div> | |
| <input | |
| v-model="form.totalCostLimit" | |
| class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="0" | |
| placeholder="0 表示无限制" | |
| step="0.01" | |
| type="number" | |
| /> | |
| <p class="text-xs text-gray-500 dark:text-gray-400"> | |
| 设置此 API Key 的累计总费用限制,达到限制后将拒绝所有后续请求,0 或留空表示无限制 | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >Opus 模型周费用限制 (美元)</label | |
| > | |
| <div class="space-y-2"> | |
| <div class="flex gap-2"> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.weeklyOpusCostLimit = '100'" | |
| > | |
| $100 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.weeklyOpusCostLimit = '500'" | |
| > | |
| $500 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.weeklyOpusCostLimit = '1000'" | |
| > | |
| $1000 | |
| </button> | |
| <button | |
| class="rounded bg-gray-100 px-2 py-1 text-xs font-medium hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="form.weeklyOpusCostLimit = ''" | |
| > | |
| 自定义 | |
| </button> | |
| </div> | |
| <input | |
| v-model="form.weeklyOpusCostLimit" | |
| class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="0" | |
| placeholder="0 表示无限制" | |
| step="0.01" | |
| type="number" | |
| /> | |
| <p class="text-xs text-gray-500 dark:text-gray-400"> | |
| 设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户,0 或留空表示无限制 | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >并发限制 (可选)</label | |
| > | |
| <input | |
| v-model="form.concurrencyLimit" | |
| class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| min="0" | |
| placeholder="0 表示无限制" | |
| type="number" | |
| /> | |
| <p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
| 设置此 API Key 可同时处理的最大请求数,0 或留空表示无限制 | |
| </p> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >备注 (可选)</label | |
| > | |
| <textarea | |
| v-model="form.description" | |
| class="form-input w-full resize-none border-gray-300 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400" | |
| placeholder="描述此 API Key 的用途..." | |
| rows="2" | |
| /> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >过期设置</label | |
| > | |
| <!-- 过期模式选择 --> | |
| <div | |
| class="mb-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-800" | |
| > | |
| <div class="flex items-center gap-4"> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.expirationMode" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="fixed" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">固定时间过期</span> | |
| </label> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.expirationMode" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="activation" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">首次使用后激活</span> | |
| </label> | |
| </div> | |
| <p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
| <span v-if="form.expirationMode === 'fixed'"> | |
| <i class="fas fa-info-circle mr-1" /> | |
| 固定时间模式:Key 创建后立即生效,按设定时间过期(支持小时和天数) | |
| </span> | |
| <span v-else> | |
| <i class="fas fa-info-circle mr-1" /> | |
| 激活模式:Key 首次使用时激活,激活后按设定时间过期(支持小时和天数,适合批量销售) | |
| </span> | |
| </p> | |
| </div> | |
| <!-- 固定时间模式 --> | |
| <div v-if="form.expirationMode === 'fixed'"> | |
| <select | |
| v-model="form.expireDuration" | |
| class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" | |
| @change="updateExpireAt" | |
| > | |
| <option value="">永不过期</option> | |
| <option value="1h">1 小时</option> | |
| <option value="3h">3 小时</option> | |
| <option value="6h">6 小时</option> | |
| <option value="12h">12 小时</option> | |
| <option value="1d">1 天</option> | |
| <option value="7d">7 天</option> | |
| <option value="30d">30 天</option> | |
| <option value="90d">90 天</option> | |
| <option value="180d">180 天</option> | |
| <option value="365d">365 天</option> | |
| <option value="custom">自定义日期</option> | |
| </select> | |
| <div v-if="form.expireDuration === 'custom'" class="mt-3"> | |
| <input | |
| v-model="form.customExpireDate" | |
| class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" | |
| :min="minDateTime" | |
| type="datetime-local" | |
| @change="updateCustomExpireAt" | |
| /> | |
| </div> | |
| <p v-if="form.expiresAt" class="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
| 将于 {{ formatExpireDate(form.expiresAt) }} 过期 | |
| </p> | |
| </div> | |
| <!-- 激活模式 --> | |
| <div v-else> | |
| <div class="flex items-center gap-2"> | |
| <input | |
| v-model.number="form.activationDays" | |
| class="form-input flex-1 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" | |
| :max="form.activationUnit === 'hours' ? 8760 : 3650" | |
| min="1" | |
| :placeholder="form.activationUnit === 'hours' ? '输入小时数' : '输入天数'" | |
| type="number" | |
| /> | |
| <select | |
| v-model="form.activationUnit" | |
| class="form-input w-20 border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200" | |
| @change="updateActivationValue" | |
| > | |
| <option value="hours">小时</option> | |
| <option value="days">天</option> | |
| </select> | |
| </div> | |
| <div class="mt-2 flex flex-wrap gap-2"> | |
| <button | |
| v-for="value in getQuickTimeOptions()" | |
| :key="value.value" | |
| class="rounded-md border border-gray-300 px-3 py-1 text-xs hover:bg-gray-100 dark:border-gray-600 dark:hover:bg-gray-700" | |
| type="button" | |
| @click="form.activationDays = value.value" | |
| > | |
| {{ value.label }} | |
| </button> | |
| </div> | |
| <p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
| <i class="fas fa-clock mr-1" /> | |
| Key 将在首次使用后激活,激活后 | |
| {{ form.activationDays || (form.activationUnit === 'hours' ? 24 : 30) }} | |
| {{ form.activationUnit === 'hours' ? '小时' : '天' }}过期 | |
| </p> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="mb-2 block text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >服务权限</label | |
| > | |
| <div class="flex gap-4"> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.permissions" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="all" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">全部服务</span> | |
| </label> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.permissions" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="claude" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">仅 Claude</span> | |
| </label> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.permissions" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="gemini" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">仅 Gemini</span> | |
| </label> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.permissions" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="openai" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">仅 OpenAI</span> | |
| </label> | |
| <label class="flex cursor-pointer items-center"> | |
| <input | |
| v-model="form.permissions" | |
| class="mr-2 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700" | |
| type="radio" | |
| value="droid" | |
| /> | |
| <span class="text-sm text-gray-700 dark:text-gray-300">仅 Droid</span> | |
| </label> | |
| </div> | |
| <p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
| 控制此 API Key 可以访问哪些服务 | |
| </p> | |
| </div> | |
| <div> | |
| <div class="mb-2 flex items-center justify-between"> | |
| <label class="text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| >专属账号绑定 (可选)</label | |
| > | |
| <button | |
| class="flex items-center gap-1 text-sm text-blue-600 transition-colors hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50 dark:text-blue-400 dark:hover:text-blue-300" | |
| :disabled="accountsLoading" | |
| title="刷新账号列表" | |
| type="button" | |
| @click="refreshAccounts" | |
| > | |
| <i | |
| :class="[ | |
| 'fas', | |
| accountsLoading ? 'fa-spinner fa-spin' : 'fa-sync-alt', | |
| 'text-xs' | |
| ]" | |
| /> | |
| <span>{{ accountsLoading ? '刷新中...' : '刷新账号' }}</span> | |
| </button> | |
| </div> | |
| <div class="grid grid-cols-1 gap-3"> | |
| <div> | |
| <label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400" | |
| >Claude 专属账号</label | |
| > | |
| <AccountSelector | |
| v-model="form.claudeAccountId" | |
| :accounts="localAccounts.claude" | |
| default-option-text="使用共享账号池" | |
| :disabled="form.permissions !== 'all' && form.permissions !== 'claude'" | |
| :groups="localAccounts.claudeGroups" | |
| placeholder="请选择Claude账号" | |
| platform="claude" | |
| /> | |
| </div> | |
| <div> | |
| <label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400" | |
| >Gemini 专属账号</label | |
| > | |
| <AccountSelector | |
| v-model="form.geminiAccountId" | |
| :accounts="localAccounts.gemini" | |
| default-option-text="使用共享账号池" | |
| :disabled="form.permissions !== 'all' && form.permissions !== 'gemini'" | |
| :groups="localAccounts.geminiGroups" | |
| placeholder="请选择Gemini账号" | |
| platform="gemini" | |
| /> | |
| </div> | |
| <div> | |
| <label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400" | |
| >OpenAI 专属账号</label | |
| > | |
| <AccountSelector | |
| v-model="form.openaiAccountId" | |
| :accounts="localAccounts.openai" | |
| default-option-text="使用共享账号池" | |
| :disabled="form.permissions !== 'all' && form.permissions !== 'openai'" | |
| :groups="localAccounts.openaiGroups" | |
| placeholder="请选择OpenAI账号" | |
| platform="openai" | |
| /> | |
| </div> | |
| <div> | |
| <label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400" | |
| >Bedrock 专属账号</label | |
| > | |
| <AccountSelector | |
| v-model="form.bedrockAccountId" | |
| :accounts="localAccounts.bedrock" | |
| default-option-text="使用共享账号池" | |
| :disabled="form.permissions !== 'all' && form.permissions !== 'openai'" | |
| :groups="[]" | |
| placeholder="请选择Bedrock账号" | |
| platform="bedrock" | |
| /> | |
| </div> | |
| <div> | |
| <label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400" | |
| >Droid 专属账号</label | |
| > | |
| <AccountSelector | |
| v-model="form.droidAccountId" | |
| :accounts="localAccounts.droid" | |
| default-option-text="使用共享账号池" | |
| :disabled="form.permissions !== 'all' && form.permissions !== 'droid'" | |
| :groups="localAccounts.droidGroups" | |
| placeholder="请选择Droid账号" | |
| platform="droid" | |
| /> | |
| </div> | |
| </div> | |
| <p class="mt-2 text-xs text-gray-500 dark:text-gray-400"> | |
| 选择专属账号后,此API Key将只使用该账号,不选择则使用共享账号池 | |
| </p> | |
| </div> | |
| <div> | |
| <div class="mb-2 flex items-center"> | |
| <input | |
| id="enableModelRestriction" | |
| v-model="form.enableModelRestriction" | |
| class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500" | |
| type="checkbox" | |
| /> | |
| <label | |
| class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| for="enableModelRestriction" | |
| > | |
| 启用模型限制 | |
| </label> | |
| </div> | |
| <div v-if="form.enableModelRestriction" class="space-y-3"> | |
| <div> | |
| <label class="mb-2 block text-sm font-medium text-gray-600">限制的模型列表</label> | |
| <div | |
| class="mb-3 flex min-h-[32px] flex-wrap gap-2 rounded-lg border border-gray-200 bg-gray-50 p-2" | |
| > | |
| <span | |
| v-for="(model, index) in form.restrictedModels" | |
| :key="index" | |
| class="inline-flex items-center rounded-full bg-red-100 px-3 py-1 text-sm text-red-800" | |
| > | |
| {{ model }} | |
| <button | |
| class="ml-2 text-red-600 hover:text-red-800" | |
| type="button" | |
| @click="removeRestrictedModel(index)" | |
| > | |
| <i class="fas fa-times text-xs" /> | |
| </button> | |
| </span> | |
| <span v-if="form.restrictedModels.length === 0" class="text-sm text-gray-400"> | |
| 暂无限制的模型 | |
| </span> | |
| </div> | |
| <div class="space-y-3"> | |
| <!-- 快速添加按钮 --> | |
| <div class="flex flex-wrap gap-2"> | |
| <button | |
| v-for="model in availableQuickModels" | |
| :key="model" | |
| class="flex-shrink-0 rounded-lg bg-gray-100 px-3 py-1 text-xs text-gray-700 transition-colors hover:bg-gray-200 sm:text-sm" | |
| type="button" | |
| @click="quickAddRestrictedModel(model)" | |
| > | |
| {{ model }} | |
| </button> | |
| <span | |
| v-if="availableQuickModels.length === 0" | |
| class="text-sm italic text-gray-400" | |
| > | |
| 所有常用模型已在限制列表中 | |
| </span> | |
| </div> | |
| <!-- 手动输入 --> | |
| <div class="flex gap-2"> | |
| <input | |
| v-model="form.modelInput" | |
| class="form-input flex-1" | |
| placeholder="输入模型名称,按回车添加" | |
| type="text" | |
| @keydown.enter.prevent="addRestrictedModel" | |
| /> | |
| <button | |
| class="rounded-lg bg-red-500 px-4 py-2 text-white transition-colors hover:bg-red-600" | |
| type="button" | |
| @click="addRestrictedModel" | |
| > | |
| <i class="fas fa-plus" /> | |
| </button> | |
| </div> | |
| </div> | |
| <p class="mt-2 text-xs text-gray-500"> | |
| 设置此API Key无法访问的模型,例如:claude-opus-4-20250514 | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- 客户端限制 --> | |
| <div> | |
| <div class="mb-2 flex items-center"> | |
| <input | |
| id="enableClientRestriction" | |
| v-model="form.enableClientRestriction" | |
| class="h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500" | |
| type="checkbox" | |
| /> | |
| <label | |
| class="ml-2 cursor-pointer text-sm font-semibold text-gray-700 dark:text-gray-300" | |
| for="enableClientRestriction" | |
| > | |
| 启用客户端限制 | |
| </label> | |
| </div> | |
| <div | |
| v-if="form.enableClientRestriction" | |
| class="rounded-lg border border-green-200 bg-green-50 p-3 dark:border-green-700 dark:bg-green-900/20" | |
| > | |
| <div> | |
| <label class="mb-2 block text-xs font-medium text-gray-700 dark:text-gray-300" | |
| >允许的客户端</label | |
| > | |
| <div class="space-y-1"> | |
| <div v-for="client in supportedClients" :key="client.id" class="flex items-start"> | |
| <input | |
| :id="`client_${client.id}`" | |
| v-model="form.allowedClients" | |
| class="mt-0.5 h-4 w-4 rounded border-gray-300 bg-gray-100 text-blue-600 focus:ring-blue-500" | |
| type="checkbox" | |
| :value="client.id" | |
| /> | |
| <label class="ml-2 flex-1 cursor-pointer" :for="`client_${client.id}`"> | |
| <span class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ | |
| client.name | |
| }}</span> | |
| <span class="block text-xs text-gray-500 dark:text-gray-400">{{ | |
| client.description | |
| }}</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex gap-3 pt-2"> | |
| <button | |
| class="flex-1 rounded-lg bg-gray-100 px-4 py-2.5 text-sm font-semibold text-gray-700 transition-colors hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600" | |
| type="button" | |
| @click="$emit('close')" | |
| > | |
| 取消 | |
| </button> | |
| <button | |
| class="btn btn-primary flex-1 px-4 py-2.5 text-sm font-semibold" | |
| :disabled="loading" | |
| type="submit" | |
| > | |
| <div v-if="loading" class="loading-spinner mr-2" /> | |
| <i v-else class="fas fa-plus mr-2" /> | |
| {{ loading ? '创建中...' : '创建' }} | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </Teleport> | |
| </template> | |
| <script setup> | |
| import { ref, reactive, computed, onMounted } from 'vue' | |
| import { showToast } from '@/utils/toast' | |
| import { useClientsStore } from '@/stores/clients' | |
| import { useApiKeysStore } from '@/stores/apiKeys' | |
| import { apiClient } from '@/config/api' | |
| import AccountSelector from '@/components/common/AccountSelector.vue' | |
| const props = defineProps({ | |
| accounts: { | |
| type: Object, | |
| default: () => ({ | |
| claude: [], | |
| gemini: [], | |
| openai: [], | |
| bedrock: [], | |
| droid: [], | |
| claudeGroups: [], | |
| geminiGroups: [], | |
| openaiGroups: [], | |
| droidGroups: [] | |
| }) | |
| } | |
| }) | |
| const emit = defineEmits(['close', 'success', 'batch-success']) | |
| const clientsStore = useClientsStore() | |
| const apiKeysStore = useApiKeysStore() | |
| const loading = ref(false) | |
| const accountsLoading = ref(false) | |
| const localAccounts = ref({ | |
| claude: [], | |
| gemini: [], | |
| openai: [], | |
| bedrock: [], | |
| droid: [], | |
| claudeGroups: [], | |
| geminiGroups: [], | |
| openaiGroups: [], | |
| droidGroups: [] | |
| }) | |
| // 表单验证状态 | |
| const errors = ref({ | |
| name: '' | |
| }) | |
| // 标签相关 | |
| const newTag = ref('') | |
| const availableTags = ref([]) | |
| // 计算未选择的标签 | |
| const unselectedTags = computed(() => { | |
| return availableTags.value.filter((tag) => !form.tags.includes(tag)) | |
| }) | |
| // 支持的客户端列表 | |
| const supportedClients = ref([]) | |
| // 表单数据 | |
| const form = reactive({ | |
| createType: 'single', | |
| batchCount: 10, | |
| name: '', | |
| description: '', | |
| rateLimitWindow: '', | |
| rateLimitRequests: '', | |
| rateLimitCost: '', // 新增:费用限制 | |
| concurrencyLimit: '', | |
| dailyCostLimit: '', | |
| totalCostLimit: '', | |
| weeklyOpusCostLimit: '', | |
| expireDuration: '', | |
| customExpireDate: '', | |
| expiresAt: null, | |
| expirationMode: 'fixed', // 过期模式:fixed(固定) 或 activation(激活) | |
| activationDays: 30, // 激活后有效天数 | |
| activationUnit: 'days', // 激活时间单位:hours 或 days | |
| permissions: 'all', | |
| claudeAccountId: '', | |
| geminiAccountId: '', | |
| openaiAccountId: '', | |
| bedrockAccountId: '', | |
| droidAccountId: '', | |
| enableModelRestriction: false, | |
| restrictedModels: [], | |
| modelInput: '', | |
| enableClientRestriction: false, | |
| allowedClients: [], | |
| tags: [] | |
| }) | |
| // 加载支持的客户端和已存在的标签 | |
| onMounted(async () => { | |
| supportedClients.value = await clientsStore.loadSupportedClients() | |
| availableTags.value = await apiKeysStore.fetchTags() | |
| // 初始化账号数据 | |
| if (props.accounts) { | |
| // 合并 OpenAI 和 OpenAI-Responses 账号 | |
| const openaiAccounts = [] | |
| if (props.accounts.openai) { | |
| props.accounts.openai.forEach((account) => { | |
| openaiAccounts.push({ | |
| ...account, | |
| platform: 'openai' | |
| }) | |
| }) | |
| } | |
| if (props.accounts.openaiResponses) { | |
| props.accounts.openaiResponses.forEach((account) => { | |
| openaiAccounts.push({ | |
| ...account, | |
| platform: 'openai-responses' | |
| }) | |
| }) | |
| } | |
| localAccounts.value = { | |
| claude: props.accounts.claude || [], | |
| gemini: props.accounts.gemini || [], | |
| openai: openaiAccounts, | |
| bedrock: props.accounts.bedrock || [], | |
| droid: (props.accounts.droid || []).map((account) => ({ | |
| ...account, | |
| platform: 'droid' | |
| })), | |
| claudeGroups: props.accounts.claudeGroups || [], | |
| geminiGroups: props.accounts.geminiGroups || [], | |
| openaiGroups: props.accounts.openaiGroups || [], | |
| droidGroups: props.accounts.droidGroups || [] | |
| } | |
| } | |
| // 自动加载账号数据 | |
| await refreshAccounts() | |
| }) | |
| // 刷新账号列表 | |
| const refreshAccounts = async () => { | |
| accountsLoading.value = true | |
| try { | |
| const [ | |
| claudeData, | |
| claudeConsoleData, | |
| geminiData, | |
| openaiData, | |
| openaiResponsesData, | |
| bedrockData, | |
| droidData, | |
| groupsData | |
| ] = await Promise.all([ | |
| apiClient.get('/admin/claude-accounts'), | |
| apiClient.get('/admin/claude-console-accounts'), | |
| apiClient.get('/admin/gemini-accounts'), | |
| apiClient.get('/admin/openai-accounts'), | |
| apiClient.get('/admin/openai-responses-accounts'), // 获取 OpenAI-Responses 账号 | |
| apiClient.get('/admin/bedrock-accounts'), | |
| apiClient.get('/admin/droid-accounts'), | |
| apiClient.get('/admin/account-groups') | |
| ]) | |
| // 合并Claude OAuth账户和Claude Console账户 | |
| const claudeAccounts = [] | |
| if (claudeData.success) { | |
| claudeData.data?.forEach((account) => { | |
| claudeAccounts.push({ | |
| ...account, | |
| platform: 'claude-oauth', | |
| isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容 | |
| }) | |
| }) | |
| } | |
| if (claudeConsoleData.success) { | |
| claudeConsoleData.data?.forEach((account) => { | |
| claudeAccounts.push({ | |
| ...account, | |
| platform: 'claude-console', | |
| isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容 | |
| }) | |
| }) | |
| } | |
| localAccounts.value.claude = claudeAccounts | |
| if (geminiData.success) { | |
| localAccounts.value.gemini = (geminiData.data || []).map((account) => ({ | |
| ...account, | |
| isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容 | |
| })) | |
| } | |
| // 合并 OpenAI 和 OpenAI-Responses 账号 | |
| const openaiAccounts = [] | |
| if (openaiData.success) { | |
| ;(openaiData.data || []).forEach((account) => { | |
| openaiAccounts.push({ | |
| ...account, | |
| platform: 'openai', | |
| isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容 | |
| }) | |
| }) | |
| } | |
| if (openaiResponsesData.success) { | |
| ;(openaiResponsesData.data || []).forEach((account) => { | |
| openaiAccounts.push({ | |
| ...account, | |
| platform: 'openai-responses', | |
| isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容 | |
| }) | |
| }) | |
| } | |
| localAccounts.value.openai = openaiAccounts | |
| if (bedrockData.success) { | |
| localAccounts.value.bedrock = (bedrockData.data || []).map((account) => ({ | |
| ...account, | |
| isDedicated: account.accountType === 'dedicated' // 保留以便向后兼容 | |
| })) | |
| } | |
| if (droidData.success) { | |
| localAccounts.value.droid = (droidData.data || []).map((account) => ({ | |
| ...account, | |
| platform: 'droid', | |
| isDedicated: account.accountType === 'dedicated' | |
| })) | |
| } | |
| // 处理分组数据 | |
| if (groupsData.success) { | |
| const allGroups = groupsData.data || [] | |
| localAccounts.value.claudeGroups = allGroups.filter((g) => g.platform === 'claude') | |
| localAccounts.value.geminiGroups = allGroups.filter((g) => g.platform === 'gemini') | |
| localAccounts.value.openaiGroups = allGroups.filter((g) => g.platform === 'openai') | |
| localAccounts.value.droidGroups = allGroups.filter((g) => g.platform === 'droid') | |
| } | |
| showToast('账号列表已刷新', 'success') | |
| } catch (error) { | |
| showToast('刷新账号列表失败', 'error') | |
| } finally { | |
| accountsLoading.value = false | |
| } | |
| } | |
| // 计算最小日期时间 | |
| const minDateTime = computed(() => { | |
| const now = new Date() | |
| now.setMinutes(now.getMinutes() + 1) | |
| return now.toISOString().slice(0, 16) | |
| }) | |
| // 更新过期时间 | |
| const updateExpireAt = () => { | |
| if (!form.expireDuration) { | |
| form.expiresAt = null | |
| return | |
| } | |
| if (form.expireDuration === 'custom') { | |
| return | |
| } | |
| const now = new Date() | |
| const duration = form.expireDuration | |
| const match = duration.match(/(\d+)([dhmy])/) | |
| if (match) { | |
| const [, value, unit] = match | |
| const num = parseInt(value) | |
| switch (unit) { | |
| case 'd': | |
| now.setDate(now.getDate() + num) | |
| break | |
| case 'h': | |
| now.setHours(now.getHours() + num) | |
| break | |
| case 'm': | |
| now.setMonth(now.getMonth() + num) | |
| break | |
| case 'y': | |
| now.setFullYear(now.getFullYear() + num) | |
| break | |
| } | |
| form.expiresAt = now.toISOString() | |
| } | |
| } | |
| // 更新自定义过期时间 | |
| const updateCustomExpireAt = () => { | |
| if (form.customExpireDate) { | |
| form.expiresAt = new Date(form.customExpireDate).toISOString() | |
| } | |
| } | |
| // 格式化过期日期 | |
| const formatExpireDate = (dateString) => { | |
| const date = new Date(dateString) | |
| return date.toLocaleString('zh-CN', { | |
| year: 'numeric', | |
| month: '2-digit', | |
| day: '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }) | |
| } | |
| // 添加限制的模型 | |
| const addRestrictedModel = () => { | |
| if (form.modelInput && !form.restrictedModels.includes(form.modelInput)) { | |
| form.restrictedModels.push(form.modelInput) | |
| form.modelInput = '' | |
| } | |
| } | |
| // 移除限制的模型 | |
| const removeRestrictedModel = (index) => { | |
| form.restrictedModels.splice(index, 1) | |
| } | |
| // 常用模型列表 | |
| const commonModels = ref(['claude-opus-4-20250514', 'claude-opus-4-1-20250805']) | |
| // 可用的快捷模型(过滤掉已在限制列表中的) | |
| const availableQuickModels = computed(() => { | |
| return commonModels.value.filter((model) => !form.restrictedModels.includes(model)) | |
| }) | |
| // 快速添加限制的模型 | |
| const quickAddRestrictedModel = (model) => { | |
| if (!form.restrictedModels.includes(model)) { | |
| form.restrictedModels.push(model) | |
| } | |
| } | |
| // 标签管理方法 | |
| const addTag = () => { | |
| if (newTag.value && newTag.value.trim()) { | |
| const tag = newTag.value.trim() | |
| if (!form.tags.includes(tag)) { | |
| form.tags.push(tag) | |
| } | |
| newTag.value = '' | |
| } | |
| } | |
| const selectTag = (tag) => { | |
| if (!form.tags.includes(tag)) { | |
| form.tags.push(tag) | |
| } | |
| } | |
| const removeTag = (index) => { | |
| form.tags.splice(index, 1) | |
| } | |
| // 获取快捷时间选项 | |
| const getQuickTimeOptions = () => { | |
| if (form.activationUnit === 'hours') { | |
| return [ | |
| { value: 1, label: '1小时' }, | |
| { value: 3, label: '3小时' }, | |
| { value: 6, label: '6小时' }, | |
| { value: 12, label: '12小时' } | |
| ] | |
| } else { | |
| return [ | |
| { value: 30, label: '30天' }, | |
| { value: 90, label: '90天' }, | |
| { value: 180, label: '180天' }, | |
| { value: 365, label: '365天' } | |
| ] | |
| } | |
| } | |
| // 单位变化时更新数值 | |
| const updateActivationValue = () => { | |
| if (form.activationUnit === 'hours') { | |
| // 从天切换到小时,设置一个合理的默认值 | |
| if (form.activationDays > 24) { | |
| form.activationDays = 24 | |
| } | |
| } else { | |
| // 从小时切换到天,设置一个合理的默认值 | |
| if (form.activationDays < 1) { | |
| form.activationDays = 1 | |
| } | |
| } | |
| } | |
| // 创建 API Key | |
| const createApiKey = async () => { | |
| // 验证表单 | |
| errors.value.name = '' | |
| if (!form.name || !form.name.trim()) { | |
| errors.value.name = '请输入API Key名称' | |
| return | |
| } | |
| // 批量创建时验证数量 | |
| if (form.createType === 'batch') { | |
| if (!form.batchCount || form.batchCount < 2 || form.batchCount > 500) { | |
| showToast('批量创建数量必须在 2-500 之间', 'error') | |
| return | |
| } | |
| } | |
| // 检查是否设置了时间窗口但费用限制为0 | |
| if (form.rateLimitWindow && (!form.rateLimitCost || parseFloat(form.rateLimitCost) === 0)) { | |
| let confirmed = false | |
| if (window.showConfirm) { | |
| confirmed = await window.showConfirm( | |
| '费用限制提醒', | |
| '您设置了时间窗口但费用限制为0,这意味着不会有费用限制。\n\n是否继续?', | |
| '继续创建', | |
| '返回修改' | |
| ) | |
| } else { | |
| // 降级方案 | |
| confirmed = confirm('您设置了时间窗口但费用限制为0,这意味着不会有费用限制。\n是否继续?') | |
| } | |
| if (!confirmed) { | |
| return | |
| } | |
| } | |
| loading.value = true | |
| try { | |
| // 准备提交的数据 | |
| const baseData = { | |
| description: form.description || undefined, | |
| tokenLimit: 0, // 设置为0,清除历史token限制 | |
| rateLimitWindow: | |
| form.rateLimitWindow !== '' && form.rateLimitWindow !== null | |
| ? parseInt(form.rateLimitWindow) | |
| : null, | |
| rateLimitRequests: | |
| form.rateLimitRequests !== '' && form.rateLimitRequests !== null | |
| ? parseInt(form.rateLimitRequests) | |
| : null, | |
| rateLimitCost: | |
| form.rateLimitCost !== '' && form.rateLimitCost !== null | |
| ? parseFloat(form.rateLimitCost) | |
| : null, | |
| concurrencyLimit: | |
| form.concurrencyLimit !== '' && form.concurrencyLimit !== null | |
| ? parseInt(form.concurrencyLimit) | |
| : 0, | |
| dailyCostLimit: | |
| form.dailyCostLimit !== '' && form.dailyCostLimit !== null | |
| ? parseFloat(form.dailyCostLimit) | |
| : 0, | |
| totalCostLimit: | |
| form.totalCostLimit !== '' && form.totalCostLimit !== null | |
| ? parseFloat(form.totalCostLimit) | |
| : 0, | |
| weeklyOpusCostLimit: | |
| form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null | |
| ? parseFloat(form.weeklyOpusCostLimit) | |
| : 0, | |
| expiresAt: form.expirationMode === 'fixed' ? form.expiresAt || undefined : undefined, | |
| expirationMode: form.expirationMode, | |
| activationDays: form.expirationMode === 'activation' ? form.activationDays : undefined, | |
| activationUnit: form.expirationMode === 'activation' ? form.activationUnit : undefined, | |
| permissions: form.permissions, | |
| tags: form.tags.length > 0 ? form.tags : undefined, | |
| enableModelRestriction: form.enableModelRestriction, | |
| restrictedModels: form.restrictedModels, | |
| enableClientRestriction: form.enableClientRestriction, | |
| allowedClients: form.allowedClients | |
| } | |
| // 处理Claude账户绑定(区分OAuth和Console) | |
| if (form.claudeAccountId) { | |
| if (form.claudeAccountId.startsWith('console:')) { | |
| // Claude Console账户 | |
| baseData.claudeConsoleAccountId = form.claudeAccountId.substring(8) | |
| // 确保不会同时设置OAuth账号 | |
| delete baseData.claudeAccountId | |
| } else { | |
| // Claude OAuth账户或分组 | |
| baseData.claudeAccountId = form.claudeAccountId | |
| // 确保不会同时设置Console账号 | |
| delete baseData.claudeConsoleAccountId | |
| } | |
| } | |
| // Gemini账户绑定 | |
| if (form.geminiAccountId) { | |
| baseData.geminiAccountId = form.geminiAccountId | |
| } | |
| // OpenAI账户绑定 | |
| if (form.openaiAccountId) { | |
| baseData.openaiAccountId = form.openaiAccountId | |
| } | |
| // Bedrock账户绑定 | |
| if (form.bedrockAccountId) { | |
| baseData.bedrockAccountId = form.bedrockAccountId | |
| } | |
| if (form.droidAccountId) { | |
| baseData.droidAccountId = form.droidAccountId | |
| } | |
| if (form.createType === 'single') { | |
| // 单个创建 | |
| const data = { | |
| ...baseData, | |
| name: form.name | |
| } | |
| const result = await apiClient.post('/admin/api-keys', data) | |
| if (result.success) { | |
| showToast('API Key 创建成功', 'success') | |
| emit('success', result.data) | |
| emit('close') | |
| } else { | |
| showToast(result.message || '创建失败', 'error') | |
| } | |
| } else { | |
| // 批量创建 | |
| const data = { | |
| ...baseData, | |
| createType: 'batch', | |
| baseName: form.name, | |
| count: form.batchCount | |
| } | |
| const result = await apiClient.post('/admin/api-keys/batch', data) | |
| if (result.success) { | |
| showToast(`成功创建 ${result.data.length} 个 API Key`, 'success') | |
| emit('batch-success', result.data) | |
| emit('close') | |
| } else { | |
| showToast(result.message || '批量创建失败', 'error') | |
| } | |
| } | |
| } catch (error) { | |
| showToast('创建失败', 'error') | |
| } finally { | |
| loading.value = false | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| /* 表单样式由全局样式提供 */ | |
| </style> | |