mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
style: 优化表格显示固定列宽
This commit is contained in:
174
web/admin-spa/src/components/common/ActionDropdown.vue
Normal file
174
web/admin-spa/src/components/common/ActionDropdown.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative">
|
||||||
|
<!-- 触发器按钮 -->
|
||||||
|
<button
|
||||||
|
ref="triggerRef"
|
||||||
|
class="flex h-8 w-8 items-center justify-center rounded-lg border border-gray-200 bg-white text-gray-600 transition-all duration-200 hover:border-gray-300 hover:bg-gray-50 hover:text-gray-900 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-400 dark:hover:border-gray-500 dark:hover:bg-gray-700 dark:hover:text-gray-200"
|
||||||
|
:class="[
|
||||||
|
isOpen &&
|
||||||
|
'border-blue-400 bg-blue-50 text-blue-600 dark:border-blue-500 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
|
]"
|
||||||
|
title="更多操作"
|
||||||
|
@click.stop="toggleDropdown"
|
||||||
|
>
|
||||||
|
<i class="fas fa-ellipsis-v text-sm"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- 下拉菜单 - 使用 Teleport 避免被父容器裁剪 -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<transition
|
||||||
|
enter-active-class="transition duration-200 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0"
|
||||||
|
enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-150 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
|
leave-to-class="transform scale-95 opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="isOpen"
|
||||||
|
ref="dropdownRef"
|
||||||
|
class="fixed z-[9999] min-w-[140px] overflow-hidden rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-600 dark:bg-gray-800"
|
||||||
|
:style="dropdownStyle"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-for="action in actions"
|
||||||
|
:key="action.key"
|
||||||
|
class="flex w-full items-center gap-2 whitespace-nowrap px-3 py-2 text-left text-sm transition-colors duration-150"
|
||||||
|
:class="getActionClass(action)"
|
||||||
|
@click.stop="handleAction(action)"
|
||||||
|
>
|
||||||
|
<i :class="['fas', action.icon, 'w-4 text-center text-xs']"></i>
|
||||||
|
<span>{{ action.label }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
actions: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
// 格式: [{ key: 'edit', label: '编辑', icon: 'fa-edit', color: 'blue', handler: () => {} }]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['action'])
|
||||||
|
|
||||||
|
const isOpen = ref(false)
|
||||||
|
const triggerRef = ref(null)
|
||||||
|
const dropdownRef = ref(null)
|
||||||
|
const dropdownStyle = ref({})
|
||||||
|
|
||||||
|
const getActionClass = (action) => {
|
||||||
|
const colorMap = {
|
||||||
|
purple: 'text-purple-600 hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-purple-900/20',
|
||||||
|
indigo: 'text-indigo-600 hover:bg-indigo-50 dark:text-indigo-400 dark:hover:bg-indigo-900/20',
|
||||||
|
blue: 'text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/20',
|
||||||
|
green: 'text-green-600 hover:bg-green-50 dark:text-green-400 dark:hover:bg-green-900/20',
|
||||||
|
orange: 'text-orange-600 hover:bg-orange-50 dark:text-orange-400 dark:hover:bg-orange-900/20',
|
||||||
|
red: 'text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/20',
|
||||||
|
gray: 'text-gray-600 hover:bg-gray-50 dark:text-gray-400 dark:hover:bg-gray-700'
|
||||||
|
}
|
||||||
|
return colorMap[action.color] || colorMap.gray
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleDropdown = async () => {
|
||||||
|
isOpen.value = !isOpen.value
|
||||||
|
if (isOpen.value) {
|
||||||
|
await nextTick()
|
||||||
|
updateDropdownPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDropdown = () => {
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAction = (action) => {
|
||||||
|
closeDropdown()
|
||||||
|
if (action.handler) {
|
||||||
|
action.handler()
|
||||||
|
}
|
||||||
|
emit('action', action.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDropdownPosition = () => {
|
||||||
|
if (!triggerRef.value || !isOpen.value) return
|
||||||
|
|
||||||
|
const trigger = triggerRef.value.getBoundingClientRect()
|
||||||
|
const dropdownHeight = 200 // 预估高度
|
||||||
|
const dropdownWidth = 160 // 预估宽度
|
||||||
|
const spaceBelow = window.innerHeight - trigger.bottom
|
||||||
|
const spaceAbove = trigger.top
|
||||||
|
const spaceRight = window.innerWidth - trigger.right
|
||||||
|
const spaceLeft = trigger.left
|
||||||
|
|
||||||
|
let top, left
|
||||||
|
|
||||||
|
// 计算垂直位置
|
||||||
|
if (spaceBelow >= dropdownHeight || spaceBelow >= spaceAbove) {
|
||||||
|
top = trigger.bottom + 4
|
||||||
|
} else {
|
||||||
|
top = trigger.top - dropdownHeight - 4
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算水平位置 - 优先显示在左侧(因为按钮在右侧固定列)
|
||||||
|
if (spaceLeft >= dropdownWidth) {
|
||||||
|
left = trigger.left - dropdownWidth + trigger.width
|
||||||
|
} else if (spaceRight >= dropdownWidth) {
|
||||||
|
left = trigger.left
|
||||||
|
} else {
|
||||||
|
left = window.innerWidth - dropdownWidth - 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保不超出边界
|
||||||
|
if (left < 10) left = 10
|
||||||
|
if (top < 10) top = 10
|
||||||
|
|
||||||
|
dropdownStyle.value = {
|
||||||
|
top: `${top}px`,
|
||||||
|
left: `${left}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleScroll = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
updateDropdownPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
if (isOpen.value) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (!triggerRef.value || !isOpen.value) return
|
||||||
|
|
||||||
|
if (!triggerRef.value.contains(event.target)) {
|
||||||
|
if (dropdownRef.value && !dropdownRef.value.contains(event.target)) {
|
||||||
|
closeDropdown()
|
||||||
|
} else if (!dropdownRef.value) {
|
||||||
|
closeDropdown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('scroll', handleScroll, true)
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('scroll', handleScroll, true)
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
document.removeEventListener('click', handleClickOutside)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -161,11 +161,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 桌面端表格视图 -->
|
<!-- 桌面端表格视图 -->
|
||||||
<div v-else class="table-container hidden md:block">
|
<div v-else class="table-wrapper hidden md:block">
|
||||||
<table class="w-full table-fixed">
|
<div class="table-container">
|
||||||
<thead class="bg-gray-50/80 backdrop-blur-sm dark:bg-gray-700/80">
|
<table class="w-full">
|
||||||
|
<thead
|
||||||
|
class="sticky top-0 z-10 bg-gradient-to-b from-gray-50 to-gray-100/90 backdrop-blur-sm dark:from-gray-700 dark:to-gray-800/90"
|
||||||
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="shouldShowCheckboxes" class="w-[50px] px-3 py-4 text-left">
|
<th
|
||||||
|
v-if="shouldShowCheckboxes"
|
||||||
|
class="checkbox-column sticky left-0 z-20 min-w-[50px] px-3 py-4 text-left"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="selectAllChecked"
|
v-model="selectAllChecked"
|
||||||
@@ -177,7 +183,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[22%] min-w-[180px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="name-column sticky z-20 min-w-[180px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
:class="shouldShowCheckboxes ? 'left-[50px]' : 'left-0'"
|
||||||
@click="sortAccounts('name')"
|
@click="sortAccounts('name')"
|
||||||
>
|
>
|
||||||
名称
|
名称
|
||||||
@@ -192,7 +199,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[15%] min-w-[120px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[220px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortAccounts('platform')"
|
@click="sortAccounts('platform')"
|
||||||
>
|
>
|
||||||
平台/类型
|
平台/类型
|
||||||
@@ -207,7 +214,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[12%] min-w-[110px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[110px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortAccounts('expiresAt')"
|
@click="sortAccounts('expiresAt')"
|
||||||
>
|
>
|
||||||
到期时间
|
到期时间
|
||||||
@@ -222,7 +229,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[12%] min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="w-[100px] min-w-[120px] max-w-[150px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortAccounts('status')"
|
@click="sortAccounts('status')"
|
||||||
>
|
>
|
||||||
状态
|
状态
|
||||||
@@ -237,7 +244,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortAccounts('priority')"
|
@click="sortAccounts('priority')"
|
||||||
>
|
>
|
||||||
优先级
|
优先级
|
||||||
@@ -252,17 +259,17 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[150px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
代理
|
代理
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[150px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
今日使用
|
今日使用
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[210px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span>会话窗口</span>
|
<span>会话窗口</span>
|
||||||
@@ -382,12 +389,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
最后使用
|
最后使用
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[15%] min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="operations-column sticky right-0 z-20 min-w-[200px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
操作
|
操作
|
||||||
</th>
|
</th>
|
||||||
@@ -395,7 +402,10 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
||||||
<tr v-for="account in paginatedAccounts" :key="account.id" class="table-row">
|
<tr v-for="account in paginatedAccounts" :key="account.id" class="table-row">
|
||||||
<td v-if="shouldShowCheckboxes" class="px-3 py-3">
|
<td
|
||||||
|
v-if="shouldShowCheckboxes"
|
||||||
|
class="checkbox-column sticky left-0 z-10 px-3 py-3"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="selectedAccounts"
|
v-model="selectedAccounts"
|
||||||
@@ -406,7 +416,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-4">
|
<td
|
||||||
|
class="name-column sticky z-10 px-3 py-4"
|
||||||
|
:class="shouldShowCheckboxes ? 'left-[50px]' : 'left-0'"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<div
|
||||||
class="mr-2 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-green-500 to-green-600"
|
class="mr-2 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-green-500 to-green-600"
|
||||||
@@ -502,7 +515,9 @@
|
|||||||
<div class="fa-openai" />
|
<div class="fa-openai" />
|
||||||
<span class="text-xs font-semibold text-gray-950">OpenAi</span>
|
<span class="text-xs font-semibold text-gray-950">OpenAi</span>
|
||||||
<span class="mx-1 h-4 w-px bg-gray-400" />
|
<span class="mx-1 h-4 w-px bg-gray-400" />
|
||||||
<span class="text-xs font-medium text-gray-950">{{ getOpenAIAuthType() }}</span>
|
<span class="text-xs font-medium text-gray-950">{{
|
||||||
|
getOpenAIAuthType()
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="account.platform === 'azure_openai'"
|
v-else-if="account.platform === 'azure_openai'"
|
||||||
@@ -523,7 +538,7 @@
|
|||||||
>
|
>
|
||||||
<i class="fas fa-server text-xs text-teal-700 dark:text-teal-400" />
|
<i class="fas fa-server text-xs text-teal-700 dark:text-teal-400" />
|
||||||
<span class="text-xs font-semibold text-teal-800 dark:text-teal-300"
|
<span class="text-xs font-semibold text-teal-800 dark:text-teal-300"
|
||||||
>OpenAI-Responses</span
|
>OpenAI-Api</span
|
||||||
>
|
>
|
||||||
<span class="mx-1 h-4 w-px bg-teal-300 dark:bg-teal-600" />
|
<span class="mx-1 h-4 w-px bg-teal-300 dark:bg-teal-600" />
|
||||||
<span class="text-xs font-medium text-teal-700 dark:text-teal-400"
|
<span class="text-xs font-medium text-teal-700 dark:text-teal-400"
|
||||||
@@ -531,7 +546,9 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="account.platform === 'claude' || account.platform === 'claude-oauth'"
|
v-else-if="
|
||||||
|
account.platform === 'claude' || account.platform === 'claude-oauth'
|
||||||
|
"
|
||||||
class="flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-gradient-to-r from-indigo-100 to-blue-100 px-2.5 py-1"
|
class="flex items-center gap-1.5 rounded-lg border border-indigo-200 bg-gradient-to-r from-indigo-100 to-blue-100 px-2.5 py-1"
|
||||||
>
|
>
|
||||||
<i class="fas fa-brain text-xs text-indigo-700" />
|
<i class="fas fa-brain text-xs text-indigo-700" />
|
||||||
@@ -548,9 +565,13 @@
|
|||||||
class="flex items-center gap-1.5 rounded-lg border border-teal-200 bg-gradient-to-r from-teal-100 to-emerald-100 px-2.5 py-1 dark:border-teal-700 dark:from-teal-900/20 dark:to-emerald-900/20"
|
class="flex items-center gap-1.5 rounded-lg border border-teal-200 bg-gradient-to-r from-teal-100 to-emerald-100 px-2.5 py-1 dark:border-teal-700 dark:from-teal-900/20 dark:to-emerald-900/20"
|
||||||
>
|
>
|
||||||
<i class="fas fa-code-branch text-xs text-teal-700 dark:text-teal-400" />
|
<i class="fas fa-code-branch text-xs text-teal-700 dark:text-teal-400" />
|
||||||
<span class="text-xs font-semibold text-teal-800 dark:text-teal-300">CCR</span>
|
<span class="text-xs font-semibold text-teal-800 dark:text-teal-300"
|
||||||
|
>CCR</span
|
||||||
|
>
|
||||||
<span class="mx-1 h-4 w-px bg-teal-300 dark:bg-teal-600" />
|
<span class="mx-1 h-4 w-px bg-teal-300 dark:bg-teal-600" />
|
||||||
<span class="text-xs font-medium text-teal-700 dark:text-teal-300">Relay</span>
|
<span class="text-xs font-medium text-teal-700 dark:text-teal-300"
|
||||||
|
>Relay</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="account.platform === 'droid'"
|
v-else-if="account.platform === 'droid'"
|
||||||
@@ -637,7 +658,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-3 py-4">
|
<td class="w-[100px] min-w-[100px] max-w-[100px] whitespace-nowrap px-3 py-4">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<span
|
<span
|
||||||
:class="[
|
:class="[
|
||||||
@@ -1132,23 +1153,13 @@
|
|||||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-600 dark:text-gray-300">
|
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-600 dark:text-gray-300">
|
||||||
{{ formatLastUsed(account.lastUsedAt) }}
|
{{ formatLastUsed(account.lastUsedAt) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-3 py-4 text-sm font-medium">
|
<td
|
||||||
<div class="flex flex-wrap items-center gap-1">
|
class="operations-column sticky right-0 z-10 whitespace-nowrap px-3 py-4 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<!-- 大屏显示所有按钮 -->
|
||||||
|
<div class="hidden items-center gap-1 2xl:flex">
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="showResetButton(account)"
|
||||||
(account.platform === 'claude' ||
|
|
||||||
account.platform === 'claude-console' ||
|
|
||||||
account.platform === 'openai' ||
|
|
||||||
account.platform === 'openai-responses' ||
|
|
||||||
account.platform === 'gemini' ||
|
|
||||||
account.platform === 'gemini-api' ||
|
|
||||||
account.platform === 'ccr') &&
|
|
||||||
(account.status === 'unauthorized' ||
|
|
||||||
account.status !== 'active' ||
|
|
||||||
account.rateLimitStatus?.isRateLimited ||
|
|
||||||
account.rateLimitStatus === 'limited' ||
|
|
||||||
!account.isActive)
|
|
||||||
"
|
|
||||||
:class="[
|
:class="[
|
||||||
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
|
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
|
||||||
account.isResetting
|
account.isResetting
|
||||||
@@ -1181,7 +1192,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="canViewUsage(account)"
|
v-if="canViewUsage(account)"
|
||||||
class="rounded bg-indigo-100 px-2.5 py-1 text-xs font-medium text-indigo-700 transition-colors hover:bg-indigo-200"
|
class="rounded bg-indigo-100 px-2.5 py-1 text-xs font-medium text-indigo-700 transition-colors hover:bg-indigo-200"
|
||||||
:title="'查看使用详情'"
|
title="查看使用详情"
|
||||||
@click="openAccountUsageModal(account)"
|
@click="openAccountUsageModal(account)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chart-line" />
|
<i class="fas fa-chart-line" />
|
||||||
@@ -1190,7 +1201,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="canTestAccount(account)"
|
v-if="canTestAccount(account)"
|
||||||
class="rounded bg-cyan-100 px-2.5 py-1 text-xs font-medium text-cyan-700 transition-colors hover:bg-cyan-200 dark:bg-cyan-900/40 dark:text-cyan-300 dark:hover:bg-cyan-800/50"
|
class="rounded bg-cyan-100 px-2.5 py-1 text-xs font-medium text-cyan-700 transition-colors hover:bg-cyan-200 dark:bg-cyan-900/40 dark:text-cyan-300 dark:hover:bg-cyan-800/50"
|
||||||
:title="'测试账户连通性'"
|
title="测试账户连通性"
|
||||||
@click="openAccountTestModal(account)"
|
@click="openAccountTestModal(account)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-vial" />
|
<i class="fas fa-vial" />
|
||||||
@@ -1198,7 +1209,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200"
|
class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200"
|
||||||
:title="'编辑账户'"
|
title="编辑账户"
|
||||||
@click="editAccount(account)"
|
@click="editAccount(account)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit" />
|
<i class="fas fa-edit" />
|
||||||
@@ -1206,18 +1217,47 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-700 transition-colors hover:bg-red-200"
|
class="rounded bg-red-100 px-2.5 py-1 text-xs font-medium text-red-700 transition-colors hover:bg-red-200"
|
||||||
:title="'删除账户'"
|
title="删除账户"
|
||||||
@click="deleteAccount(account)"
|
@click="deleteAccount(account)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash" />
|
<i class="fas fa-trash" />
|
||||||
<span class="ml-1">删除</span>
|
<span class="ml-1">删除</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 小屏显示:2个快捷按钮 + 下拉菜单 -->
|
||||||
|
<div class="flex items-center gap-1 2xl:hidden">
|
||||||
|
<button
|
||||||
|
:class="[
|
||||||
|
'rounded px-2.5 py-1 text-xs font-medium transition-colors',
|
||||||
|
account.isTogglingSchedulable
|
||||||
|
? 'cursor-not-allowed bg-gray-100 text-gray-400'
|
||||||
|
: account.schedulable
|
||||||
|
? 'bg-green-100 text-green-700 hover:bg-green-200'
|
||||||
|
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||||
|
]"
|
||||||
|
:disabled="account.isTogglingSchedulable"
|
||||||
|
:title="account.schedulable ? '点击禁用调度' : '点击启用调度'"
|
||||||
|
@click="toggleSchedulable(account)"
|
||||||
|
>
|
||||||
|
<i :class="['fas', account.schedulable ? 'fa-toggle-on' : 'fa-toggle-off']" />
|
||||||
|
<span class="ml-1">{{ account.schedulable ? '调度' : '停用' }}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="rounded bg-blue-100 px-2.5 py-1 text-xs font-medium text-blue-700 transition-colors hover:bg-blue-200"
|
||||||
|
title="编辑账户"
|
||||||
|
@click="editAccount(account)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-edit" />
|
||||||
|
<span class="ml-1">编辑</span>
|
||||||
|
</button>
|
||||||
|
<ActionDropdown :actions="getAccountActions(account)" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 移动端卡片视图 -->
|
<!-- 移动端卡片视图 -->
|
||||||
<div v-if="!accountsLoading && sortedAccounts.length > 0" class="space-y-3 md:hidden">
|
<div v-if="!accountsLoading && sortedAccounts.length > 0" class="space-y-3 md:hidden">
|
||||||
@@ -1824,6 +1864,7 @@ import AccountExpiryEditModal from '@/components/accounts/AccountExpiryEditModal
|
|||||||
import AccountTestModal from '@/components/accounts/AccountTestModal.vue'
|
import AccountTestModal from '@/components/accounts/AccountTestModal.vue'
|
||||||
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
import ConfirmModal from '@/components/common/ConfirmModal.vue'
|
||||||
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
||||||
|
import ActionDropdown from '@/components/common/ActionDropdown.vue'
|
||||||
|
|
||||||
// 使用确认弹窗
|
// 使用确认弹窗
|
||||||
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
const { showConfirmModal, confirmOptions, showConfirm, handleConfirm, handleCancel } = useConfirm()
|
||||||
@@ -2010,6 +2051,76 @@ const accountMatchesKeyword = (account, normalizedKeyword) => {
|
|||||||
|
|
||||||
const canViewUsage = (account) => !!account && supportedUsagePlatforms.includes(account.platform)
|
const canViewUsage = (account) => !!account && supportedUsagePlatforms.includes(account.platform)
|
||||||
|
|
||||||
|
// 判断是否显示重置状态按钮
|
||||||
|
const showResetButton = (account) => {
|
||||||
|
const supportedPlatforms = [
|
||||||
|
'claude',
|
||||||
|
'claude-console',
|
||||||
|
'openai',
|
||||||
|
'openai-responses',
|
||||||
|
'gemini',
|
||||||
|
'gemini-api',
|
||||||
|
'ccr'
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
supportedPlatforms.includes(account.platform) &&
|
||||||
|
(account.status === 'unauthorized' ||
|
||||||
|
account.status !== 'active' ||
|
||||||
|
account.rateLimitStatus?.isRateLimited ||
|
||||||
|
account.rateLimitStatus === 'limited' ||
|
||||||
|
!account.isActive)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取账户操作菜单项(用于小屏下拉菜单)
|
||||||
|
const getAccountActions = (account) => {
|
||||||
|
const actions = []
|
||||||
|
|
||||||
|
// 重置状态(仅在需要时显示)
|
||||||
|
if (showResetButton(account)) {
|
||||||
|
actions.push({
|
||||||
|
key: 'reset',
|
||||||
|
label: '重置状态',
|
||||||
|
icon: 'fa-redo',
|
||||||
|
color: 'orange',
|
||||||
|
handler: () => resetAccountStatus(account)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
if (canViewUsage(account)) {
|
||||||
|
actions.push({
|
||||||
|
key: 'usage',
|
||||||
|
label: '详情',
|
||||||
|
icon: 'fa-chart-line',
|
||||||
|
color: 'indigo',
|
||||||
|
handler: () => openAccountUsageModal(account)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试账户
|
||||||
|
if (canTestAccount(account)) {
|
||||||
|
actions.push({
|
||||||
|
key: 'test',
|
||||||
|
label: '测试',
|
||||||
|
icon: 'fa-vial',
|
||||||
|
color: 'blue',
|
||||||
|
handler: () => openAccountTestModal(account)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
actions.push({
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除',
|
||||||
|
icon: 'fa-trash',
|
||||||
|
color: 'red',
|
||||||
|
handler: () => deleteAccount(account)
|
||||||
|
})
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
const openAccountUsageModal = async (account) => {
|
const openAccountUsageModal = async (account) => {
|
||||||
if (!canViewUsage(account)) {
|
if (!canViewUsage(account)) {
|
||||||
showToast('该账户类型暂不支持查看详情', 'warning')
|
showToast('该账户类型暂不支持查看详情', 'warning')
|
||||||
@@ -4018,19 +4129,11 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.table-container {
|
.accounts-container {
|
||||||
border-radius: 12px;
|
min-height: calc(100vh - 300px);
|
||||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-row:hover {
|
|
||||||
background-color: rgba(0, 0, 0, 0.02);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 加载动画 */
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
@@ -4048,10 +4151,68 @@ onMounted(() => {
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.accounts-container {
|
|
||||||
min-height: calc(100vh - 300px);
|
/* 表格外层包装器 - 圆角和边框 */
|
||||||
|
.table-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .table-wrapper {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格内层容器 - 横向滚动 */
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 防止表格内容溢出,保证横向滚动 */
|
||||||
|
.table-container table {
|
||||||
|
min-width: 1400px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 滚动条样式 */
|
||||||
|
.table-container::-webkit-scrollbar {
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container::-webkit-scrollbar-track {
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #d1d5db;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container::-webkit-scrollbar-track {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格行样式 */
|
||||||
.table-row {
|
.table-row {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -4059,4 +4220,106 @@ onMounted(() => {
|
|||||||
.table-row:hover {
|
.table-row:hover {
|
||||||
background-color: rgba(0, 0, 0, 0.02);
|
background-color: rgba(0, 0, 0, 0.02);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .table-row:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表头左侧固定列背景 */
|
||||||
|
.table-container thead .checkbox-column,
|
||||||
|
.table-container thead .name-column {
|
||||||
|
z-index: 30;
|
||||||
|
background: linear-gradient(to bottom, #f9fafb, rgba(243, 244, 246, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container thead .checkbox-column,
|
||||||
|
.dark .table-container thead .name-column {
|
||||||
|
background: linear-gradient(to bottom, #374151, rgba(31, 41, 55, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表头右侧操作列背景 */
|
||||||
|
.table-container thead .operations-column {
|
||||||
|
z-index: 30;
|
||||||
|
background: linear-gradient(to bottom, #f9fafb, rgba(243, 244, 246, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container thead .operations-column {
|
||||||
|
background: linear-gradient(to bottom, #374151, rgba(31, 41, 55, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tbody 中的左侧固定列背景处理 */
|
||||||
|
.table-container tbody tr:nth-child(odd) .checkbox-column,
|
||||||
|
.table-container tbody tr:nth-child(odd) .name-column {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tbody tr:nth-child(even) .checkbox-column,
|
||||||
|
.table-container tbody tr:nth-child(even) .name-column {
|
||||||
|
background-color: rgba(249, 250, 251, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(odd) .checkbox-column,
|
||||||
|
.dark .table-container tbody tr:nth-child(odd) .name-column {
|
||||||
|
background-color: rgba(31, 41, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(even) .checkbox-column,
|
||||||
|
.dark .table-container tbody tr:nth-child(even) .name-column {
|
||||||
|
background-color: rgba(55, 65, 81, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 状态下的左侧固定列背景 */
|
||||||
|
.table-container tbody tr:hover .checkbox-column,
|
||||||
|
.table-container tbody tr:hover .name-column {
|
||||||
|
background-color: rgba(239, 246, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:hover .checkbox-column,
|
||||||
|
.dark .table-container tbody tr:hover .name-column {
|
||||||
|
background-color: rgba(30, 58, 138, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 名称列右侧阴影(分隔效果) */
|
||||||
|
.table-container tbody .name-column {
|
||||||
|
box-shadow: 8px 0 12px -8px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody .name-column {
|
||||||
|
box-shadow: 8px 0 12px -8px rgba(30, 41, 59, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tbody 中的操作列背景处理 */
|
||||||
|
.table-container tbody tr:nth-child(odd) .operations-column {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tbody tr:nth-child(even) .operations-column {
|
||||||
|
background-color: rgba(249, 250, 251, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(odd) .operations-column {
|
||||||
|
background-color: rgba(31, 41, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(even) .operations-column {
|
||||||
|
background-color: rgba(55, 65, 81, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 状态下的操作列背景 */
|
||||||
|
.table-container tbody tr:hover .operations-column {
|
||||||
|
background-color: rgba(239, 246, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:hover .operations-column {
|
||||||
|
background-color: rgba(30, 58, 138, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作列左侧阴影 */
|
||||||
|
.table-container tbody .operations-column {
|
||||||
|
box-shadow: -8px 0 12px -8px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody .operations-column {
|
||||||
|
box-shadow: -8px 0 12px -8px rgba(30, 41, 59, 0.45);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -253,12 +253,15 @@
|
|||||||
<!-- 桌面端表格视图 -->
|
<!-- 桌面端表格视图 -->
|
||||||
<div v-else class="table-wrapper hidden md:block">
|
<div v-else class="table-wrapper hidden md:block">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="w-full table-fixed">
|
<table class="w-full">
|
||||||
<thead
|
<thead
|
||||||
class="sticky top-0 z-10 bg-gradient-to-b from-gray-50 to-gray-100/90 backdrop-blur-sm dark:from-gray-700 dark:to-gray-800/90"
|
class="sticky top-0 z-10 bg-gradient-to-b from-gray-50 to-gray-100/90 backdrop-blur-sm dark:from-gray-700 dark:to-gray-800/90"
|
||||||
>
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="shouldShowCheckboxes" class="w-[50px] px-3 py-4 text-left">
|
<th
|
||||||
|
v-if="shouldShowCheckboxes"
|
||||||
|
class="checkbox-column sticky left-0 z-20 min-w-[50px] px-3 py-4 text-left"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="selectAllChecked"
|
v-model="selectAllChecked"
|
||||||
@@ -270,7 +273,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[14%] min-w-[120px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="name-column sticky z-20 min-w-[140px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
|
:class="shouldShowCheckboxes ? 'left-[50px]' : 'left-0'"
|
||||||
@click="sortApiKeys('name')"
|
@click="sortApiKeys('name')"
|
||||||
>
|
>
|
||||||
名称
|
名称
|
||||||
@@ -285,17 +289,17 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
所属账号
|
所属账号
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
标签
|
标签
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[6%] min-w-[60px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[80px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('status')"
|
@click="sortApiKeys('status')"
|
||||||
>
|
>
|
||||||
状态
|
状态
|
||||||
@@ -310,7 +314,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[4%] min-w-[40px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[70px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600': canSortByCost,
|
'cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600': canSortByCost,
|
||||||
'cursor-not-allowed opacity-60': !canSortByCost
|
'cursor-not-allowed opacity-60': !canSortByCost
|
||||||
@@ -331,22 +335,22 @@
|
|||||||
<i v-else class="fas fa-clock ml-1 text-gray-400" title="索引更新中" />
|
<i v-else class="fas fa-clock ml-1 text-gray-400" title="索引更新中" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[14%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[180px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
限制
|
限制
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[5%] min-w-[45px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[80px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Token
|
Token
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[5%] min-w-[45px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[80px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
请求数
|
请求数
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[70px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('lastUsedAt')"
|
@click="sortApiKeys('lastUsedAt')"
|
||||||
>
|
>
|
||||||
最后使用
|
最后使用
|
||||||
@@ -361,7 +365,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[70px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('createdAt')"
|
@click="sortApiKeys('createdAt')"
|
||||||
>
|
>
|
||||||
创建时间
|
创建时间
|
||||||
@@ -376,7 +380,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[70px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
class="min-w-[100px] cursor-pointer px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600"
|
||||||
@click="sortApiKeys('expiresAt')"
|
@click="sortApiKeys('expiresAt')"
|
||||||
>
|
>
|
||||||
过期时间
|
过期时间
|
||||||
@@ -391,7 +395,7 @@
|
|||||||
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
<i v-else class="fas fa-sort ml-1 text-gray-400" />
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="operations-column sticky right-0 w-[23%] min-w-[200px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="operations-column sticky right-0 min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
操作
|
操作
|
||||||
</th>
|
</th>
|
||||||
@@ -410,7 +414,10 @@
|
|||||||
'hover:bg-blue-50/60 hover:shadow-sm dark:hover:bg-blue-900/20'
|
'hover:bg-blue-50/60 hover:shadow-sm dark:hover:bg-blue-900/20'
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<td v-if="shouldShowCheckboxes" class="px-3 py-3">
|
<td
|
||||||
|
v-if="shouldShowCheckboxes"
|
||||||
|
class="checkbox-column sticky left-0 z-10 px-3 py-3"
|
||||||
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
v-model="selectedApiKeys"
|
v-model="selectedApiKeys"
|
||||||
@@ -421,7 +428,10 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-3">
|
<td
|
||||||
|
class="name-column sticky z-10 px-3 py-3"
|
||||||
|
:class="shouldShowCheckboxes ? 'left-[50px]' : 'left-0'"
|
||||||
|
>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<!-- 名称 -->
|
<!-- 名称 -->
|
||||||
<div
|
<div
|
||||||
@@ -854,14 +864,15 @@
|
|||||||
class="operations-column operations-cell whitespace-nowrap px-3 py-3"
|
class="operations-column operations-cell whitespace-nowrap px-3 py-3"
|
||||||
style="font-size: 13px"
|
style="font-size: 13px"
|
||||||
>
|
>
|
||||||
<div class="flex gap-1">
|
<!-- 大屏幕:展开所有按钮 -->
|
||||||
|
<div class="hidden gap-1 2xl:flex">
|
||||||
<button
|
<button
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:hover:bg-purple-900/20"
|
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:hover:bg-purple-900/20"
|
||||||
title="查看详细统计"
|
title="查看详细统计"
|
||||||
@click="showUsageDetails(key)"
|
@click="showUsageDetails(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-chart-line" />
|
<i class="fas fa-chart-line" />
|
||||||
<span class="ml-1 hidden xl:inline">详情</span>
|
<span class="ml-1">详情</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="key && key.id"
|
v-if="key && key.id"
|
||||||
@@ -875,7 +886,7 @@
|
|||||||
expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down'
|
expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down'
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<span class="ml-1 hidden xl:inline">模型</span>
|
<span class="ml-1">模型</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-900 dark:hover:bg-blue-900/20"
|
class="rounded px-2 py-1 text-xs font-medium text-blue-600 transition-colors hover:bg-blue-50 hover:text-blue-900 dark:hover:bg-blue-900/20"
|
||||||
@@ -883,7 +894,7 @@
|
|||||||
@click="openEditApiKeyModal(key)"
|
@click="openEditApiKeyModal(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit" />
|
<i class="fas fa-edit" />
|
||||||
<span class="ml-1 hidden xl:inline">编辑</span>
|
<span class="ml-1">编辑</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="
|
v-if="
|
||||||
@@ -896,7 +907,7 @@
|
|||||||
@click="openRenewApiKeyModal(key)"
|
@click="openRenewApiKeyModal(key)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-clock" />
|
<i class="fas fa-clock" />
|
||||||
<span class="ml-1 hidden xl:inline">续期</span>
|
<span class="ml-1">续期</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
@@ -909,9 +920,7 @@
|
|||||||
@click="toggleApiKeyStatus(key)"
|
@click="toggleApiKeyStatus(key)"
|
||||||
>
|
>
|
||||||
<i :class="['fas', key.isActive ? 'fa-ban' : 'fa-check-circle']" />
|
<i :class="['fas', key.isActive ? 'fa-ban' : 'fa-check-circle']" />
|
||||||
<span class="ml-1 hidden xl:inline">{{
|
<span class="ml-1">{{ key.isActive ? '禁用' : '激活' }}</span>
|
||||||
key.isActive ? '禁用' : '激活'
|
|
||||||
}}</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900 dark:hover:bg-red-900/20"
|
class="rounded px-2 py-1 text-xs font-medium text-red-600 transition-colors hover:bg-red-50 hover:text-red-900 dark:hover:bg-red-900/20"
|
||||||
@@ -919,9 +928,35 @@
|
|||||||
@click="deleteApiKey(key.id)"
|
@click="deleteApiKey(key.id)"
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash" />
|
<i class="fas fa-trash" />
|
||||||
<span class="ml-1 hidden xl:inline">删除</span>
|
<span class="ml-1">删除</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- 小屏幕:常用按钮 + 下拉菜单 -->
|
||||||
|
<div class="flex items-center gap-1 2xl:hidden">
|
||||||
|
<!-- 始终显示的快捷按钮 -->
|
||||||
|
<button
|
||||||
|
class="rounded px-2 py-1 text-xs font-medium text-purple-600 transition-colors hover:bg-purple-50 hover:text-purple-900 dark:hover:bg-purple-900/20"
|
||||||
|
title="查看详细统计"
|
||||||
|
@click="showUsageDetails(key)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-chart-line" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="key && key.id"
|
||||||
|
class="rounded px-2 py-1 text-xs font-medium text-indigo-600 transition-colors hover:bg-indigo-50 hover:text-indigo-900 dark:hover:bg-indigo-900/20"
|
||||||
|
title="模型使用分布"
|
||||||
|
@click="toggleApiKeyModelStats(key.id)"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="[
|
||||||
|
'fas',
|
||||||
|
expandedApiKeys[key.id] ? 'fa-chevron-up' : 'fa-chevron-down'
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<!-- 更多操作下拉菜单 -->
|
||||||
|
<ActionDropdown :actions="getApiKeyActions(key)" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@@ -1722,62 +1757,64 @@
|
|||||||
|
|
||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="w-full table-fixed">
|
<table class="w-full">
|
||||||
<thead class="bg-gray-50/80 backdrop-blur-sm dark:bg-gray-700/80">
|
<thead
|
||||||
|
class="sticky top-0 z-10 bg-gradient-to-b from-gray-50 to-gray-100/90 backdrop-blur-sm dark:from-gray-700 dark:to-gray-800/90"
|
||||||
|
>
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
class="w-[14%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="name-column sticky left-0 z-20 min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
名称
|
名称
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
所属账号
|
所属账号
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
v-if="isLdapEnabled"
|
v-if="isLdapEnabled"
|
||||||
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
创建者
|
创建者
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[15%] min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[120px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
创建时间
|
创建时间
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
删除者
|
删除者
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[10%] min-w-[90px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
删除时间
|
删除时间
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[60px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[70px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
费用
|
费用
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[60px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[80px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
Token
|
Token
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[8%] min-w-[60px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[80px] px-3 py-4 text-right text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
请求数
|
请求数
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="w-[9%] min-w-[80px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="min-w-[100px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
最后使用
|
最后使用
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
class="operations-column sticky right-0 w-[15%] min-w-[160px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
class="operations-column sticky right-0 min-w-[140px] px-3 py-4 text-left text-xs font-bold uppercase tracking-wider text-gray-700 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
操作
|
操作
|
||||||
</th>
|
</th>
|
||||||
@@ -1785,7 +1822,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
<tbody class="divide-y divide-gray-200/50 dark:divide-gray-600/50">
|
||||||
<tr v-for="key in deletedApiKeys" :key="key.id" class="table-row">
|
<tr v-for="key in deletedApiKeys" :key="key.id" class="table-row">
|
||||||
<td class="px-3 py-3">
|
<td class="name-column sticky left-0 z-10 px-3 py-3">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<div
|
||||||
class="mr-2 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-500 to-red-600"
|
class="mr-2 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-red-500 to-red-600"
|
||||||
@@ -2074,6 +2111,7 @@ import ExpiryEditModal from '@/components/apikeys/ExpiryEditModal.vue'
|
|||||||
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
import UsageDetailModal from '@/components/apikeys/UsageDetailModal.vue'
|
||||||
import LimitProgressBar from '@/components/apikeys/LimitProgressBar.vue'
|
import LimitProgressBar from '@/components/apikeys/LimitProgressBar.vue'
|
||||||
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
import CustomDropdown from '@/components/common/CustomDropdown.vue'
|
||||||
|
import ActionDropdown from '@/components/common/ActionDropdown.vue'
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const clientsStore = useClientsStore()
|
const clientsStore = useClientsStore()
|
||||||
@@ -3705,6 +3743,50 @@ const handleRenewSuccess = () => {
|
|||||||
loadApiKeys()
|
loadApiKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取API Key的操作菜单项(用于ActionDropdown)
|
||||||
|
const getApiKeyActions = (key) => {
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
key: 'edit',
|
||||||
|
label: '编辑',
|
||||||
|
icon: 'fa-edit',
|
||||||
|
color: 'blue',
|
||||||
|
handler: () => openEditApiKeyModal(key)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 如果需要续期
|
||||||
|
if (key.expiresAt && (isApiKeyExpired(key.expiresAt) || isApiKeyExpiringSoon(key.expiresAt))) {
|
||||||
|
actions.push({
|
||||||
|
key: 'renew',
|
||||||
|
label: '续期',
|
||||||
|
icon: 'fa-clock',
|
||||||
|
color: 'green',
|
||||||
|
handler: () => openRenewApiKeyModal(key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激活/禁用
|
||||||
|
actions.push({
|
||||||
|
key: 'toggle',
|
||||||
|
label: key.isActive ? '禁用' : '激活',
|
||||||
|
icon: key.isActive ? 'fa-ban' : 'fa-check-circle',
|
||||||
|
color: key.isActive ? 'orange' : 'green',
|
||||||
|
handler: () => toggleApiKeyStatus(key)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
actions.push({
|
||||||
|
key: 'delete',
|
||||||
|
label: '删除',
|
||||||
|
icon: 'fa-trash',
|
||||||
|
color: 'red',
|
||||||
|
handler: () => deleteApiKey(key.id)
|
||||||
|
})
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
|
||||||
// 切换API Key状态(激活/禁用)
|
// 切换API Key状态(激活/禁用)
|
||||||
const toggleApiKeyStatus = async (key) => {
|
const toggleApiKeyStatus = async (key) => {
|
||||||
let confirmed = true
|
let confirmed = true
|
||||||
@@ -4689,6 +4771,10 @@ onUnmounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .table-wrapper {
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
@@ -4696,12 +4782,14 @@ onUnmounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 防止表格内容溢出,保证横向滚动 */
|
/* 防止表格内容溢出,保证横向滚动 */
|
||||||
.table-container table {
|
.table-container table {
|
||||||
min-width: 1200px;
|
min-width: 1400px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container::-webkit-scrollbar {
|
.table-container::-webkit-scrollbar {
|
||||||
@@ -4722,6 +4810,18 @@ onUnmounted(() => {
|
|||||||
background: #9ca3af;
|
background: #9ca3af;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark .table-container::-webkit-scrollbar-track {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container::-webkit-scrollbar-thumb {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
.table-row {
|
.table-row {
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -4738,13 +4838,43 @@ onUnmounted(() => {
|
|||||||
.operations-column {
|
.operations-column {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: inherit;
|
|
||||||
background-color: inherit;
|
|
||||||
z-index: 12;
|
z-index: 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 确保操作列在浅色模式下有正确的背景 */
|
||||||
.table-container thead .operations-column {
|
.table-container thead .operations-column {
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
|
background: linear-gradient(to bottom, #f9fafb, rgba(243, 244, 246, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container thead .operations-column {
|
||||||
|
background: linear-gradient(to bottom, #374151, rgba(31, 41, 55, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tbody 中的操作列背景处理 */
|
||||||
|
.table-container tbody tr:nth-child(odd) .operations-column {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tbody tr:nth-child(even) .operations-column {
|
||||||
|
background-color: rgba(249, 250, 251, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(odd) .operations-column {
|
||||||
|
background-color: rgba(31, 41, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(even) .operations-column {
|
||||||
|
background-color: rgba(55, 65, 81, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 状态下的操作列背景 */
|
||||||
|
.table-container tbody tr:hover .operations-column {
|
||||||
|
background-color: rgba(239, 246, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:hover .operations-column {
|
||||||
|
background-color: rgba(30, 58, 138, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container tbody .operations-column {
|
.table-container tbody .operations-column {
|
||||||
@@ -4755,6 +4885,66 @@ onUnmounted(() => {
|
|||||||
box-shadow: -8px 0 12px -8px rgba(30, 41, 59, 0.45);
|
box-shadow: -8px 0 12px -8px rgba(30, 41, 59, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 固定左侧列(复选框和名称列)*/
|
||||||
|
.checkbox-column,
|
||||||
|
.name-column {
|
||||||
|
position: sticky;
|
||||||
|
z-index: 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表头左侧固定列背景 */
|
||||||
|
.table-container thead .checkbox-column,
|
||||||
|
.table-container thead .name-column {
|
||||||
|
z-index: 30;
|
||||||
|
background: linear-gradient(to bottom, #f9fafb, rgba(243, 244, 246, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container thead .checkbox-column,
|
||||||
|
.dark .table-container thead .name-column {
|
||||||
|
background: linear-gradient(to bottom, #374151, rgba(31, 41, 55, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tbody 中的左侧固定列背景处理 */
|
||||||
|
.table-container tbody tr:nth-child(odd) .checkbox-column,
|
||||||
|
.table-container tbody tr:nth-child(odd) .name-column {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container tbody tr:nth-child(even) .checkbox-column,
|
||||||
|
.table-container tbody tr:nth-child(even) .name-column {
|
||||||
|
background-color: rgba(249, 250, 251, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(odd) .checkbox-column,
|
||||||
|
.dark .table-container tbody tr:nth-child(odd) .name-column {
|
||||||
|
background-color: rgba(31, 41, 55, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:nth-child(even) .checkbox-column,
|
||||||
|
.dark .table-container tbody tr:nth-child(even) .name-column {
|
||||||
|
background-color: rgba(55, 65, 81, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hover 状态下的左侧固定列背景 */
|
||||||
|
.table-container tbody tr:hover .checkbox-column,
|
||||||
|
.table-container tbody tr:hover .name-column {
|
||||||
|
background-color: rgba(239, 246, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody tr:hover .checkbox-column,
|
||||||
|
.dark .table-container tbody tr:hover .name-column {
|
||||||
|
background-color: rgba(30, 58, 138, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 名称列右侧阴影(分隔效果) */
|
||||||
|
.table-container tbody .name-column {
|
||||||
|
box-shadow: 8px 0 12px -8px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .table-container tbody .name-column {
|
||||||
|
box-shadow: 8px 0 12px -8px rgba(30, 41, 59, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
|
|||||||
Reference in New Issue
Block a user