File size: 31,456 Bytes
69b897d 75031b4 69b897d 75031b4 69b897d 75031b4 69b897d 75031b4 69b897d 75031b4 69b897d 75031b4 69b897d 75031b4 69b897d 75031b4 69b897d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 |
<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 md:p-8"
>
<div class="mb-4 flex items-center justify-between sm:mb-6">
<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-edit 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 Keys ({{ selectedCount }} 个)
</h3>
</div>
<button
class="p-1 text-gray-400 transition-colors hover:text-gray-600 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 sm:space-y-6"
@submit.prevent="batchUpdateApiKeys"
>
<!-- 说明文本 -->
<div class="rounded-lg bg-blue-50 p-4 dark:bg-blue-900/20">
<div class="flex items-start gap-3">
<i class="fas fa-info-circle mt-1 text-blue-500" />
<div>
<p class="text-sm font-medium text-blue-800 dark:text-blue-300">批量编辑说明</p>
<p class="mt-1 text-sm text-blue-700 dark:text-blue-400">
以下设置将应用到所选的 {{ selectedCount }} 个 API
Key。只有填写或修改的字段才会被更新,空白字段将保持原值不变。
</p>
</div>
</div>
</div>
<!-- 标签编辑 -->
<div>
<label
class="mb-1.5 block text-xs font-semibold text-gray-700 dark:text-gray-300 sm:mb-3 sm:text-sm"
>
标签 (批量操作)
</label>
<div class="space-y-4">
<!-- 标签操作模式选择 -->
<div class="flex flex-wrap gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="replace" />
<span class="text-sm text-gray-700 dark:text-gray-300">替换标签</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="add" />
<span class="text-sm text-gray-700 dark:text-gray-300">添加标签</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="remove" />
<span class="text-sm text-gray-700 dark:text-gray-300">移除标签</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="tagOperation" class="mr-2" type="radio" value="none" />
<span class="text-sm text-gray-700 dark:text-gray-300">不修改标签</span>
</label>
</div>
<!-- 标签编辑区域 -->
<div v-if="tagOperation !== 'none'" class="space-y-3">
<!-- 已选择的标签 -->
<div v-if="form.tags.length > 0">
<div class="mb-2 text-xs font-medium text-gray-600 dark:text-gray-400">
{{
tagOperation === 'replace'
? '新标签列表:'
: tagOperation === 'add'
? '要添加的标签:'
: '要移除的标签:'
}}
</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-300"
>
{{ tag }}
<button
class="ml-1 hover:text-blue-900"
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-300"
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-800 dark:text-gray-200"
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 dark:bg-green-600 dark:hover:bg-green-700"
type="button"
@click="addTag"
>
<i class="fas fa-plus" />
</button>
</div>
</div>
</div>
</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-800 dark:text-gray-200"
min="1"
placeholder="不修改"
type="number"
/>
</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-800 dark:text-gray-200"
min="1"
placeholder="不修改"
type="number"
/>
</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-800 dark:text-gray-200"
min="0"
placeholder="不修改"
step="0.01"
type="number"
/>
</div>
</div>
</div>
</div>
<!-- 每日费用限制 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
每日费用限制 (美元)
</label>
<input
v-model="form.dailyCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
step="0.01"
type="number"
/>
</div>
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
总费用限制 (美元)
</label>
<input
v-model="form.totalCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
step="0.01"
type="number"
/>
</div>
<!-- Opus 模型周费用限制 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300">
Opus 模型周费用限制 (美元)
</label>
<input
v-model="form.weeklyOpusCostLimit"
class="form-input w-full border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
step="0.01"
type="number"
/>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
设置 Opus 模型的周费用限制(周一到周日),仅限 Claude 官方账户
</p>
</div>
<!-- 并发限制 -->
<div>
<label class="mb-3 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-800 dark:text-gray-200"
min="0"
placeholder="不修改 (0 表示无限制)"
type="number"
/>
</div>
<!-- 激活状态 -->
<div>
<div class="mb-3 flex items-center gap-4">
<label class="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.isActive" class="mr-2" type="radio" :value="true" />
<span class="text-sm text-gray-700 dark:text-gray-300">激活</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.isActive" class="mr-2" type="radio" :value="false" />
<span class="text-sm text-gray-700 dark:text-gray-300">禁用</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.isActive" class="mr-2" type="radio" :value="null" />
<span class="text-sm text-gray-700 dark:text-gray-300">不修改</span>
</label>
</div>
</div>
</div>
<!-- 服务权限 -->
<div>
<label class="mb-3 block text-sm font-semibold text-gray-700 dark:text-gray-300"
>服务权限</label
>
<div class="flex flex-wrap gap-4">
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="" />
<span class="text-sm text-gray-700">不修改</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="all" />
<span class="text-sm text-gray-700">全部服务</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="claude" />
<span class="text-sm text-gray-700">仅 Claude</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="gemini" />
<span class="text-sm text-gray-700">仅 Gemini</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="openai" />
<span class="text-sm text-gray-700">仅 OpenAI</span>
</label>
<label class="flex cursor-pointer items-center">
<input v-model="form.permissions" class="mr-2" type="radio" value="droid" />
<span class="text-sm text-gray-700">仅 Droid</span>
</label>
</div>
</div>
<!-- 专属账号绑定 -->
<div>
<div class="mb-3 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="claudeAccountSelectorValue"
:accounts="localAccounts.claude"
default-option-text="请选择Claude账号"
:disabled="!isServiceSelectable('claude')"
:groups="localAccounts.claudeGroups"
placeholder="请选择Claude账号"
platform="claude"
:special-options="accountSpecialOptions"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Gemini 专属账号</label
>
<AccountSelector
v-model="geminiAccountSelectorValue"
:accounts="localAccounts.gemini"
default-option-text="请选择Gemini账号"
:disabled="!isServiceSelectable('gemini')"
:groups="localAccounts.geminiGroups"
placeholder="请选择Gemini账号"
platform="gemini"
:special-options="accountSpecialOptions"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>OpenAI 专属账号</label
>
<AccountSelector
v-model="openaiAccountSelectorValue"
:accounts="localAccounts.openai"
default-option-text="请选择OpenAI账号"
:disabled="!isServiceSelectable('openai')"
:groups="localAccounts.openaiGroups"
placeholder="请选择OpenAI账号"
platform="openai"
:special-options="accountSpecialOptions"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Bedrock 专属账号</label
>
<AccountSelector
v-model="bedrockAccountSelectorValue"
:accounts="localAccounts.bedrock"
default-option-text="请选择Bedrock账号"
:disabled="!isServiceSelectable('openai')"
:groups="[]"
placeholder="请选择Bedrock账号"
platform="bedrock"
:special-options="accountSpecialOptions"
/>
</div>
<div>
<label class="mb-1 block text-sm font-medium text-gray-600 dark:text-gray-400"
>Droid 专属账号</label
>
<AccountSelector
v-model="droidAccountSelectorValue"
:accounts="localAccounts.droid"
default-option-text="请选择Droid账号"
:disabled="!isServiceSelectable('droid')"
:groups="localAccounts.droidGroups"
placeholder="请选择Droid账号"
platform="droid"
:special-options="accountSpecialOptions"
/>
</div>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
class="flex-1 rounded-xl bg-gray-100 px-6 py-3 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-6 py-3 font-semibold"
:disabled="loading"
type="submit"
>
<div v-if="loading" class="loading-spinner mr-2" />
<i v-else class="fas fa-save 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 { useApiKeysStore } from '@/stores/apiKeys'
import { apiClient } from '@/config/api'
import AccountSelector from '@/components/common/AccountSelector.vue'
const props = defineProps({
selectedKeys: {
type: Array,
required: true
},
accounts: {
type: Object,
default: () => ({
claude: [],
gemini: [],
openai: [],
openaiResponses: [],
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: [],
droidGroups: []
})
}
})
const emit = defineEmits(['close', 'success'])
const apiKeysStore = useApiKeysStore()
const loading = ref(false)
const accountsLoading = ref(false)
const localAccounts = ref({
claude: [],
gemini: [],
openai: [],
bedrock: [],
droid: [],
claudeGroups: [],
geminiGroups: [],
openaiGroups: [],
droidGroups: []
})
// 标签相关
const newTag = ref('')
const availableTags = ref([])
const tagOperation = ref('none') // 'replace', 'add', 'remove', 'none'
const selectedCount = computed(() => props.selectedKeys.length)
// 计算未选择的标签
const unselectedTags = computed(() => {
return availableTags.value.filter((tag) => !form.tags.includes(tag))
})
// 表单数据
const form = reactive({
rateLimitCost: '', // 费用限制替代token限制
rateLimitWindow: '',
rateLimitRequests: '',
concurrencyLimit: '',
dailyCostLimit: '',
totalCostLimit: '',
weeklyOpusCostLimit: '', // 新增Opus周费用限制
permissions: '', // 空字符串表示不修改
claudeAccountId: '',
geminiAccountId: '',
openaiAccountId: '',
bedrockAccountId: '',
droidAccountId: '',
tags: [],
isActive: null // null表示不修改
})
const UNCHANGED_OPTION_VALUE = '__KEEP_ORIGINAL__'
const accountSpecialOptions = [
{ value: UNCHANGED_OPTION_VALUE, label: '不修改' },
{ value: 'SHARED_POOL', label: '使用共享账号池' }
]
const createAccountSelectorModel = (field) =>
computed({
get: () => (form[field] === '' ? UNCHANGED_OPTION_VALUE : form[field]),
set: (value) => {
if (!value || value === UNCHANGED_OPTION_VALUE) {
form[field] = ''
} else {
form[field] = value
}
}
})
const claudeAccountSelectorValue = createAccountSelectorModel('claudeAccountId')
const geminiAccountSelectorValue = createAccountSelectorModel('geminiAccountId')
const openaiAccountSelectorValue = createAccountSelectorModel('openaiAccountId')
const bedrockAccountSelectorValue = createAccountSelectorModel('bedrockAccountId')
const droidAccountSelectorValue = createAccountSelectorModel('droidAccountId')
const isServiceSelectable = (service) => {
if (!form.permissions) return true
if (form.permissions === 'all') return true
return form.permissions === service
}
// 标签管理方法
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 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'),
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'
}))
}
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
}
}
// 批量更新API Keys
const batchUpdateApiKeys = async () => {
loading.value = true
try {
// 准备提交的数据
const updates = {}
// 只有非空值才添加到更新对象中
if (form.rateLimitCost !== '' && form.rateLimitCost !== null) {
updates.rateLimitCost = parseFloat(form.rateLimitCost)
}
if (form.rateLimitWindow !== '' && form.rateLimitWindow !== null) {
updates.rateLimitWindow = parseInt(form.rateLimitWindow)
}
if (form.rateLimitRequests !== '' && form.rateLimitRequests !== null) {
updates.rateLimitRequests = parseInt(form.rateLimitRequests)
}
if (form.concurrencyLimit !== '' && form.concurrencyLimit !== null) {
updates.concurrencyLimit = parseInt(form.concurrencyLimit)
}
if (form.dailyCostLimit !== '' && form.dailyCostLimit !== null) {
updates.dailyCostLimit = parseFloat(form.dailyCostLimit)
}
if (form.totalCostLimit !== '' && form.totalCostLimit !== null) {
updates.totalCostLimit = parseFloat(form.totalCostLimit)
}
if (form.weeklyOpusCostLimit !== '' && form.weeklyOpusCostLimit !== null) {
updates.weeklyOpusCostLimit = parseFloat(form.weeklyOpusCostLimit)
}
// 权限设置
if (form.permissions !== '') {
updates.permissions = form.permissions
}
// 账户绑定
if (form.claudeAccountId !== '') {
if (form.claudeAccountId === 'SHARED_POOL') {
updates.claudeAccountId = null
updates.claudeConsoleAccountId = null
} else if (form.claudeAccountId.startsWith('console:')) {
updates.claudeConsoleAccountId = form.claudeAccountId.substring(8)
updates.claudeAccountId = null
} else if (!form.claudeAccountId.startsWith('group:')) {
updates.claudeAccountId = form.claudeAccountId
updates.claudeConsoleAccountId = null
} else {
updates.claudeAccountId = form.claudeAccountId
updates.claudeConsoleAccountId = null
}
}
if (form.geminiAccountId !== '') {
if (form.geminiAccountId === 'SHARED_POOL') {
updates.geminiAccountId = null
} else {
updates.geminiAccountId = form.geminiAccountId
}
}
if (form.openaiAccountId !== '') {
if (form.openaiAccountId === 'SHARED_POOL') {
updates.openaiAccountId = null
} else {
updates.openaiAccountId = form.openaiAccountId
}
}
if (form.bedrockAccountId !== '') {
if (form.bedrockAccountId === 'SHARED_POOL') {
updates.bedrockAccountId = null
} else {
updates.bedrockAccountId = form.bedrockAccountId
}
}
if (form.droidAccountId !== '') {
if (form.droidAccountId === 'SHARED_POOL') {
updates.droidAccountId = null
} else {
updates.droidAccountId = form.droidAccountId
}
}
// 激活状态
if (form.isActive !== null) {
updates.isActive = form.isActive
}
// 标签处理
if (tagOperation.value !== 'none') {
updates.tags = form.tags
updates.tagOperation = tagOperation.value
}
const result = await apiClient.put('/admin/api-keys/batch', {
keyIds: props.selectedKeys,
updates
})
if (result.success) {
const { successCount, failedCount, errors } = result.data
if (successCount > 0) {
showToast(`成功批量编辑 ${successCount} 个 API Keys`, 'success')
if (failedCount > 0) {
const errorMessages = errors.map((e) => `${e.keyId}: ${e.error}`).join('\n')
showToast(`${failedCount} 个编辑失败:\n${errorMessages}`, 'warning')
}
} else {
showToast('所有 API Keys 编辑失败', 'error')
}
emit('success')
emit('close')
} else {
showToast(result.message || '批量编辑失败', 'error')
}
} catch (error) {
showToast('批量编辑失败', 'error')
console.error('批量编辑 API Keys 失败:', error)
} finally {
loading.value = false
}
}
onMounted(async () => {
// 加载已存在的标签
availableTags.value = await apiKeysStore.fetchTags()
// 初始化账号数据
if (props.accounts) {
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 || [],
claudeGroups: props.accounts.claudeGroups || [],
geminiGroups: props.accounts.geminiGroups || [],
openaiGroups: props.accounts.openaiGroups || [],
droidGroups: props.accounts.droidGroups || []
}
}
})
</script>
<style scoped>
/* 表单样式由全局样式提供 */
</style>
|