fix: api-keys页面布局优化

This commit is contained in:
shaw
2025-09-08 20:45:19 +08:00
parent 7f8fae70e6
commit c4f1e7a411
4 changed files with 705 additions and 129 deletions

View File

@@ -0,0 +1,94 @@
<template>
<div class="inline-flex items-center gap-1.5 rounded-md px-2 py-1" :class="badgeClass">
<div class="flex items-center gap-1">
<i :class="['text-xs', iconClass]" />
<span class="text-xs font-medium">{{ label }}</span>
</div>
<div class="flex items-center gap-1">
<span class="text-xs font-semibold">${{ current.toFixed(2) }}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">/</span>
<span class="text-xs">${{ limit.toFixed(2) }}</span>
</div>
<!-- 小型进度条 -->
<div class="h-1 w-12 rounded-full bg-gray-200 dark:bg-gray-600">
<div
class="h-1 rounded-full transition-all duration-300"
:class="progressClass"
:style="{ width: progress + '%' }"
/>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
type: {
type: String,
required: true,
validator: (value) => ['daily', 'opus', 'window'].includes(value)
},
label: {
type: String,
required: true
},
current: {
type: Number,
default: 0
},
limit: {
type: Number,
required: true
}
})
const progress = computed(() => {
if (!props.limit || props.limit === 0) return 0
const percentage = (props.current / props.limit) * 100
return Math.min(percentage, 100)
})
const badgeClass = computed(() => {
switch (props.type) {
case 'daily':
return 'bg-gray-50 dark:bg-gray-700/50'
case 'opus':
return 'bg-indigo-50 dark:bg-indigo-900/20'
case 'window':
return 'bg-blue-50 dark:bg-blue-900/20'
default:
return 'bg-gray-50 dark:bg-gray-700/50'
}
})
const iconClass = computed(() => {
switch (props.type) {
case 'daily':
return 'fas fa-calendar-day text-gray-500'
case 'opus':
return 'fas fa-gem text-indigo-500'
case 'window':
return 'fas fa-clock text-blue-500'
default:
return 'fas fa-info-circle text-gray-500'
}
})
const progressClass = computed(() => {
const p = progress.value
if (p >= 100) return 'bg-red-500'
if (p >= 80) return 'bg-yellow-500'
switch (props.type) {
case 'daily':
return 'bg-green-500'
case 'opus':
return 'bg-indigo-500'
case 'window':
return 'bg-blue-500'
default:
return 'bg-gray-500'
}
})
</script>

View File

