Add cross-panel tenant shortcuts with live status colors and values.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-08 12:35:59 +03:30
co-authored by Cursor
parent d0d621502f
commit da25d91ba7
6 changed files with 354 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import {
MessageOutlined,
CloudServerOutlined,
AppstoreAddOutlined,
CustomerServiceOutlined,
} from '@ant-design/icons-vue'
import api from '@/api/http'
export type TenantPanelKey = 'sms' | 'server' | 'plugin' | 'support'
type Tone = 'healthy' | 'warning' | 'critical' | 'unknown' | 'dedicated'
type ShortcutCard = {
key: TenantPanelKey
label: string
routeName: string
icon: typeof MessageOutlined
tone: Tone
value: string
meta?: string
}
const props = defineProps<{
tenantId: string
current: TenantPanelKey
}>()
const router = useRouter()
const loading = ref(false)
const cards = ref<ShortcutCard[]>([])
const EXPIRY_RANK: Record<string, number> = {
critical: 0,
warning: 1,
unknown: 2,
none: 3,
healthy: 4,
dedicated: 5,
}
const panelDefs: {
key: TenantPanelKey
label: string
routeName: string
icon: typeof MessageOutlined
}[] = [
{ key: 'sms', label: 'پیامک', routeName: 'sms-tenant', icon: MessageOutlined },
{ key: 'server', label: 'سرور', routeName: 'server-tenant', icon: CloudServerOutlined },
{ key: 'plugin', label: 'افزونه', routeName: 'plugin-tenant', icon: AppstoreAddOutlined },
{ key: 'support', label: 'پشتیبانی', routeName: 'support-tenant', icon: CustomerServiceOutlined },
]
const visible = computed(() => cards.value.filter((c) => c.key !== props.current))
function asTone(level?: string | null): Tone {
if (
level === 'healthy' ||
level === 'warning' ||
level === 'critical' ||
level === 'dedicated' ||
level === 'unknown'
) {
return level
}
return 'unknown'
}
function worstLevel(levels: string[]): Tone {
if (!levels.length) return 'unknown'
let worst = levels[0]
for (const level of levels.slice(1)) {
if ((EXPIRY_RANK[level] ?? 99) < (EXPIRY_RANK[worst] ?? 99)) worst = level
}
return asTone(worst === 'none' ? 'unknown' : worst)
}
function pluginSupportLabel(
kind: 'plugin' | 'support',
count: number,
level: Tone,
): string {
const empty = kind === 'plugin' ? 'بدون افزونه' : 'بدون پشتیبانی'
const noun = kind === 'plugin' ? 'افزونه' : 'پشتیبانی'
if (count === 0) return empty
if (level === 'critical') return `${noun} نزدیک انقضا / منقضی`
if (level === 'warning') return `${noun} در بازه هشدار`
if (level === 'unknown') return `${noun} بدون تاریخ`
return `${count} ${noun} فعال`
}
async function loadSms(): Promise<ShortcutCard> {
const def = panelDefs.find((p) => p.key === 'sms')!
try {
const { data } = await api.get(`/sms/tenants/${props.tenantId}`)
return {
...def,
tone: asTone(data?.summary?.balanceLevel),
value: data?.summary?.balanceLabel || 'نامشخص',
meta: data?.account?.isBlocked ? 'ارسال مسدود' : undefined,
}
} catch {
return { ...def, tone: 'unknown', value: 'خطا در دریافت' }
}
}
async function loadServer(): Promise<ShortcutCard> {
const def = panelDefs.find((p) => p.key === 'server')!
try {
const { data } = await api.get(`/servers/tenants/${props.tenantId}`)
const servers: Array<{
summary?: {
expiryLevel?: string
expiryLabel?: string
serverName?: string | null
expiresAt?: string | null
isDedicated?: boolean
}
}> = data?.servers || []
if (!servers.length) {
return { ...def, tone: 'unknown', value: 'بدون سرور' }
}
let worst = servers[0].summary
for (const row of servers.slice(1)) {
const s = row.summary
if (!s || !worst) continue
const wr = EXPIRY_RANK[worst.expiryLevel || 'unknown'] ?? 99
const sr = EXPIRY_RANK[s.expiryLevel || 'unknown'] ?? 99
if (sr < wr) worst = s
else if (
sr === wr &&
s.expiresAt &&
worst.expiresAt &&
Date.parse(s.expiresAt) < Date.parse(worst.expiresAt)
) {
worst = s
}
}
const names = servers
.map((s) => s.summary?.serverName)
.filter(Boolean)
.slice(0, 2) as string[]
const meta =
servers.length > 1
? `${servers.length} سرور${names.length ? ` · ${names.join('، ')}` : ''}`
: names[0] || undefined
return {
...def,
tone: asTone(worst?.expiryLevel),
value: worst?.expiryLabel || 'نامشخص',
meta,
}
} catch {
return { ...def, tone: 'unknown', value: 'خطا در دریافت' }
}
}
async function loadPluginLike(
key: 'plugin' | 'support',
): Promise<ShortcutCard> {
const def = panelDefs.find((p) => p.key === key)!
const path = key === 'plugin' ? 'plugins' : 'support'
const listKey = key === 'plugin' ? 'plugins' : 'supports'
try {
const { data } = await api.get(`/${path}/tenants/${props.tenantId}`)
const items: Array<{ isActive?: boolean; summary?: { expiryLevel?: string } }> =
data?.[listKey] || []
const active = items.filter((i) => i.isActive !== false)
const levels = active.map((i) => i.summary?.expiryLevel || 'unknown')
const tone = worstLevel(levels)
return {
...def,
tone,
value: pluginSupportLabel(key, active.length, tone),
meta: active.length ? `${active.length} مورد` : undefined,
}
} catch {
return { ...def, tone: 'unknown', value: 'خطا در دریافت' }
}
}
async function loadAll() {
if (!props.tenantId) {
cards.value = []
return
}
loading.value = true
try {
const loaders: Promise<ShortcutCard>[] = []
if (props.current !== 'sms') loaders.push(loadSms())
if (props.current !== 'server') loaders.push(loadServer())
if (props.current !== 'plugin') loaders.push(loadPluginLike('plugin'))
if (props.current !== 'support') loaders.push(loadPluginLike('support'))
const order: TenantPanelKey[] = ['sms', 'server', 'plugin', 'support']
const loaded = await Promise.all(loaders)
cards.value = order
.map((key) => loaded.find((c) => c.key === key))
.filter(Boolean) as ShortcutCard[]
} finally {
loading.value = false
}
}
function openPanel(routeName: string) {
if (!props.tenantId) return
router.push({ name: routeName, params: { tenantId: props.tenantId } })
}
watch(
() => [props.tenantId, props.current] as const,
() => {
void loadAll()
},
{ immediate: true },
)
</script>
<template>
<nav
v-if="tenantId"
class="tenant-panel-shortcuts"
aria-label="میانبر پنل‌های همین نسخه"
>
<div v-if="loading && !visible.length" class="tenant-panel-shortcuts-loading">
در حال بارگذاری میانبرها…
</div>
<button
v-for="item in visible"
:key="item.key"
type="button"
class="tenant-panel-status-card"
:class="item.tone"
:title="`رفتن به پنل ${item.label} همین نسخه`"
@click="openPanel(item.routeName)"
>
<div class="card-top">
<span class="card-icon"><component :is="item.icon" /></span>
<strong>{{ item.label }}</strong>
</div>
<span class="card-value">{{ item.value }}</span>
<small v-if="item.meta" class="card-meta">{{ item.meta }}</small>
</button>
</nav>
</template>