style(frontend): 优化 Components 代码风格和结构
- 统一移除语句末尾分号,规范代码格式 - 优化组件类型定义和 props 声明 - 改进组件文档和示例代码 - 提升代码可读性和一致性
This commit is contained in:
@@ -3,11 +3,21 @@
|
||||
<!-- Mini Progress Display -->
|
||||
<button
|
||||
@click="toggleTooltip"
|
||||
class="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-purple-50 dark:bg-purple-900/20 hover:bg-purple-100 dark:hover:bg-purple-900/30 transition-colors cursor-pointer"
|
||||
class="flex cursor-pointer items-center gap-2 rounded-xl bg-purple-50 px-3 py-1.5 transition-colors hover:bg-purple-100 dark:bg-purple-900/20 dark:hover:bg-purple-900/30"
|
||||
:title="t('subscriptionProgress.viewDetails')"
|
||||
>
|
||||
<svg class="w-4 h-4 text-purple-600 dark:text-purple-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
|
||||
<svg
|
||||
class="h-4 w-4 text-purple-600 dark:text-purple-400"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<!-- Combined progress indicator -->
|
||||
@@ -15,7 +25,7 @@
|
||||
<div
|
||||
v-for="(sub, index) in displaySubscriptions.slice(0, 3)"
|
||||
:key="index"
|
||||
class="w-2 h-2 rounded-full"
|
||||
class="h-2 w-2 rounded-full"
|
||||
:class="getProgressDotClass(sub)"
|
||||
></div>
|
||||
</div>
|
||||
@@ -29,13 +39,13 @@
|
||||
<transition name="dropdown">
|
||||
<div
|
||||
v-if="tooltipOpen"
|
||||
class="absolute right-0 mt-2 w-[340px] bg-white dark:bg-dark-800 rounded-xl shadow-xl border border-gray-200 dark:border-dark-700 z-50 overflow-hidden"
|
||||
class="absolute right-0 z-50 mt-2 w-[340px] overflow-hidden rounded-xl border border-gray-200 bg-white shadow-xl dark:border-dark-700 dark:bg-dark-800"
|
||||
>
|
||||
<div class="p-3 border-b border-gray-100 dark:border-dark-700">
|
||||
<div class="border-b border-gray-100 p-3 dark:border-dark-700">
|
||||
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('subscriptionProgress.title') }}
|
||||
</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-dark-400 mt-0.5">
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-dark-400">
|
||||
{{ t('subscriptionProgress.activeCount', { count: activeSubscriptions.length }) }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -44,9 +54,9 @@
|
||||
<div
|
||||
v-for="subscription in displaySubscriptions"
|
||||
:key="subscription.id"
|
||||
class="p-3 border-b border-gray-50 dark:border-dark-700/50 last:border-b-0"
|
||||
class="border-b border-gray-50 p-3 last:border-b-0 dark:border-dark-700/50"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ subscription.group?.name || `Group #${subscription.group_id}` }}
|
||||
</span>
|
||||
@@ -62,55 +72,100 @@
|
||||
<!-- Progress bars -->
|
||||
<div class="space-y-1.5">
|
||||
<div v-if="subscription.group?.daily_limit_usd" class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-gray-500 w-8 flex-shrink-0">{{ t('subscriptionProgress.daily') }}</span>
|
||||
<div class="flex-1 min-w-0 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
|
||||
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
||||
t('subscriptionProgress.daily')
|
||||
}}</span>
|
||||
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all"
|
||||
:class="getProgressBarClass(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)"
|
||||
:style="{ width: getProgressWidth(subscription.daily_usage_usd, subscription.group?.daily_limit_usd) }"
|
||||
:class="
|
||||
getProgressBarClass(
|
||||
subscription.daily_usage_usd,
|
||||
subscription.group?.daily_limit_usd
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
width: getProgressWidth(
|
||||
subscription.daily_usage_usd,
|
||||
subscription.group?.daily_limit_usd
|
||||
)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-[10px] text-gray-500 w-24 text-right flex-shrink-0">
|
||||
{{ formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd) }}
|
||||
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
||||
{{
|
||||
formatUsage(subscription.daily_usage_usd, subscription.group?.daily_limit_usd)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="subscription.group?.weekly_limit_usd" class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-gray-500 w-8 flex-shrink-0">{{ t('subscriptionProgress.weekly') }}</span>
|
||||
<div class="flex-1 min-w-0 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
|
||||
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
||||
t('subscriptionProgress.weekly')
|
||||
}}</span>
|
||||
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all"
|
||||
:class="getProgressBarClass(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)"
|
||||
:style="{ width: getProgressWidth(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd) }"
|
||||
:class="
|
||||
getProgressBarClass(
|
||||
subscription.weekly_usage_usd,
|
||||
subscription.group?.weekly_limit_usd
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
width: getProgressWidth(
|
||||
subscription.weekly_usage_usd,
|
||||
subscription.group?.weekly_limit_usd
|
||||
)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-[10px] text-gray-500 w-24 text-right flex-shrink-0">
|
||||
{{ formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd) }}
|
||||
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
||||
{{
|
||||
formatUsage(subscription.weekly_usage_usd, subscription.group?.weekly_limit_usd)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="subscription.group?.monthly_limit_usd" class="flex items-center gap-2">
|
||||
<span class="text-[10px] text-gray-500 w-8 flex-shrink-0">{{ t('subscriptionProgress.monthly') }}</span>
|
||||
<div class="flex-1 min-w-0 bg-gray-200 dark:bg-dark-600 rounded-full h-1.5">
|
||||
<span class="w-8 flex-shrink-0 text-[10px] text-gray-500">{{
|
||||
t('subscriptionProgress.monthly')
|
||||
}}</span>
|
||||
<div class="h-1.5 min-w-0 flex-1 rounded-full bg-gray-200 dark:bg-dark-600">
|
||||
<div
|
||||
class="h-1.5 rounded-full transition-all"
|
||||
:class="getProgressBarClass(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd)"
|
||||
:style="{ width: getProgressWidth(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd) }"
|
||||
:class="
|
||||
getProgressBarClass(
|
||||
subscription.monthly_usage_usd,
|
||||
subscription.group?.monthly_limit_usd
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
width: getProgressWidth(
|
||||
subscription.monthly_usage_usd,
|
||||
subscription.group?.monthly_limit_usd
|
||||
)
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<span class="text-[10px] text-gray-500 w-24 text-right flex-shrink-0">
|
||||
{{ formatUsage(subscription.monthly_usage_usd, subscription.group?.monthly_limit_usd) }}
|
||||
<span class="w-24 flex-shrink-0 text-right text-[10px] text-gray-500">
|
||||
{{
|
||||
formatUsage(
|
||||
subscription.monthly_usage_usd,
|
||||
subscription.group?.monthly_limit_usd
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-2 border-t border-gray-100 dark:border-dark-700">
|
||||
<div class="border-t border-gray-100 p-2 dark:border-dark-700">
|
||||
<router-link
|
||||
to="/subscriptions"
|
||||
@click="closeTooltip"
|
||||
class="block w-full text-center text-xs text-primary-600 dark:text-primary-400 hover:underline py-1"
|
||||
class="block w-full py-1 text-center text-xs text-primary-600 hover:underline dark:text-primary-400"
|
||||
>
|
||||
{{ t('subscriptionProgress.viewAll') }}
|
||||
</router-link>
|
||||
@@ -121,136 +176,136 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import subscriptionsAPI from '@/api/subscriptions';
|
||||
import type { UserSubscription } from '@/types';
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import subscriptionsAPI from '@/api/subscriptions'
|
||||
import type { UserSubscription } from '@/types'
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const tooltipOpen = ref(false);
|
||||
const activeSubscriptions = ref<UserSubscription[]>([]);
|
||||
const loading = ref(false);
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const tooltipOpen = ref(false)
|
||||
const activeSubscriptions = ref<UserSubscription[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const hasActiveSubscriptions = computed(() => activeSubscriptions.value.length > 0);
|
||||
const hasActiveSubscriptions = computed(() => activeSubscriptions.value.length > 0)
|
||||
|
||||
const displaySubscriptions = computed(() => {
|
||||
// Sort by most usage (highest percentage first)
|
||||
return [...activeSubscriptions.value].sort((a, b) => {
|
||||
const aMax = getMaxUsagePercentage(a);
|
||||
const bMax = getMaxUsagePercentage(b);
|
||||
return bMax - aMax;
|
||||
});
|
||||
});
|
||||
const aMax = getMaxUsagePercentage(a)
|
||||
const bMax = getMaxUsagePercentage(b)
|
||||
return bMax - aMax
|
||||
})
|
||||
})
|
||||
|
||||
function getMaxUsagePercentage(sub: UserSubscription): number {
|
||||
const percentages: number[] = [];
|
||||
const percentages: number[] = []
|
||||
if (sub.group?.daily_limit_usd) {
|
||||
percentages.push((sub.daily_usage_usd || 0) / sub.group.daily_limit_usd * 100);
|
||||
percentages.push(((sub.daily_usage_usd || 0) / sub.group.daily_limit_usd) * 100)
|
||||
}
|
||||
if (sub.group?.weekly_limit_usd) {
|
||||
percentages.push((sub.weekly_usage_usd || 0) / sub.group.weekly_limit_usd * 100);
|
||||
percentages.push(((sub.weekly_usage_usd || 0) / sub.group.weekly_limit_usd) * 100)
|
||||
}
|
||||
if (sub.group?.monthly_limit_usd) {
|
||||
percentages.push((sub.monthly_usage_usd || 0) / sub.group.monthly_limit_usd * 100);
|
||||
percentages.push(((sub.monthly_usage_usd || 0) / sub.group.monthly_limit_usd) * 100)
|
||||
}
|
||||
return percentages.length > 0 ? Math.max(...percentages) : 0;
|
||||
return percentages.length > 0 ? Math.max(...percentages) : 0
|
||||
}
|
||||
|
||||
function getProgressDotClass(sub: UserSubscription): string {
|
||||
const maxPercentage = getMaxUsagePercentage(sub);
|
||||
if (maxPercentage >= 90) return 'bg-red-500';
|
||||
if (maxPercentage >= 70) return 'bg-orange-500';
|
||||
return 'bg-green-500';
|
||||
const maxPercentage = getMaxUsagePercentage(sub)
|
||||
if (maxPercentage >= 90) return 'bg-red-500'
|
||||
if (maxPercentage >= 70) return 'bg-orange-500'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
function getProgressBarClass(used: number | undefined, limit: number | null | undefined): string {
|
||||
if (!limit || limit === 0) return 'bg-gray-400';
|
||||
const percentage = ((used || 0) / limit) * 100;
|
||||
if (percentage >= 90) return 'bg-red-500';
|
||||
if (percentage >= 70) return 'bg-orange-500';
|
||||
return 'bg-green-500';
|
||||
if (!limit || limit === 0) return 'bg-gray-400'
|
||||
const percentage = ((used || 0) / limit) * 100
|
||||
if (percentage >= 90) return 'bg-red-500'
|
||||
if (percentage >= 70) return 'bg-orange-500'
|
||||
return 'bg-green-500'
|
||||
}
|
||||
|
||||
function getProgressWidth(used: number | undefined, limit: number | null | undefined): string {
|
||||
if (!limit || limit === 0) return '0%';
|
||||
const percentage = Math.min(((used || 0) / limit) * 100, 100);
|
||||
return `${percentage}%`;
|
||||
if (!limit || limit === 0) return '0%'
|
||||
const percentage = Math.min(((used || 0) / limit) * 100, 100)
|
||||
return `${percentage}%`
|
||||
}
|
||||
|
||||
function formatUsage(used: number | undefined, limit: number | null | undefined): string {
|
||||
const usedValue = (used || 0).toFixed(2);
|
||||
const limitValue = limit?.toFixed(2) || '∞';
|
||||
return `$${usedValue}/$${limitValue}`;
|
||||
const usedValue = (used || 0).toFixed(2)
|
||||
const limitValue = limit?.toFixed(2) || '∞'
|
||||
return `$${usedValue}/$${limitValue}`
|
||||
}
|
||||
|
||||
function formatDaysRemaining(expiresAt: string): string {
|
||||
const now = new Date();
|
||||
const expires = new Date(expiresAt);
|
||||
const diff = expires.getTime() - now.getTime();
|
||||
if (diff < 0) return t('subscriptionProgress.expired');
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
if (days === 0) return t('subscriptionProgress.expirestoday');
|
||||
if (days === 1) return t('subscriptionProgress.expiresTomorrow');
|
||||
return t('subscriptionProgress.daysRemaining', { days });
|
||||
const now = new Date()
|
||||
const expires = new Date(expiresAt)
|
||||
const diff = expires.getTime() - now.getTime()
|
||||
if (diff < 0) return t('subscriptionProgress.expired')
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
if (days === 0) return t('subscriptionProgress.expirestoday')
|
||||
if (days === 1) return t('subscriptionProgress.expiresTomorrow')
|
||||
return t('subscriptionProgress.daysRemaining', { days })
|
||||
}
|
||||
|
||||
function getDaysRemainingClass(expiresAt: string): string {
|
||||
const now = new Date();
|
||||
const expires = new Date(expiresAt);
|
||||
const diff = expires.getTime() - now.getTime();
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
if (days <= 3) return 'text-red-600 dark:text-red-400';
|
||||
if (days <= 7) return 'text-orange-600 dark:text-orange-400';
|
||||
return 'text-gray-500 dark:text-dark-400';
|
||||
const now = new Date()
|
||||
const expires = new Date(expiresAt)
|
||||
const diff = expires.getTime() - now.getTime()
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24))
|
||||
if (days <= 3) return 'text-red-600 dark:text-red-400'
|
||||
if (days <= 7) return 'text-orange-600 dark:text-orange-400'
|
||||
return 'text-gray-500 dark:text-dark-400'
|
||||
}
|
||||
|
||||
function toggleTooltip() {
|
||||
tooltipOpen.value = !tooltipOpen.value;
|
||||
tooltipOpen.value = !tooltipOpen.value
|
||||
}
|
||||
|
||||
function closeTooltip() {
|
||||
tooltipOpen.value = false;
|
||||
tooltipOpen.value = false
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
|
||||
closeTooltip();
|
||||
closeTooltip()
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubscriptions() {
|
||||
try {
|
||||
loading.value = true;
|
||||
activeSubscriptions.value = await subscriptionsAPI.getActiveSubscriptions();
|
||||
loading.value = true
|
||||
activeSubscriptions.value = await subscriptionsAPI.getActiveSubscriptions()
|
||||
} catch (error) {
|
||||
console.error('Failed to load subscriptions:', error);
|
||||
activeSubscriptions.value = [];
|
||||
console.error('Failed to load subscriptions:', error)
|
||||
activeSubscriptions.value = []
|
||||
} finally {
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
loadSubscriptions();
|
||||
});
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
loadSubscriptions()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
// Refresh subscriptions periodically (every 5 minutes)
|
||||
let refreshInterval: ReturnType<typeof setInterval> | null = null;
|
||||
let refreshInterval: ReturnType<typeof setInterval> | null = null
|
||||
onMounted(() => {
|
||||
refreshInterval = setInterval(loadSubscriptions, 5 * 60 * 1000);
|
||||
});
|
||||
refreshInterval = setInterval(loadSubscriptions, 5 * 60 * 1000)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
clearInterval(refreshInterval)
|
||||
}
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user