@@ -0,0 +1,213 @@
<template>
<div class="w-full">
<div class="relative h-7 w-full overflow-hidden rounded-md" :class="containerClass">
<!-- 背景层 -->
<div class="absolute inset-0" :class="backgroundClass"></div>
<!-- 进度条层 -->
<div
class="absolute inset-0 h-full transition-all duration-500 ease-out"
:class="progressBarClass"
:style="{ width: progress + '%' }"
></div>
<!-- 文字层 -->
<div class="relative z-10 flex h-full items-center justify-between px-2">
<div class="flex items-center gap-1.5">
<i :class="['text-xs', iconClass]" />
<span class="text-xs font-medium" :class="textClass">{{ label }}</span>
</div>
<div class="flex items-center gap-1">
<span class="text-xs font-bold" :class="valueTextClass"> ${{ current.toFixed(2) }} </span>
<span class="text-xs" :class="textClass">/ ${{ limit.toFixed(2) }}</span>
</div>
</div>
<!-- 闪光效果可选 -->
<div
v-if="showShine && progress > 0"
class="absolute inset-0 opacity-30"
:style="{
background:
'linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.7) 50%, transparent 60%)',
animation: 'shine 3s infinite'
}"
></div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
type: {
type: String,
required: true,
validator: (value) => ['daily', 'opus', 'window'].includes(value)
},
label: {
type: String,
required: true
},
current: {
type: Number,
default: 0
},
limit: {
type: Number,
required: true
},
showShine: {
type: Boolean,
default: false
}
})
const progress = computed(() => {
if (!props.limit || props.limit === 0) return 0
const percentage = (props.current / props.limit) * 100
return Math.min(percentage, 100)
})
// 容器样式
const containerClass = computed(() => {
return 'bg-gradient-to-r border border-opacity-20'
})
// 背景样式(浅色背景)
const backgroundClass = computed(() => {
switch (props.type) {
case 'daily':
return 'bg-gradient-to-r from-green-50 to-green-100 dark:from-green-950/30 dark:to-green-900/30'
case 'opus':
return 'bg-gradient-to-r from-purple-50 to-indigo-100 dark:from-purple-950/30 dark:to-indigo-900/30'
case 'window':
return 'bg-gradient-to-r from-blue-50 to-cyan-100 dark:from-blue-950/30 dark:to-cyan-900/30'
default:
return 'bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-800 dark:to-gray-700'
}
})
// 进度条样式(根据进度值动态变化)
const progressBarClass = computed(() => {
const p = progress.value
if (props.type === 'daily') {
if (p >= 90) {
return 'bg-gradient-to-r from-red-500 to-red-600'
} else if (p >= 70) {
return 'bg-gradient-to-r from-yellow-500 to-orange-500'
} else {
return 'bg-gradient-to-r from-green-500 to-emerald-500'
}
}
if (props.type === 'opus') {
if (p >= 90) {
return 'bg-gradient-to-r from-red-500 to-pink-600'
} else if (p >= 70) {
return 'bg-gradient-to-r from-orange-500 to-amber-500'
} else {
return 'bg-gradient-to-r from-purple-500 to-indigo-600'
}
}
if (props.type === 'window') {
if (p >= 90) {
return 'bg-gradient-to-r from-red-500 to-rose-600'
} else if (p >= 70) {
return 'bg-gradient-to-r from-orange-500 to-yellow-500'
} else {
return 'bg-gradient-to-r from-blue-500 to-cyan-500'
}
}
return 'bg-gradient-to-r from-gray-400 to-gray-500'
})
// 图标类
const iconClass = computed(() => {
const p = progress.value
let colorClass = ''
if (p >= 90) {
colorClass = 'text-red-600 dark:text-red-400'
} else if (p >= 70) {
colorClass = 'text-orange-600 dark:text-orange-400'
} else {
switch (props.type) {
case 'daily':
colorClass = 'text-green-600 dark:text-green-400'
break
case 'opus':
colorClass = 'text-purple-600 dark:text-purple-400'
break
case 'window':
colorClass = 'text-blue-600 dark:text-blue-400'
break
default:
colorClass = 'text-gray-500 dark:text-gray-400'
}
}
let iconName = ''
switch (props.type) {
case 'daily':
iconName = 'fas fa-calendar-day'
break
case 'opus':
iconName = 'fas fa-gem'
break
case 'window':
iconName = 'fas fa-clock'
break
default:
iconName = 'fas fa-infinity'
}
return `${iconName} ${colorClass}`
})
// 文字颜色(根据进度自动调整)
const textClass = computed(() => {
const p = progress.value
if (p > 50) {
// 进度超过50%时,文字使用白色(在深色进度条上)
return 'text-white drop-shadow-sm'
} else {
// 进度较低时,文字使用深色
return 'text-gray-600 dark:text-gray-300'
}
})
// 数值文字颜色(更突出)
const valueTextClass = computed(() => {
const p = progress.value
if (p > 50) {
return 'text-white drop-shadow-md'
} else {
return 'text-gray-800 dark:text-gray-200'
}
})
</script>
<style scoped>
@keyframes shine {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}
/* 添加柔和的边框 */
.border-opacity-20 {
border-color: rgba(0, 0, 0, 0.05);
}
.dark .border-opacity-20 {
border-color: rgba(255, 255, 255, 0.1);
}
</style>

View File

