feat: 用户列表显示订阅分组及剩余天数

- User模型新增Subscriptions关联
- 用户列表批量加载订阅信息避免N+1查询
- GroupBadge组件支持显示剩余天数(过期红色、<=3天红色、<=7天橙色)
- 用户管理页面新增订阅分组列
This commit is contained in:
shaw
2025-12-23 11:03:10 +08:00
parent 5bbfbcdae9
commit f0fabf89a1
7 changed files with 125 additions and 16 deletions

View File

@@ -9,9 +9,9 @@
<PlatformIcon v-if="platform" :platform="platform" size="sm" />
<!-- Group name -->
<span class="truncate">{{ name }}</span>
<!-- Right side label: subscription shows "订阅", standard shows rate multiplier -->
<!-- Right side label -->
<span
v-if="showRate"
v-if="showLabel"
:class="labelClass"
>
{{ labelText }}
@@ -31,39 +31,73 @@ interface Props {
subscriptionType?: SubscriptionType
rateMultiplier?: number
showRate?: boolean
daysRemaining?: number | null // 剩余天数(订阅类型时使用)
}
const props = withDefaults(defineProps<Props>(), {
subscriptionType: 'standard',
showRate: true
showRate: true,
daysRemaining: null
})
const { t } = useI18n()
const isSubscription = computed(() => props.subscriptionType === 'subscription')
// Label text: subscription shows localized text, standard shows rate
// 是否显示右侧标签
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
// 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) {
// Subscription: more prominent style with border
if (props.platform === 'anthropic') {
return `${base} bg-orange-200/60 text-orange-800 dark:bg-orange-800/40 dark:text-orange-300`
} else if (props.platform === 'openai') {
return `${base} bg-emerald-200/60 text-emerald-800 dark:bg-emerald-800/40 dark:text-emerald-300`
}
return `${base} bg-violet-200/60 text-violet-800 dark:bg-violet-800/40 dark:text-violet-300`
if (!isSubscription.value) {
// Standard: subtle background
return `${base} bg-black/10 dark:bg-white/10`
}
// 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`
}
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