mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
refactor(frontend): extract centralized platform color config
Create utils/platformColors.ts as single source of truth for all platform color classes (badge, border, accent, text, icon, button, discount). Refactor SubscriptionPlanCard to use it.
This commit is contained in:
@@ -3,20 +3,20 @@
|
||||
:class="[
|
||||
'group relative flex flex-col overflow-hidden rounded-2xl border transition-all',
|
||||
'hover:shadow-xl hover:-translate-y-0.5',
|
||||
cardBorderClass,
|
||||
borderClass,
|
||||
'bg-white dark:bg-dark-800',
|
||||
]"
|
||||
>
|
||||
<!-- Colored top accent bar -->
|
||||
<div :class="['h-1.5', accentBarClass]" />
|
||||
<div :class="['h-1.5', accentClass]" />
|
||||
|
||||
<div class="flex flex-1 flex-col p-5">
|
||||
<!-- Header: name + platform badge -->
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-base font-bold text-gray-900 dark:text-white">{{ plan.name }}</h3>
|
||||
<span :class="['rounded-full px-2 py-0.5 text-[11px] font-medium', platformBadgeClass]">
|
||||
{{ platformLabel }}
|
||||
<span :class="['rounded-full px-2 py-0.5 text-[11px] font-medium', badgeLightClass]">
|
||||
{{ pLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="plan.group_name" class="mt-1 text-xs text-gray-400 dark:text-dark-500">
|
||||
@@ -31,14 +31,14 @@
|
||||
<div class="mb-4">
|
||||
<div class="flex items-baseline gap-1.5">
|
||||
<span class="text-sm text-gray-400 dark:text-dark-500">¥</span>
|
||||
<span :class="['text-4xl font-extrabold tracking-tight', priceClass]">{{ plan.price }}</span>
|
||||
<span :class="['text-4xl font-extrabold tracking-tight', textClass]">{{ plan.price }}</span>
|
||||
<span class="text-sm text-gray-400 dark:text-dark-500">/ {{ validitySuffix }}</span>
|
||||
</div>
|
||||
<div v-if="plan.original_price" class="mt-1 flex items-center gap-2">
|
||||
<span class="text-sm text-gray-400 line-through dark:text-dark-500">
|
||||
¥{{ plan.original_price }}
|
||||
</span>
|
||||
<span :class="['rounded px-1.5 py-0.5 text-[11px] font-semibold', discountBadgeClass]">
|
||||
<span :class="['rounded px-1.5 py-0.5 text-[11px] font-semibold', discountClass]">
|
||||
{{ discountText }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -46,32 +46,26 @@
|
||||
|
||||
<!-- Group quota info -->
|
||||
<div class="mb-4 space-y-1.5 rounded-lg bg-gray-50 px-3 py-2.5 dark:bg-dark-700/50">
|
||||
<!-- Rate multiplier -->
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.rate') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ rateDisplay }}</span>
|
||||
</div>
|
||||
<!-- Daily limit -->
|
||||
<div v-if="plan.daily_limit_usd != null" class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.dailyLimit') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">${{ plan.daily_limit_usd }}</span>
|
||||
</div>
|
||||
<!-- Weekly limit -->
|
||||
<div v-if="plan.weekly_limit_usd != null" class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.weeklyLimit') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">${{ plan.weekly_limit_usd }}</span>
|
||||
</div>
|
||||
<!-- Monthly limit -->
|
||||
<div v-if="plan.monthly_limit_usd != null" class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.monthlyLimit') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">${{ plan.monthly_limit_usd }}</span>
|
||||
</div>
|
||||
<!-- No limits indicator -->
|
||||
<div v-if="plan.daily_limit_usd == null && plan.weekly_limit_usd == null && plan.monthly_limit_usd == null" class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.quota') }}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{{ t('payment.planCard.unlimited') }}</span>
|
||||
</div>
|
||||
<!-- Model scopes -->
|
||||
<div v-if="modelScopeLabels.length > 0" class="flex items-center justify-between text-xs">
|
||||
<span class="text-gray-400 dark:text-dark-500">{{ t('payment.planCard.models') }}</span>
|
||||
<div class="flex flex-wrap justify-end gap-1">
|
||||
@@ -85,24 +79,19 @@
|
||||
|
||||
<!-- Features list with checkmarks -->
|
||||
<div v-if="plan.features.length > 0" class="mb-4 space-y-2">
|
||||
<div
|
||||
v-for="feature in plan.features"
|
||||
:key="feature"
|
||||
class="flex items-start gap-2"
|
||||
>
|
||||
<svg :class="['mt-0.5 h-4 w-4 flex-shrink-0', checkIconClass]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<div v-for="feature in plan.features" :key="feature" class="flex items-start gap-2">
|
||||
<svg :class="['mt-0.5 h-4 w-4 flex-shrink-0', iconClass]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
<span class="text-sm text-gray-600 dark:text-gray-300">{{ feature }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Spacer pushes button to bottom -->
|
||||
<div class="flex-1" />
|
||||
|
||||
<!-- Validity badge -->
|
||||
<div class="mb-3 text-center">
|
||||
<span :class="['rounded-full px-3 py-1 text-xs font-medium', badgeClass]">
|
||||
<span :class="['rounded-full px-3 py-1 text-xs font-medium', badgeLightClass]">
|
||||
{{ validityLabel }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -110,11 +99,7 @@
|
||||
<!-- Subscribe Button -->
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'w-full rounded-xl py-2.5 text-sm font-semibold transition-all',
|
||||
'active:scale-[0.98]',
|
||||
buttonClass,
|
||||
]"
|
||||
:class="['w-full rounded-xl py-2.5 text-sm font-semibold transition-all active:scale-[0.98]', btnClass]"
|
||||
@click="emit('select', plan)"
|
||||
>
|
||||
{{ t('payment.subscribeNow') }}
|
||||
@@ -127,44 +112,44 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { SubscriptionPlan } from '@/types/payment'
|
||||
import {
|
||||
platformAccentBarClass,
|
||||
platformBadgeLightClass,
|
||||
platformBorderClass,
|
||||
platformTextClass,
|
||||
platformIconClass,
|
||||
platformButtonClass,
|
||||
platformDiscountClass,
|
||||
platformLabel,
|
||||
} from '@/utils/platformColors'
|
||||
|
||||
const props = defineProps<{
|
||||
plan: SubscriptionPlan
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [plan: SubscriptionPlan]
|
||||
}>()
|
||||
|
||||
const props = defineProps<{ plan: SubscriptionPlan }>()
|
||||
const emit = defineEmits<{ select: [plan: SubscriptionPlan] }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const platform = computed(() => props.plan.group_platform || '')
|
||||
|
||||
// Discount percentage
|
||||
// Derived color classes from central config
|
||||
const accentClass = computed(() => platformAccentBarClass(platform.value))
|
||||
const borderClass = computed(() => platformBorderClass(platform.value))
|
||||
const badgeLightClass = computed(() => platformBadgeLightClass(platform.value))
|
||||
const textClass = computed(() => platformTextClass(platform.value))
|
||||
const iconClass = computed(() => platformIconClass(platform.value))
|
||||
const btnClass = computed(() => platformButtonClass(platform.value))
|
||||
const discountClass = computed(() => platformDiscountClass(platform.value))
|
||||
const pLabel = computed(() => platformLabel(platform.value))
|
||||
|
||||
const discountText = computed(() => {
|
||||
if (!props.plan.original_price || props.plan.original_price <= 0) return ''
|
||||
const pct = Math.round((1 - props.plan.price / props.plan.original_price) * 100)
|
||||
return pct > 0 ? `-${pct}%` : ''
|
||||
})
|
||||
|
||||
// Rate multiplier display
|
||||
const rateDisplay = computed(() => {
|
||||
const rate = props.plan.rate_multiplier ?? 1
|
||||
return `×${Number(rate.toPrecision(10))}`
|
||||
})
|
||||
|
||||
// Platform display label
|
||||
const platformLabel = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'Anthropic'
|
||||
case 'openai': return 'OpenAI'
|
||||
case 'antigravity': return 'Antigravity'
|
||||
case 'gemini': return 'Gemini'
|
||||
default: return platform.value || 'API'
|
||||
}
|
||||
})
|
||||
|
||||
// Model scope labels
|
||||
const MODEL_SCOPE_LABELS: Record<string, string> = {
|
||||
claude: 'Claude',
|
||||
gemini_text: 'Gemini',
|
||||
@@ -177,87 +162,6 @@ const modelScopeLabels = computed(() => {
|
||||
return scopes.map(s => MODEL_SCOPE_LABELS[s] || s)
|
||||
})
|
||||
|
||||
// Color schemes per platform
|
||||
const accentBarClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'bg-gradient-to-r from-orange-400 to-orange-500'
|
||||
case 'openai': return 'bg-gradient-to-r from-emerald-400 to-emerald-500'
|
||||
case 'antigravity': return 'bg-gradient-to-r from-purple-400 to-purple-500'
|
||||
case 'gemini': return 'bg-gradient-to-r from-blue-400 to-blue-500'
|
||||
default: return 'bg-gradient-to-r from-primary-400 to-primary-500'
|
||||
}
|
||||
})
|
||||
|
||||
const cardBorderClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'border-orange-100 dark:border-orange-900/30'
|
||||
case 'openai': return 'border-emerald-100 dark:border-emerald-900/30'
|
||||
case 'antigravity': return 'border-purple-100 dark:border-purple-900/30'
|
||||
case 'gemini': return 'border-blue-100 dark:border-blue-900/30'
|
||||
default: return 'border-gray-200 dark:border-dark-700'
|
||||
}
|
||||
})
|
||||
|
||||
const platformBadgeClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'bg-orange-50 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300'
|
||||
case 'openai': return 'bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
case 'antigravity': return 'bg-purple-50 text-purple-600 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
case 'gemini': return 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
default: return 'bg-gray-100 text-gray-600 dark:bg-dark-700 dark:text-gray-300'
|
||||
}
|
||||
})
|
||||
|
||||
const badgeClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'bg-orange-50 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300'
|
||||
case 'openai': return 'bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-300'
|
||||
case 'antigravity': return 'bg-purple-50 text-purple-600 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
case 'gemini': return 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300'
|
||||
default: return 'bg-gray-100 text-gray-600 dark:bg-dark-700 dark:text-gray-300'
|
||||
}
|
||||
})
|
||||
|
||||
const priceClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'text-orange-600 dark:text-orange-400'
|
||||
case 'openai': return 'text-emerald-600 dark:text-emerald-400'
|
||||
case 'antigravity': return 'text-purple-600 dark:text-purple-400'
|
||||
case 'gemini': return 'text-blue-600 dark:text-blue-400'
|
||||
default: return 'text-primary-600 dark:text-primary-400'
|
||||
}
|
||||
})
|
||||
|
||||
const discountBadgeClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300'
|
||||
case 'openai': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300'
|
||||
case 'antigravity': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300'
|
||||
case 'gemini': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'
|
||||
default: return 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||
}
|
||||
})
|
||||
|
||||
const checkIconClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'text-orange-500 dark:text-orange-400'
|
||||
case 'openai': return 'text-emerald-500 dark:text-emerald-400'
|
||||
case 'antigravity': return 'text-purple-500 dark:text-purple-400'
|
||||
case 'gemini': return 'text-blue-500 dark:text-blue-400'
|
||||
default: return 'text-primary-500 dark:text-primary-400'
|
||||
}
|
||||
})
|
||||
|
||||
const buttonClass = computed(() => {
|
||||
switch (platform.value) {
|
||||
case 'anthropic': return 'bg-orange-500 text-white hover:bg-orange-600 dark:bg-orange-600 dark:hover:bg-orange-500'
|
||||
case 'openai': return 'bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-500'
|
||||
case 'antigravity': return 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-500'
|
||||
case 'gemini': return 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500'
|
||||
default: return 'bg-primary-500 text-white hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-500'
|
||||
}
|
||||
})
|
||||
|
||||
const validityLabel = computed(() => {
|
||||
const d = props.plan.validity_days
|
||||
const u = props.plan.validity_unit || 'day'
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Centralized platform color definitions.
|
||||
*
|
||||
* All components that need platform-specific styling should import from here
|
||||
* instead of defining their own color mappings.
|
||||
*/
|
||||
|
||||
export type Platform = 'anthropic' | 'openai' | 'antigravity' | 'gemini'
|
||||
|
||||
// ── Badge (bg + text) ───────────────────────────────────────────────
|
||||
const BADGE: Record<Platform, string> = {
|
||||
anthropic: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400',
|
||||
openai: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400',
|
||||
antigravity: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
|
||||
gemini: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
|
||||
}
|
||||
const BADGE_DEFAULT = 'bg-gray-100 text-gray-600 dark:bg-dark-700 dark:text-gray-300'
|
||||
|
||||
// ── Light badge (softer bg) ─────────────────────────────────────────
|
||||
const BADGE_LIGHT: Record<Platform, string> = {
|
||||
anthropic: 'bg-orange-50 text-orange-600 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
openai: 'bg-emerald-50 text-emerald-600 dark:bg-emerald-900/30 dark:text-emerald-300',
|
||||
antigravity: 'bg-purple-50 text-purple-600 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
gemini: 'bg-blue-50 text-blue-600 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
}
|
||||
|
||||
// ── Border ──────────────────────────────────────────────────────────
|
||||
const BORDER: Record<Platform, string> = {
|
||||
anthropic: 'border-orange-100 dark:border-orange-900/30',
|
||||
openai: 'border-emerald-100 dark:border-emerald-900/30',
|
||||
antigravity: 'border-purple-100 dark:border-purple-900/30',
|
||||
gemini: 'border-blue-100 dark:border-blue-900/30',
|
||||
}
|
||||
const BORDER_DEFAULT = 'border-gray-200 dark:border-dark-700'
|
||||
|
||||
// ── Accent bar (gradient) ───────────────────────────────────────────
|
||||
const ACCENT_BAR: Record<Platform, string> = {
|
||||
anthropic: 'bg-gradient-to-r from-orange-400 to-orange-500',
|
||||
openai: 'bg-gradient-to-r from-emerald-400 to-emerald-500',
|
||||
antigravity: 'bg-gradient-to-r from-purple-400 to-purple-500',
|
||||
gemini: 'bg-gradient-to-r from-blue-400 to-blue-500',
|
||||
}
|
||||
const ACCENT_BAR_DEFAULT = 'bg-gradient-to-r from-primary-400 to-primary-500'
|
||||
|
||||
// ── Text (price, icon) ─────────────────────────────────────────────
|
||||
const TEXT: Record<Platform, string> = {
|
||||
anthropic: 'text-orange-600 dark:text-orange-400',
|
||||
openai: 'text-emerald-600 dark:text-emerald-400',
|
||||
antigravity: 'text-purple-600 dark:text-purple-400',
|
||||
gemini: 'text-blue-600 dark:text-blue-400',
|
||||
}
|
||||
const TEXT_DEFAULT = 'text-primary-600 dark:text-primary-400'
|
||||
|
||||
// ── Icon (check mark etc.) ──────────────────────────────────────────
|
||||
const ICON: Record<Platform, string> = {
|
||||
anthropic: 'text-orange-500 dark:text-orange-400',
|
||||
openai: 'text-emerald-500 dark:text-emerald-400',
|
||||
antigravity: 'text-purple-500 dark:text-purple-400',
|
||||
gemini: 'text-blue-500 dark:text-blue-400',
|
||||
}
|
||||
const ICON_DEFAULT = 'text-primary-500 dark:text-primary-400'
|
||||
|
||||
// ── Button (solid bg) ───────────────────────────────────────────────
|
||||
const BUTTON: Record<Platform, string> = {
|
||||
anthropic: 'bg-orange-500 text-white hover:bg-orange-600 dark:bg-orange-600 dark:hover:bg-orange-500',
|
||||
openai: 'bg-emerald-500 text-white hover:bg-emerald-600 dark:bg-emerald-600 dark:hover:bg-emerald-500',
|
||||
antigravity: 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-500',
|
||||
gemini: 'bg-blue-500 text-white hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-500',
|
||||
}
|
||||
const BUTTON_DEFAULT = 'bg-primary-500 text-white hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-500'
|
||||
|
||||
// ── Discount badge ──────────────────────────────────────────────────
|
||||
const DISCOUNT: Record<Platform, string> = {
|
||||
anthropic: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
|
||||
openai: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
|
||||
antigravity: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
|
||||
gemini: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
|
||||
}
|
||||
const DISCOUNT_DEFAULT = 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300'
|
||||
|
||||
// ── Public API ──────────────────────────────────────────────────────
|
||||
|
||||
function isPlatform(p: string): p is Platform {
|
||||
return p === 'anthropic' || p === 'openai' || p === 'antigravity' || p === 'gemini'
|
||||
}
|
||||
|
||||
export function platformBadgeClass(p: string): string {
|
||||
return isPlatform(p) ? BADGE[p] : BADGE_DEFAULT
|
||||
}
|
||||
|
||||
export function platformBadgeLightClass(p: string): string {
|
||||
return isPlatform(p) ? BADGE_LIGHT[p] : BADGE_DEFAULT
|
||||
}
|
||||
|
||||
export function platformBorderClass(p: string): string {
|
||||
return isPlatform(p) ? BORDER[p] : BORDER_DEFAULT
|
||||
}
|
||||
|
||||
export function platformAccentBarClass(p: string): string {
|
||||
return isPlatform(p) ? ACCENT_BAR[p] : ACCENT_BAR_DEFAULT
|
||||
}
|
||||
|
||||
export function platformTextClass(p: string): string {
|
||||
return isPlatform(p) ? TEXT[p] : TEXT_DEFAULT
|
||||
}
|
||||
|
||||
export function platformIconClass(p: string): string {
|
||||
return isPlatform(p) ? ICON[p] : ICON_DEFAULT
|
||||
}
|
||||
|
||||
export function platformButtonClass(p: string): string {
|
||||
return isPlatform(p) ? BUTTON[p] : BUTTON_DEFAULT
|
||||
}
|
||||
|
||||
export function platformDiscountClass(p: string): string {
|
||||
return isPlatform(p) ? DISCOUNT[p] : DISCOUNT_DEFAULT
|
||||
}
|
||||
|
||||
export function platformLabel(p: string): string {
|
||||
switch (p) {
|
||||
case 'anthropic': return 'Anthropic'
|
||||
case 'openai': return 'OpenAI'
|
||||
case 'antigravity': return 'Antigravity'
|
||||
case 'gemini': return 'Gemini'
|
||||
default: return p || 'API'
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user