平台配置页改造

This commit is contained in:
yml2213
2026-05-14 19:05:08 +08:00
parent f7f3289d04
commit 7775525f30
15 changed files with 2351 additions and 1727 deletions
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { RefreshRight } from '@element-plus/icons-vue'
import AdminPlatformAgisoSection from '@/components/admin/AdminPlatformAgisoSection.vue'
import AdminPlatformCloudtentaclesSection from '@/components/admin/AdminPlatformCloudtentaclesSection.vue'
@@ -20,7 +21,6 @@ import {
fetchAdminScheduledJobsConfig,
} from '@/services/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import '@/styles/admin-platform-shops.css'
const loading = ref(true)
const isDevMode = import.meta.env.DEV
@@ -189,287 +189,317 @@ async function loadConfigs() {
}
}
function switchPlatform(platform: PlatformTab) {
activePlatform.value = platform
}
async function saveAgisoConfigs() {
await persistAgisoConfigs(loadConfigs)
}
function switchTab(tab: string) {
activePlatform.value = tab as PlatformTab
}
onMounted(loadConfigs)
</script>
<template>
<section class="admin-panel admin-platform-shops">
<header class="hero-card">
<div class="hero-copy">
<div class="platform-page">
<!-- 页面头部 -->
<div class="page-header">
<div>
<h1>平台配置</h1>
<p>按平台分别管理来源接入凭据与店铺能力Agiso 负责 webhook / 店铺消息配置91卡券负责新订单来源快手小店核销负责独立券核销调试cloudtentacles 负责外部履约平台登录</p>
<p>按平台分别管理来源接入凭据与店铺能力</p>
</div>
<div class="hero-actions">
<el-button round @click="loadConfigs">刷新全部配置</el-button>
<el-button v-if="activePlatform === 'agiso'" round type="primary" @click="addShop">新增 Agiso 店铺</el-button>
<div class="header-actions">
<el-button circle title="刷新全部配置" @click="loadConfigs">
<template #icon><el-icon><RefreshRight /></el-icon></template>
</el-button>
<el-button
v-if="activePlatform === 'agiso'"
type="primary"
@click="addShop"
>
新增 Agiso 店铺
</el-button>
</div>
</header>
</div>
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护平台配置</div>
<!-- 权限不足 -->
<el-result
v-if="!hasAdminRole('admin')"
icon="warning"
title="仅管理员可以维护平台配置"
sub-title="请切换管理员账号登录"
/>
<template v-else>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">平台配置加载中</div>
<!-- 错误提示 -->
<el-alert
v-if="errorMessage"
:title="errorMessage"
type="error"
show-icon
:closable="false"
style="margin-bottom: 16px"
/>
<!-- 加载态 -->
<el-skeleton v-if="loading" :rows="8" animated />
<template v-else>
<section class="overview-grid">
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'agiso' }]"
@click="switchPlatform('agiso')"
<!-- 概览卡片 -->
<div class="overview-grid">
<el-card
v-for="card in [
{ key: 'agiso', label: 'Agiso', tag: 'Webhook / 店铺', value: agisoStats.configuredShopCount, unit: '已配置店铺', meta: `识别到 ${agisoStats.observedShopCount} 家 · 待补 ${agisoStats.pendingObservedCount} 家` },
{ key: 'notifications', label: '内部通知', tag: 'Bark / 值班', value: notificationStats.enabledRecipientCount, unit: '启用接收人', meta: `配置 ${notificationStats.configuredRecipientCount} 人 · 最近成功 ${notificationStats.lastSuccessCount} 个` },
{ key: 'ninetyone', label: '91卡券', tag: '接入 / 待补全', value: ninetyoneStats.pendingCount, unit: '待补全订单', meta: `当前 ${ninetyoneStats.currentCount} 条 · 已生成任务 ${ninetyoneStats.taskReadyCount} 条` },
{ key: 'kuaishouEticket', label: '快手小店核销', tag: 'Cookie / 券核销', value: kuaishouEticketStats.configuredShopCount, unit: '已配置店铺', meta: `Cookie 就绪 ${kuaishouEticketStats.cookieReadyCount} 家 · 当前店铺 ${kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全'}` },
...(isDevMode ? [{ key: 'cloudtentacles', label: 'cloudtentacles', tag: '履约 / 登录', value: cloudtentaclesStats.hasCredential ? '已配置' : '未配置', unit: '履约凭据', meta: `短信 ${cloudtentaclesStats.smsSent ? '已发送' : '未发送'} · 会话 ${cloudtentaclesStats.validated ? '已验证' : '未验证'}` }] : []),
]"
:key="card.key"
:class="['overview-card', { 'is-active': activePlatform === card.key }]"
:body-style="{ padding: '18px', cursor: 'pointer' }"
shadow="hover"
@click="switchTab(card.key)"
>
<div class="platform-head">
<span class="platform-badge">Agiso</span>
<span class="platform-tag">Webhook / 店铺</span>
<div class="card-head">
<el-tag size="small" type="primary">{{ card.label }}</el-tag>
<el-tag size="small">{{ card.tag }}</el-tag>
</div>
<strong>{{ agisoStats.configuredShopCount }}</strong>
<span>已配置店铺</span>
<div class="platform-metrics">
<span>识别到 {{ agisoStats.observedShopCount }} </span>
<span>待补 {{ agisoStats.pendingObservedCount }} </span>
</div>
</button>
<div class="card-value">{{ card.value }}</div>
<div class="card-unit">{{ card.unit }}</div>
<div class="card-meta">{{ card.meta }}</div>
</el-card>
</div>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'notifications' }]"
@click="switchPlatform('notifications')"
>
<div class="platform-head">
<span class="platform-badge">内部通知</span>
<span class="platform-tag">Bark / 值班</span>
</div>
<strong>{{ notificationStats.enabledRecipientCount }}</strong>
<span>启用接收人</span>
<div class="platform-metrics">
<span>配置 {{ notificationStats.configuredRecipientCount }} </span>
<span>最近成功 {{ notificationStats.lastSuccessCount }} </span>
</div>
</button>
<!-- 平台切换 Tab -->
<el-tabs v-model="activePlatform" type="card" class="platform-tabs">
<el-tab-pane label="Agiso 店铺与消息" name="agiso">
<AdminPlatformAgisoSection
:agiso-file-path="agisoFilePath"
:defaults="defaults"
:shops="shops"
:observed-shops="observedShops"
:saving="saving"
:is-shop-expanded="isShopExpanded"
:toggle-shop="toggleShop"
:expand-all-shops="expandAllShops"
:collapse-all-shops="collapseAllShops"
:describe-shop="describeShop"
:remove-shop="removeShop"
:import-observed-shop="importObservedShop"
:save-agiso-configs="saveAgisoConfigs"
/>
</el-tab-pane>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'ninetyone' }]"
@click="switchPlatform('ninetyone')"
>
<div class="platform-head">
<span class="platform-badge">91卡券</span>
<span class="platform-tag">接入 / 待补全</span>
</div>
<strong>{{ ninetyoneStats.pendingCount }}</strong>
<span>待补全订单</span>
<div class="platform-metrics">
<span>当前 {{ ninetyoneStats.currentCount }} </span>
<span>已生成任务 {{ ninetyoneStats.taskReadyCount }} </span>
</div>
</button>
<el-tab-pane label="91卡券接入" name="ninetyone">
<AdminPlatformNinetyoneSection
:ninetyone-loading="ninetyoneLoading"
:ninetyone-action-loading-id="ninetyoneActionLoadingId"
:ninetyone-result-error="ninetyoneResultError"
:ninetyone-status="ninetyoneStatus"
:ninetyone-orders="ninetyoneOrders"
:ninetyone-stats="ninetyoneStats"
:load-ninetyone-orders="loadNinetyoneOrders"
:handle-ninetyone-status-change="handleNinetyoneStatusChange"
:handle-ninetyone-retry-order="handleNinetyoneRetryOrder"
:handle-ninetyone-fail-order="handleNinetyoneFailOrder"
/>
</el-tab-pane>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'kuaishouEticket' }]"
@click="switchPlatform('kuaishouEticket')"
>
<div class="platform-head">
<span class="platform-badge">快手小店核销</span>
<span class="platform-tag">Cookie / 券核销</span>
</div>
<strong>{{ kuaishouEticketStats.configuredShopCount }}</strong>
<span>已配置店铺</span>
<div class="platform-metrics">
<span>Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} </span>
<span>当前店铺 {{ kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全' }}</span>
</div>
</button>
<el-tab-pane label="内部通知" name="notifications">
<AdminPlatformNotificationSection
:notification-file-path="notificationFilePath"
:notification-form="notificationForm"
:notification-recipients="notificationRecipients"
:notification-saving="notificationSaving"
:notification-testing="notificationTesting"
:notification-result-error="notificationResultError"
:notification-test-result="notificationTestResult"
:scheduled-jobs-file-path="scheduledJobsFilePath"
:scheduled-jobs-form="scheduledJobsForm"
:scheduled-jobs="scheduledJobs"
:scheduled-job-runtime="scheduledJobRuntime"
:scheduled-jobs-saving="scheduledJobsSaving"
:scheduled-job-running-id="scheduledJobRunningId"
:notification-stats="notificationStats"
:add-notification-recipient="addNotificationRecipient"
:remove-notification-recipient="removeNotificationRecipient"
:handle-notification-save-config="handleNotificationSaveConfig"
:handle-notification-test="handleNotificationTest"
:handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
:handle-scheduled-job-run-now="handleScheduledJobRunNow"
:get-scheduled-job-runtime="getScheduledJobRuntime"
/>
</el-tab-pane>
<button
type="button"
:class="['platform-overview-card', { 'is-active': activePlatform === 'cloudtentacles' }]"
@click="switchPlatform('cloudtentacles')"
<el-tab-pane label="快手小店核销" name="kuaishouEticket">
<AdminPlatformKuaishouEticketSection
:kuaishou-eticket-file-path="kuaishouEticketFilePath"
:kuaishou-eticket-shops="kuaishouEticketShops"
:kuaishou-eticket-form="kuaishouEticketForm"
:kuaishou-eticket-saving="kuaishouEticketSaving"
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
:kuaishou-eticket-result-error="kuaishouEticketResultError"
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
:kuaishou-eticket-stats="kuaishouEticketStats"
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
:is-kuaishou-eticket-shop-expanded="isKuaishouEticketShopExpanded"
:toggle-kuaishou-eticket-shop="toggleKuaishouEticketShop"
:expand-all-kuaishou-eticket-shops="expandAllKuaishouEticketShops"
:collapse-all-kuaishou-eticket-shops="collapseAllKuaishouEticketShops"
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
/>
</el-tab-pane>
<el-tab-pane
v-if="isDevMode"
label="cloudtentacles 履约平台"
name="cloudtentacles"
>
<div class="platform-head">
<span class="platform-badge">cloudtentacles</span>
<span class="platform-tag">履约 / 登录</span>
</div>
<strong>{{ cloudtentaclesStats.hasCredential ? '已配置' : '未配置' }}</strong>
<span>履约凭据</span>
<div class="platform-metrics">
<span>短信 {{ cloudtentaclesStats.smsSent ? '已发送' : '未发送' }}</span>
<span>会话 {{ cloudtentaclesStats.validated ? '已验证' : '未验证' }}</span>
</div>
</button>
</section>
<section class="platform-switcher">
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'agiso' }]"
@click="switchPlatform('agiso')"
>
Agiso 店铺与消息
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'ninetyone' }]"
@click="switchPlatform('ninetyone')"
>
91卡券接入
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'notifications' }]"
@click="switchPlatform('notifications')"
>
内部通知
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'kuaishouEticket' }]"
@click="switchPlatform('kuaishouEticket')"
>
快手小店核销
</button>
<button
type="button"
:class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]"
@click="switchPlatform('cloudtentacles')"
v-if="isDevMode"
>
cloudtentacles 履约平台
</button>
</section>
<AdminPlatformAgisoSection
v-if="activePlatform === 'agiso'"
:agiso-file-path="agisoFilePath"
:defaults="defaults"
:shops="shops"
:observed-shops="observedShops"
:saving="saving"
:is-shop-expanded="isShopExpanded"
:toggle-shop="toggleShop"
:expand-all-shops="expandAllShops"
:collapse-all-shops="collapseAllShops"
:describe-shop="describeShop"
:remove-shop="removeShop"
:import-observed-shop="importObservedShop"
:save-agiso-configs="saveAgisoConfigs"
/>
<AdminPlatformNinetyoneSection
v-else-if="activePlatform === 'ninetyone'"
:ninetyone-loading="ninetyoneLoading"
:ninetyone-action-loading-id="ninetyoneActionLoadingId"
:ninetyone-result-error="ninetyoneResultError"
:ninetyone-status="ninetyoneStatus"
:ninetyone-orders="ninetyoneOrders"
:ninetyone-stats="ninetyoneStats"
:load-ninetyone-orders="loadNinetyoneOrders"
:handle-ninetyone-status-change="handleNinetyoneStatusChange"
:handle-ninetyone-retry-order="handleNinetyoneRetryOrder"
:handle-ninetyone-fail-order="handleNinetyoneFailOrder"
/>
<AdminPlatformNotificationSection
v-else-if="activePlatform === 'notifications'"
:notification-file-path="notificationFilePath"
:notification-form="notificationForm"
:notification-recipients="notificationRecipients"
:notification-saving="notificationSaving"
:notification-testing="notificationTesting"
:notification-result-error="notificationResultError"
:notification-test-result="notificationTestResult"
:scheduled-jobs-file-path="scheduledJobsFilePath"
:scheduled-jobs-form="scheduledJobsForm"
:scheduled-jobs="scheduledJobs"
:scheduled-job-runtime="scheduledJobRuntime"
:scheduled-jobs-saving="scheduledJobsSaving"
:scheduled-job-running-id="scheduledJobRunningId"
:notification-stats="notificationStats"
:add-notification-recipient="addNotificationRecipient"
:remove-notification-recipient="removeNotificationRecipient"
:handle-notification-save-config="handleNotificationSaveConfig"
:handle-notification-test="handleNotificationTest"
:handle-scheduled-jobs-save-config="handleScheduledJobsSaveConfig"
:handle-scheduled-job-run-now="handleScheduledJobRunNow"
:get-scheduled-job-runtime="getScheduledJobRuntime"
/>
<AdminPlatformKuaishouEticketSection
v-else-if="activePlatform === 'kuaishouEticket'"
:kuaishou-eticket-file-path="kuaishouEticketFilePath"
:kuaishou-eticket-shops="kuaishouEticketShops"
:kuaishou-eticket-form="kuaishouEticketForm"
:kuaishou-eticket-saving="kuaishouEticketSaving"
:kuaishou-eticket-resolving-shop-id="kuaishouEticketResolvingShopId"
:kuaishou-eticket-detailing="kuaishouEticketDetailing"
:kuaishou-eticket-consuming="kuaishouEticketConsuming"
:kuaishou-eticket-result-error="kuaishouEticketResultError"
:kuaishou-eticket-shop-info-result="kuaishouEticketShopInfoResult"
:kuaishou-eticket-detail-result="kuaishouEticketDetailResult"
:kuaishou-eticket-consume-result="kuaishouEticketConsumeResult"
:active-kuaishou-eticket-shop="activeKuaishouEticketShop"
:kuaishou-eticket-stats="kuaishouEticketStats"
:add-kuaishou-eticket-shop="addKuaishouEticketShop"
:remove-kuaishou-eticket-shop="removeKuaishouEticketShop"
:is-kuaishou-eticket-shop-expanded="isKuaishouEticketShopExpanded"
:toggle-kuaishou-eticket-shop="toggleKuaishouEticketShop"
:expand-all-kuaishou-eticket-shops="expandAllKuaishouEticketShops"
:collapse-all-kuaishou-eticket-shops="collapseAllKuaishouEticketShops"
:set-kuaishou-eticket-debug-shop="setKuaishouEticketDebugShop"
:describe-kuaishou-eticket-shop="describeKuaishouEticketShop"
:get-kuaishou-eticket-shop-initial="getKuaishouEticketShopInitial"
:handle-kuaishou-eticket-resolve-shop-info="handleKuaishouEticketResolveShopInfo"
:handle-kuaishou-eticket-save-source="handleKuaishouEticketSaveSource"
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
/>
<AdminPlatformCloudtentaclesSection
v-else-if="isDevMode"
:cloudtentacles-file-path="cloudtentaclesFilePath"
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
:cloudtentacles-form="cloudtentaclesForm"
:cloudtentacles-saving="cloudtentaclesSaving"
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
:cloudtentacles-testing="cloudtentaclesTesting"
:cloudtentacles-validating="cloudtentaclesValidating"
:cloudtentacles-result-error="cloudtentaclesResultError"
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
:cloudtentacles-login-result="cloudtentaclesLoginResult"
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
:cloudtentacles-stats="cloudtentaclesStats"
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
/>
<AdminPlatformCloudtentaclesSection
:cloudtentacles-file-path="cloudtentaclesFilePath"
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
:cloudtentacles-form="cloudtentaclesForm"
:cloudtentacles-saving="cloudtentaclesSaving"
:cloudtentacles-sending-sms="cloudtentaclesSendingSms"
:cloudtentacles-testing="cloudtentaclesTesting"
:cloudtentacles-validating="cloudtentaclesValidating"
:cloudtentacles-result-error="cloudtentaclesResultError"
:cloudtentacles-sms-result="cloudtentaclesSmsResult"
:cloudtentacles-login-result="cloudtentaclesLoginResult"
:cloudtentacles-validate-result="cloudtentaclesValidateResult"
:cloudtentacles-debug-loading="cloudtentaclesDebugLoading"
:cloudtentacles-debug-result="cloudtentaclesDebugResult"
:cloudtentacles-persisted-session="cloudtentaclesPersistedSession"
:cloudtentacles-stats="cloudtentaclesStats"
:handle-cloudtentacles-save-source="handleCloudtentaclesSaveSource"
:handle-cloudtentacles-send-sms-code="handleCloudtentaclesSendSmsCode"
:handle-cloudtentacles-test-login="handleCloudtentaclesTestLogin"
:handle-cloudtentacles-validate-session="handleCloudtentaclesValidateSession"
:handle-cloudtentacles-fetch-asset="handleCloudtentaclesFetchAsset"
:handle-cloudtentacles-fetch-categories="handleCloudtentaclesFetchCategories"
:handle-cloudtentacles-fetch-sku-list="handleCloudtentaclesFetchSkuList"
:handle-cloudtentacles-buy-sku="handleCloudtentaclesBuySku"
:handle-cloudtentacles-use-sku="handleCloudtentaclesUseSku"
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
:handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
/>
</el-tab-pane>
</el-tabs>
</template>
</template>
</section>
</div>
</template>
<style scoped>
.platform-page {
display: grid;
gap: var(--space-4);
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: var(--space-3);
}
.page-header h1 {
margin: 0;
font-size: var(--text-3xl);
color: var(--text-primary);
}
.page-header p {
margin: var(--space-2) 0 0;
color: var(--text-secondary);
}
.header-actions {
display: flex;
gap: var(--space-2);
flex-shrink: 0;
}
/* 概览卡片网格 */
.overview-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: var(--space-4);
}
.overview-card {
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.overview-card.is-active {
border-color: var(--border-hover);
box-shadow: var(--shadow-lg);
}
.card-head {
display: flex;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.card-value {
font-size: 26px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.1;
}
.card-unit {
color: var(--text-muted);
font-size: var(--text-sm);
margin-top: var(--space-1);
}
.card-meta {
color: var(--text-secondary);
font-size: var(--text-xs);
margin-top: var(--space-2);
}
/* Tab 切换 */
.platform-tabs {
margin-top: var(--space-2);
}
@media (max-width: 900px) {
.page-header {
flex-direction: column;
}
.overview-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.overview-grid {
grid-template-columns: 1fr;
}
}
</style>