fix: 修复账户管理和API Key管理的多个问题

- 修复 Claude Console 账号 supportedModels 导致的 be.join 错误
- 添加专属账号选择器组件,支持搜索和按创建时间倒序排序
- 修复搜索框图标与placeholder文字重叠的问题
- 修复选择共享账号池后显示文字不正确的问题
- 优化下拉框定位逻辑,解决超出视窗无法滚动的问题

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
shaw
2025-08-04 18:44:36 +08:00
parent 8ece285f5f
commit a69d1ae1dd
4 changed files with 557 additions and 157 deletions

View File

@@ -870,7 +870,19 @@ const form = ref({
apiUrl: props.account?.apiUrl || '',
apiKey: props.account?.apiKey || '',
priority: props.account?.priority || 50,
supportedModels: props.account?.supportedModels?.join('\n') || '',
supportedModels: (() => {
const models = props.account?.supportedModels;
if (!models) return '';
// 处理对象格式Claude Console 的新格式)
if (typeof models === 'object' && !Array.isArray(models)) {
return Object.keys(models).join('\n');
}
// 处理数组格式(向后兼容)
if (Array.isArray(models)) {
return models.join('\n');
}
return '';
})(),
userAgent: props.account?.userAgent || '',
rateLimitDuration: props.account?.rateLimitDuration || 60
})
@@ -1362,7 +1374,19 @@ watch(() => props.account, (newAccount) => {
apiUrl: newAccount.apiUrl || '',
apiKey: '', // 编辑模式不显示现有的 API Key
priority: newAccount.priority || 50,
supportedModels: newAccount.supportedModels?.join('\n') || '',
supportedModels: (() => {
const models = newAccount.supportedModels;
if (!models) return '';
// 处理对象格式Claude Console 的新格式)
if (typeof models === 'object' && !Array.isArray(models)) {
return Object.keys(models).join('\n');
}
// 处理数组格式(向后兼容)
if (Array.isArray(models)) {
return models.join('\n');
}
return '';
})(),
userAgent: newAccount.userAgent || '',
rateLimitDuration: newAccount.rateLimitDuration || 60
}

View File

@@ -425,87 +425,27 @@
<div class="grid grid-cols-1 gap-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
<select
v-model="form.claudeAccountId"
class="form-input w-full"
<AccountSelector
v-model="form.claudeAccountId"
platform="claude"
:accounts="localAccounts.claude"
:groups="localAccounts.claudeGroups"
:disabled="form.permissions === 'gemini'"
>
<option value="">
使用共享账号池
</option>
<optgroup
v-if="localAccounts.claudeGroups && localAccounts.claudeGroups.length > 0"
label="调度分组"
>
<option
v-for="group in localAccounts.claudeGroups"
:key="`group:${group.id}`"
:value="`group:${group.id}`"
>
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
</option>
</optgroup>
<optgroup
v-if="localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-oauth').length > 0"
label="Claude OAuth 专属账号"
>
<option
v-for="account in localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-oauth')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
<optgroup
v-if="localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-console').length > 0"
label="Claude Console 专属账号"
>
<option
v-for="account in localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-console')"
:key="account.id"
:value="`console:${account.id}`"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
placeholder="请选择Claude账号"
default-option-text="使用共享账号池"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
<AccountSelector
v-model="form.geminiAccountId"
platform="gemini"
:accounts="localAccounts.gemini"
:groups="localAccounts.geminiGroups"
:disabled="form.permissions === 'claude'"
>
<option value="">
使用共享账号池
</option>
<optgroup
v-if="localAccounts.geminiGroups && localAccounts.geminiGroups.length > 0"
label="调度分组"
>
<option
v-for="group in localAccounts.geminiGroups"
:key="`group:${group.id}`"
:value="`group:${group.id}`"
>
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
</option>
</optgroup>
<optgroup
v-if="localAccounts.gemini.filter(a => a.accountType === 'dedicated').length > 0"
label="Gemini 专属账号"
>
<option
v-for="account in localAccounts.gemini.filter(a => a.accountType === 'dedicated')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
placeholder="请选择Gemini账号"
default-option-text="使用共享账号池"
/>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
@@ -665,6 +605,7 @@ 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: {

View File

@@ -294,87 +294,27 @@
<div class="grid grid-cols-1 gap-3">
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Claude 专属账号</label>
<select
v-model="form.claudeAccountId"
class="form-input w-full"
<AccountSelector
v-model="form.claudeAccountId"
platform="claude"
:accounts="localAccounts.claude"
:groups="localAccounts.claudeGroups"
:disabled="form.permissions === 'gemini'"
>
<option value="">
使用共享账号池
</option>
<optgroup
v-if="localAccounts.claudeGroups && localAccounts.claudeGroups.length > 0"
label="调度分组"
>
<option
v-for="group in localAccounts.claudeGroups"
:key="`group:${group.id}`"
:value="`group:${group.id}`"
>
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
</option>
</optgroup>
<optgroup
v-if="localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-oauth').length > 0"
label="Claude OAuth 专属账号"
>
<option
v-for="account in localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-oauth')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
<optgroup
v-if="localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-console').length > 0"
label="Claude Console 专属账号"
>
<option
v-for="account in localAccounts.claude.filter(a => a.accountType === 'dedicated' && a.platform === 'claude-console')"
:key="account.id"
:value="`console:${account.id}`"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
placeholder="请选择Claude账号"
default-option-text="使用共享账号池"
/>
</div>
<div>
<label class="block text-sm font-medium text-gray-600 mb-1">Gemini 专属账号</label>
<select
v-model="form.geminiAccountId"
class="form-input w-full"
<AccountSelector
v-model="form.geminiAccountId"
platform="gemini"
:accounts="localAccounts.gemini"
:groups="localAccounts.geminiGroups"
:disabled="form.permissions === 'claude'"
>
<option value="">
使用共享账号池
</option>
<optgroup
v-if="localAccounts.geminiGroups && localAccounts.geminiGroups.length > 0"
label="调度分组"
>
<option
v-for="group in localAccounts.geminiGroups"
:key="`group:${group.id}`"
:value="`group:${group.id}`"
>
{{ group.name }} ({{ group.memberCount || 0 }} 个成员)
</option>
</optgroup>
<optgroup
v-if="localAccounts.gemini.filter(a => a.accountType === 'dedicated').length > 0"
label="Gemini 专属账号"
>
<option
v-for="account in localAccounts.gemini.filter(a => a.accountType === 'dedicated')"
:key="account.id"
:value="account.id"
>
{{ account.name }} ({{ account.status === 'active' ? '正常' : '异常' }})
</option>
</optgroup>
</select>
placeholder="请选择Gemini账号"
default-option-text="使用共享账号池"
/>
</div>
</div>
<p class="text-xs text-gray-500 mt-2">
@@ -538,6 +478,7 @@ import { useAuthStore } from '@/stores/auth'
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({
apiKey: {
@@ -638,11 +579,32 @@ const updateApiKey = async () => {
concurrencyLimit: form.concurrencyLimit !== '' && form.concurrencyLimit !== null ? parseInt(form.concurrencyLimit) : 0,
dailyCostLimit: form.dailyCostLimit !== '' && form.dailyCostLimit !== null ? parseFloat(form.dailyCostLimit) : 0,
permissions: form.permissions,
claudeAccountId: form.claudeAccountId || null,
geminiAccountId: form.geminiAccountId || null,
tags: form.tags
}
// 处理Claude账户绑定区分OAuth和Console
if (form.claudeAccountId) {
if (form.claudeAccountId.startsWith('console:')) {
// Claude Console账户
data.claudeConsoleAccountId = form.claudeAccountId.substring(8);
} else if (!form.claudeAccountId.startsWith('group:')) {
// Claude OAuth账户非分组
data.claudeAccountId = form.claudeAccountId;
} else {
// 分组
data.claudeAccountId = form.claudeAccountId;
}
} else {
data.claudeAccountId = null;
}
// Gemini账户绑定
if (form.geminiAccountId) {
data.geminiAccountId = form.geminiAccountId;
} else {
data.geminiAccountId = null;
}
// 模型限制 - 始终提交这些字段
data.enableModelRestriction = form.enableModelRestriction
data.restrictedModels = form.restrictedModels
@@ -747,7 +709,12 @@ onMounted(async () => {
form.concurrencyLimit = props.apiKey.concurrencyLimit || ''
form.dailyCostLimit = props.apiKey.dailyCostLimit || ''
form.permissions = props.apiKey.permissions || 'all'
form.claudeAccountId = props.apiKey.claudeAccountId || ''
// 处理 Claude 账号(区分 OAuth 和 Console
if (props.apiKey.claudeConsoleAccountId) {
form.claudeAccountId = `console:${props.apiKey.claudeConsoleAccountId}`
} else {
form.claudeAccountId = props.apiKey.claudeAccountId || ''
}
form.geminiAccountId = props.apiKey.geminiAccountId || ''
form.restrictedModels = props.apiKey.restrictedModels || []
form.allowedClients = props.apiKey.allowedClients || []

View File

@@ -0,0 +1,468 @@
<template>
<div ref="triggerRef" class="relative">
<!-- 选择器主体 -->
<div
class="form-input w-full cursor-pointer flex items-center justify-between"
:class="{ 'opacity-50': disabled }"
@click="!disabled && toggleDropdown()"
>
<span :class="modelValue ? 'text-gray-900' : 'text-gray-500'">{{ selectedLabel }}</span>
<i class="fas fa-chevron-down text-gray-400 transition-transform duration-200" :class="{ 'rotate-180': showDropdown }" />
</div>
<!-- 下拉菜单 -->
<Teleport to="body">
<Transition
enter-active-class="transition ease-out duration-100"
enter-from-class="transform opacity-0 scale-95"
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div
v-if="showDropdown"
ref="dropdownRef"
class="absolute z-50 bg-white rounded-lg shadow-lg border border-gray-200 flex flex-col"
:style="dropdownStyle"
>
<!-- 搜索框 -->
<div class="p-3 border-b border-gray-200 flex-shrink-0">
<div class="relative">
<input
ref="searchInput"
v-model="searchQuery"
type="text"
placeholder="搜索账号名称..."
class="form-input w-full text-sm"
style="padding-left: 40px; padding-right: 36px;"
@input="handleSearch"
>
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none" />
<button
v-if="searchQuery"
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
@click="clearSearch"
>
<i class="fas fa-times text-sm" />
</button>
</div>
</div>
<!-- 选项列表 -->
<div class="flex-1 overflow-y-auto custom-scrollbar">
<!-- 默认选项 -->
<div
class="px-4 py-2 cursor-pointer hover:bg-gray-50 transition-colors"
:class="{ 'bg-blue-50': !modelValue }"
@click="selectAccount(null)"
>
<span class="text-gray-700">{{ defaultOptionText }}</span>
</div>
<!-- 分组选项 -->
<div v-if="filteredGroups.length > 0">
<div class="px-4 py-2 text-xs font-semibold text-gray-500 bg-gray-50">
调度分组
</div>
<div
v-for="group in filteredGroups"
:key="`group:${group.id}`"
class="px-4 py-2 cursor-pointer hover:bg-gray-50 transition-colors"
:class="{ 'bg-blue-50': modelValue === `group:${group.id}` }"
@click="selectAccount(`group:${group.id}`)"
>
<div class="flex items-center justify-between">
<span class="text-gray-700">{{ group.name }}</span>
<span class="text-xs text-gray-500">{{ group.memberCount || 0 }} 个成员</span>
</div>
</div>
</div>
<!-- OAuth 账号 -->
<div v-if="filteredOAuthAccounts.length > 0">
<div class="px-4 py-2 text-xs font-semibold text-gray-500 bg-gray-50">
{{ platform === 'claude' ? 'Claude OAuth 专属账号' : 'OAuth 专属账号' }}
</div>
<div
v-for="account in filteredOAuthAccounts"
:key="account.id"
class="px-4 py-2 cursor-pointer hover:bg-gray-50 transition-colors"
:class="{ 'bg-blue-50': modelValue === account.id }"
@click="selectAccount(account.id)"
>
<div class="flex items-center justify-between">
<div>
<span class="text-gray-700">{{ account.name }}</span>
<span
class="ml-2 text-xs px-2 py-0.5 rounded-full"
:class="account.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
>
{{ account.status === 'active' ? '正常' : '异常' }}
</span>
</div>
<span class="text-xs text-gray-400">
{{ formatDate(account.createdAt) }}
</span>
</div>
</div>
</div>
<!-- Console 账号 Claude -->
<div v-if="platform === 'claude' && filteredConsoleAccounts.length > 0">
<div class="px-4 py-2 text-xs font-semibold text-gray-500 bg-gray-50">
Claude Console 专属账号
</div>
<div
v-for="account in filteredConsoleAccounts"
:key="account.id"
class="px-4 py-2 cursor-pointer hover:bg-gray-50 transition-colors"
:class="{ 'bg-blue-50': modelValue === `console:${account.id}` }"
@click="selectAccount(`console:${account.id}`)"
>
<div class="flex items-center justify-between">
<div>
<span class="text-gray-700">{{ account.name }}</span>
<span
class="ml-2 text-xs px-2 py-0.5 rounded-full"
:class="account.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
>
{{ account.status === 'active' ? '正常' : '异常' }}
</span>
</div>
<span class="text-xs text-gray-400">
{{ formatDate(account.createdAt) }}
</span>
</div>
</div>
</div>
<!-- 无搜索结果 -->
<div v-if="searchQuery && !hasResults" class="px-4 py-8 text-center text-gray-500">
<i class="fas fa-search text-2xl mb-2" />
<p class="text-sm">没有找到匹配的账号</p>
</div>
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
const props = defineProps({
modelValue: {
type: String,
default: ''
},
platform: {
type: String,
required: true,
validator: (value) => ['claude', 'gemini'].includes(value)
},
accounts: {
type: Array,
default: () => []
},
groups: {
type: Array,
default: () => []
},
disabled: {
type: Boolean,
default: false
},
placeholder: {
type: String,
default: '请选择账号'
},
defaultOptionText: {
type: String,
default: '使用共享账号池'
}
})
const emit = defineEmits(['update:modelValue'])
const showDropdown = ref(false)
const searchQuery = ref('')
const searchInput = ref(null)
const dropdownRef = ref(null)
const dropdownStyle = ref({})
const triggerRef = ref(null)
const lastDirection = ref('') // 记住上次的显示方向
// 获取选中的标签
const selectedLabel = computed(() => {
// 如果没有选中值,显示默认选项文本
if (!props.modelValue) return props.defaultOptionText
// 分组
if (props.modelValue.startsWith('group:')) {
const groupId = props.modelValue.substring(6)
const group = props.groups.find(g => g.id === groupId)
return group ? `${group.name} (${group.memberCount || 0} 个成员)` : ''
}
// Console 账号
if (props.modelValue.startsWith('console:')) {
const accountId = props.modelValue.substring(8)
const account = props.accounts.find(a => a.id === accountId && a.platform === 'claude-console')
return account ? `${account.name} (${account.status === 'active' ? '正常' : '异常'})` : ''
}
// OAuth 账号
const account = props.accounts.find(a => a.id === props.modelValue)
return account ? `${account.name} (${account.status === 'active' ? '正常' : '异常'})` : ''
})
// 按创建时间倒序排序账号
const sortedAccounts = computed(() => {
return [...props.accounts].sort((a, b) => {
const dateA = new Date(a.createdAt || 0)
const dateB = new Date(b.createdAt || 0)
return dateB - dateA // 倒序排序
})
})
// 过滤的分组
const filteredGroups = computed(() => {
if (!searchQuery.value) return props.groups
const query = searchQuery.value.toLowerCase()
return props.groups.filter(group =>
group.name.toLowerCase().includes(query)
)
})
// 过滤的 OAuth 账号
const filteredOAuthAccounts = computed(() => {
let accounts = sortedAccounts.value.filter(a =>
a.accountType === 'dedicated' &&
(props.platform === 'claude' ? a.platform === 'claude-oauth' : a.platform !== 'claude-console')
)
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
accounts = accounts.filter(account =>
account.name.toLowerCase().includes(query)
)
}
return accounts
})
// 过滤的 Console 账号
const filteredConsoleAccounts = computed(() => {
if (props.platform !== 'claude') return []
let accounts = sortedAccounts.value.filter(a =>
a.accountType === 'dedicated' && a.platform === 'claude-console'
)
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
accounts = accounts.filter(account =>
account.name.toLowerCase().includes(query)
)
}
return accounts
})
// 是否有搜索结果
const hasResults = computed(() => {
return filteredGroups.value.length > 0 ||
filteredOAuthAccounts.value.length > 0 ||
filteredConsoleAccounts.value.length > 0
})
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return ''
const date = new Date(dateString)
const now = new Date()
const diffInHours = (now - date) / (1000 * 60 * 60)
if (diffInHours < 24) {
return '今天创建'
} else if (diffInHours < 48) {
return '昨天创建'
} else if (diffInHours < 168) { // 7天内
return `${Math.floor(diffInHours / 24)} 天前`
} else {
return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' })
}
}
// 更新下拉菜单位置
const updateDropdownPosition = () => {
if (!showDropdown.value || !dropdownRef.value || !triggerRef.value) return
const trigger = triggerRef.value
if (!trigger) return
const rect = trigger.getBoundingClientRect()
const windowHeight = window.innerHeight
const windowWidth = window.innerWidth
const spaceBelow = windowHeight - rect.bottom
const spaceAbove = rect.top
const margin = 8 // 边距
// 获取下拉框的高度
const dropdownHeight = dropdownRef.value.offsetHeight
// 计算最大可用高度
const maxHeightBelow = spaceBelow - margin
const maxHeightAbove = spaceAbove - margin
// 决定显示方向和最大高度
let showAbove = false
let maxHeight = maxHeightBelow
// 优先使用上次的方向,除非空间不足
if (lastDirection.value === 'above' && maxHeightAbove >= 150) {
showAbove = true
maxHeight = maxHeightAbove
} else if (lastDirection.value === 'below' && maxHeightBelow >= 150) {
showAbove = false
maxHeight = maxHeightBelow
} else {
// 如果没有历史方向或空间不足,选择空间更大的方向
if (maxHeightAbove > maxHeightBelow && maxHeightBelow < 200) {
showAbove = true
maxHeight = maxHeightAbove
}
}
// 记住这次的方向
lastDirection.value = showAbove ? 'above' : 'below'
// 确保下拉框不超出视窗左右边界
let left = rect.left
const dropdownWidth = rect.width
if (left + dropdownWidth > windowWidth - margin) {
left = windowWidth - dropdownWidth - margin
}
if (left < margin) {
left = margin
}
dropdownStyle.value = {
position: 'fixed',
left: `${left}px`,
width: `${rect.width}px`,
maxHeight: `${Math.min(maxHeight, 400)}px`, // 限制最大高度为400px
...(showAbove
? { bottom: `${windowHeight - rect.top}px` }
: { top: `${rect.bottom}px` }
)
}
}
// 切换下拉菜单
const toggleDropdown = () => {
if (!showDropdown.value && triggerRef.value) {
// 在显示前就设置初始样式,避免闪烁
const rect = triggerRef.value.getBoundingClientRect()
const windowHeight = window.innerHeight
const spaceBelow = windowHeight - rect.bottom
const margin = 8
// 预先设置一个合理的初始位置
dropdownStyle.value = {
position: 'fixed',
left: `${rect.left}px`,
width: `${rect.width}px`,
maxHeight: `${Math.min(spaceBelow - margin, 400)}px`,
top: `${rect.bottom}px`
}
}
showDropdown.value = !showDropdown.value
if (showDropdown.value) {
nextTick(() => {
updateDropdownPosition()
searchInput.value?.focus()
})
}
}
// 选择账号
const selectAccount = (value) => {
emit('update:modelValue', value || '')
showDropdown.value = false
searchQuery.value = ''
}
// 处理搜索
const handleSearch = () => {
// 搜索时自动触发
}
// 清除搜索
const clearSearch = () => {
searchQuery.value = ''
searchInput.value?.focus()
}
// 点击外部关闭
const handleClickOutside = (event) => {
if (!triggerRef.value?.contains(event.target) && !dropdownRef.value?.contains(event.target)) {
showDropdown.value = false
}
}
// 监听滚动更新位置
const handleScroll = () => {
if (showDropdown.value) {
updateDropdownPosition()
}
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
window.addEventListener('scroll', handleScroll, true)
window.addEventListener('resize', updateDropdownPosition)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('scroll', handleScroll, true)
window.removeEventListener('resize', updateDropdownPosition)
})
// 监听下拉菜单状态变化
watch(showDropdown, (newVal) => {
if (!newVal) {
searchQuery.value = ''
// 关闭时重置方向,下次打开重新计算
lastDirection.value = ''
}
})
</script>
<style scoped>
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #cbd5e0 #f7fafc;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f7fafc;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background-color: #cbd5e0;
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: #a0aec0;
}
</style>