@@ -0,0 +1,241 @@
<template>
<div class="w-full space-y-1">
<!-- 时间窗口进度条 -->
<div
class="relative h-7 w-full overflow-hidden rounded-md border border-opacity-20 bg-gradient-to-r from-blue-50 to-cyan-100 dark:from-blue-950/30 dark:to-cyan-900/30"
>
<!-- 时间进度条背景 -->
<div
class="absolute inset-0 h-full bg-gradient-to-r from-blue-500 to-cyan-500 opacity-20 transition-all duration-1000"
:style="{ width: timeProgress + '%' }"
></div>
<!-- 文字层 -->
<div class="relative z-10 flex h-full items-center justify-between px-2">
<div class="flex items-center gap-1.5">
<i class="fas fa-clock text-xs text-blue-600 dark:text-blue-400" />
<span class="text-xs font-medium text-gray-700 dark:text-gray-200">
{{ rateLimitWindow }}分钟窗口
</span>
</div>
<span
class="text-xs font-bold"
:class="
remainingSeconds > 0
? 'text-blue-700 dark:text-blue-300'
: 'text-gray-400 dark:text-gray-500'
"
>
{{ remainingSeconds > 0 ? formatTime(remainingSeconds) : '未激活' }}
</span>
</div>
</div>
<!-- 费用和请求限制如果有的话 -->
<div v-if="costLimit > 0 || requestLimit > 0" class="flex gap-1">
<!-- 费用限制进度条 -->
<div
v-if="costLimit > 0"
class="relative h-6 overflow-hidden rounded-md border border-opacity-20 bg-gradient-to-r from-green-50 to-emerald-100 dark:from-green-950/30 dark:to-emerald-900/30"
:class="requestLimit > 0 ? 'w-1/2' : 'w-full'"
>
<!-- 进度条 -->
<div
class="absolute inset-0 h-full transition-all duration-500 ease-out"
:class="getCostProgressBarClass()"
:style="{ width: costProgress + '%' }"
></div>
<!-- 文字 -->
<div class="relative z-10 flex h-full items-center justify-between px-2">
<span class="text-[10px] font-medium" :class="getCostTextClass()">费用</span>
<span class="text-[10px] font-bold" :class="getCostValueTextClass()">
${{ currentCost.toFixed(1) }}/${{ costLimit.toFixed(0) }}
</span>
</div>
</div>
<!-- 请求限制进度条 -->
<div
v-if="requestLimit > 0"
class="relative h-6 overflow-hidden rounded-md border border-opacity-20 bg-gradient-to-r from-purple-50 to-indigo-100 dark:from-purple-950/30 dark:to-indigo-900/30"
:class="costLimit > 0 ? 'w-1/2' : 'w-full'"
>
<!-- 进度条 -->
<div
class="absolute inset-0 h-full transition-all duration-500 ease-out"
:class="getRequestProgressBarClass()"
:style="{ width: requestProgress + '%' }"
></div>
<!-- 文字 -->
<div class="relative z-10 flex h-full items-center justify-between px-2">
<span class="text-[10px] font-medium" :class="getRequestTextClass()">请求</span>
<span class="text-[10px] font-bold" :class="getRequestValueTextClass()">
{{ currentRequests }}/{{ requestLimit }}
</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
rateLimitWindow: {
type: Number,
required: true
},
remainingSeconds: {
type: Number,
default: 0
},
currentRequests: {
type: Number,
default: 0
},
requestLimit: {
type: Number,
default: 0
},
currentCost: {
type: Number,
default: 0
},
costLimit: {
type: Number,
default: 0
},
currentTokens: {
type: Number,
default: 0
},
tokenLimit: {
type: Number,
default: 0
}
})
// 费用进度
const costProgress = computed(() => {
if (!props.costLimit || props.costLimit === 0) return 0
const percentage = (props.currentCost / props.costLimit) * 100
return Math.min(percentage, 100)
})
// 请求进度
const requestProgress = computed(() => {
if (!props.requestLimit || props.requestLimit === 0) return 0
const percentage = (props.currentRequests / props.requestLimit) * 100
return Math.min(percentage, 100)
})
// 时间进度(倒计时)
const timeProgress = computed(() => {
if (!props.rateLimitWindow || props.rateLimitWindow === 0) return 0
const totalSeconds = props.rateLimitWindow * 60
const elapsed = totalSeconds - props.remainingSeconds
return Math.max(0, (elapsed / totalSeconds) * 100)
})
// 费用进度条颜色
const getCostProgressBarClass = () => {
const p = costProgress.value
if (p >= 90) {
return 'bg-gradient-to-r from-red-500 to-rose-600'
} else if (p >= 70) {
return 'bg-gradient-to-r from-orange-500 to-amber-500'
} else {
return 'bg-gradient-to-r from-green-500 to-emerald-500'
}
}
// 请求进度条颜色
const getRequestProgressBarClass = () => {
const p = requestProgress.value
if (p >= 90) {
return 'bg-gradient-to-r from-red-500 to-pink-600'
} else if (p >= 70) {
return 'bg-gradient-to-r from-orange-500 to-yellow-500'
} else {
return 'bg-gradient-to-r from-purple-500 to-indigo-600'
}
}
// 费用文字颜色
const getCostTextClass = () => {
const p = costProgress.value
if (p > 50) {
return 'text-white drop-shadow-sm'
} else {
return 'text-gray-600 dark:text-gray-300'
}
}
const getCostValueTextClass = () => {
const p = costProgress.value
if (p > 50) {
return 'text-white drop-shadow-md'
} else {
return 'text-gray-800 dark:text-gray-200'
}
}
// 请求文字颜色
const getRequestTextClass = () => {
const p = requestProgress.value
if (p > 50) {
return 'text-white drop-shadow-sm'
} else {
return 'text-gray-600 dark:text-gray-300'
}
}
const getRequestValueTextClass = () => {
const p = requestProgress.value
if (p > 50) {
return 'text-white drop-shadow-md'
} else {
return 'text-gray-800 dark:text-gray-200'
}
}
// 格式化时间
const formatTime = (seconds) => {
if (seconds === null || seconds === undefined) return '--:--'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = seconds % 60
if (hours > 0) {
return `${hours}h${minutes}m`
} else if (minutes > 0) {
return `${minutes}m${secs}s`
} else {
return `${secs}s`
}
}
// 格式化Token数 - 暂时未使用
// const formatTokens = (count) => {
// if (count >= 1000000) {
// return (count / 1000000).toFixed(1) + 'M'
// } else if (count >= 1000) {
// return (count / 1000).toFixed(1) + 'K'
// }
// return count.toString()
// }
</script>
<style scoped>
.border-opacity-20 {
border-color: rgba(0, 0, 0, 0.05);
}
.dark .border-opacity-20 {
border-color: rgba(255, 255, 255, 0.1);
}
</style>