新增功能: - 新增 Sora 账号管理和 OAuth 认证 - 新增 Sora 视频/图片生成 API 网关 - 新增 Sora 任务调度和缓存机制 - 新增 Sora 使用统计和计费支持 - 前端增加 Sora 平台配置界面 安全修复(代码审核): - [SEC-001] 限制媒体下载响应体大小(图片 20MB、视频 200MB),防止 DoS 攻击 - [SEC-002] 限制 SDK API 响应大小(1MB),防止内存耗尽 - [SEC-003] 修复 SSRF 风险,添加 URL 验证并强制使用代理配置 BUG 修复(代码审核): - [BUG-001] 修复 for 循环内 defer 累积导致的资源泄漏 - [BUG-002] 修复图片并发槽位获取失败时已持有锁未释放的永久泄漏 性能优化(代码审核): - [PERF-001] 添加 Sentinel Token 缓存(3 分钟有效期),减少 PoW 计算开销 技术细节: - 使用 io.LimitReader 限制所有外部输入的大小 - 添加 urlvalidator 验证防止 SSRF 攻击 - 使用 sync.Map 实现线程安全的包级缓存 - 优化并发槽位管理,添加 releaseAll 模式防止泄漏 影响范围: - 后端:新增 Sora 相关数据模型、服务、网关和管理接口 - 前端:新增 Sora 平台配置、账号管理和监控界面 - 配置:新增 Sora 相关配置项和环境变量 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
135 lines
4.6 KiB
Vue
135 lines
4.6 KiB
Vue
<template>
|
||
<span
|
||
:class="[
|
||
'inline-flex items-center gap-1.5 rounded-md px-2 py-0.5 text-xs font-medium transition-colors',
|
||
badgeClass
|
||
]"
|
||
>
|
||
<!-- Platform logo -->
|
||
<PlatformIcon v-if="platform" :platform="platform" size="sm" />
|
||
<!-- Group name -->
|
||
<span class="truncate">{{ name }}</span>
|
||
<!-- Right side label -->
|
||
<span v-if="showLabel" :class="labelClass">
|
||
{{ labelText }}
|
||
</span>
|
||
</span>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import type { SubscriptionType, GroupPlatform } from '@/types'
|
||
import PlatformIcon from './PlatformIcon.vue'
|
||
|
||
interface Props {
|
||
name: string
|
||
platform?: GroupPlatform
|
||
subscriptionType?: SubscriptionType
|
||
rateMultiplier?: number
|
||
showRate?: boolean
|
||
daysRemaining?: number | null // 剩余天数(订阅类型时使用)
|
||
}
|
||
|
||
const props = withDefaults(defineProps<Props>(), {
|
||
subscriptionType: 'standard',
|
||
showRate: true,
|
||
daysRemaining: null
|
||
})
|
||
|
||
const { t } = useI18n()
|
||
|
||
const isSubscription = computed(() => props.subscriptionType === 'subscription')
|
||
|
||
// 是否显示右侧标签
|
||
const showLabel = computed(() => {
|
||
if (!props.showRate) return false
|
||
// 订阅类型:显示天数或"订阅"
|
||
if (isSubscription.value) return true
|
||
// 标准类型:显示倍率
|
||
return props.rateMultiplier !== undefined
|
||
})
|
||
|
||
// Label text
|
||
const labelText = computed(() => {
|
||
if (isSubscription.value) {
|
||
// 如果有剩余天数,显示天数
|
||
if (props.daysRemaining !== null && props.daysRemaining !== undefined) {
|
||
if (props.daysRemaining <= 0) {
|
||
return t('admin.users.expired')
|
||
}
|
||
return t('admin.users.daysRemaining', { days: props.daysRemaining })
|
||
}
|
||
// 否则显示"订阅"
|
||
return t('groups.subscription')
|
||
}
|
||
return props.rateMultiplier !== undefined ? `${props.rateMultiplier}x` : ''
|
||
})
|
||
|
||
// Label style based on type and days remaining
|
||
const labelClass = computed(() => {
|
||
const base = 'px-1.5 py-0.5 rounded text-[10px] font-semibold'
|
||
|
||
if (!isSubscription.value) {
|
||
// Standard: subtle background
|
||
return `${base} bg-black/10 dark:bg-white/10`
|
||
}
|
||
|
||
// 订阅类型:根据剩余天数显示不同颜色
|
||
if (props.daysRemaining !== null && props.daysRemaining !== undefined) {
|
||
if (props.daysRemaining <= 0 || props.daysRemaining <= 3) {
|
||
// 已过期或紧急(<=3天):红色
|
||
return `${base} bg-red-200/80 text-red-800 dark:bg-red-800/50 dark:text-red-300`
|
||
}
|
||
if (props.daysRemaining <= 7) {
|
||
// 警告(<=7天):橙色
|
||
return `${base} bg-amber-200/80 text-amber-800 dark:bg-amber-800/50 dark:text-amber-300`
|
||
}
|
||
}
|
||
|
||
// 正常状态或无天数:根据平台显示主题色
|
||
if (props.platform === 'anthropic') {
|
||
return `${base} bg-orange-200/60 text-orange-800 dark:bg-orange-800/40 dark:text-orange-300`
|
||
}
|
||
if (props.platform === 'openai') {
|
||
return `${base} bg-emerald-200/60 text-emerald-800 dark:bg-emerald-800/40 dark:text-emerald-300`
|
||
}
|
||
if (props.platform === 'gemini') {
|
||
return `${base} bg-blue-200/60 text-blue-800 dark:bg-blue-800/40 dark:text-blue-300`
|
||
}
|
||
if (props.platform === 'sora') {
|
||
return `${base} bg-rose-200/60 text-rose-800 dark:bg-rose-800/40 dark:text-rose-300`
|
||
}
|
||
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
|
||
})
|
||
|
||
// Badge color based on platform and subscription type
|
||
const badgeClass = computed(() => {
|
||
if (props.platform === 'anthropic') {
|
||
// Claude: orange theme
|
||
return isSubscription.value
|
||
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
|
||
: 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400'
|
||
} else if (props.platform === 'openai') {
|
||
// OpenAI: green theme
|
||
return isSubscription.value
|
||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||
: 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
|
||
}
|
||
if (props.platform === 'gemini') {
|
||
return isSubscription.value
|
||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||
: 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400'
|
||
}
|
||
if (props.platform === 'sora') {
|
||
return isSubscription.value
|
||
? 'bg-rose-100 text-rose-700 dark:bg-rose-900/30 dark:text-rose-400'
|
||
: 'bg-rose-50 text-rose-700 dark:bg-rose-900/20 dark:text-rose-400'
|
||
}
|
||
// Fallback: original colors
|
||
return isSubscription.value
|
||
? 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||
: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||
})
|
||
</script>
|