优化平台配置导航布局

This commit is contained in:
yml2213
2026-05-26 09:39:59 +08:00
parent 5c7e145766
commit 8b1904d732
4 changed files with 313 additions and 221 deletions
+1
View File
@@ -45,6 +45,7 @@ declare module 'vue' {
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSpace: typeof import('element-plus/es')['ElSpace'] ElSpace: typeof import('element-plus/es')['ElSpace']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTable: typeof import('element-plus/es')['ElTable'] ElTable: typeof import('element-plus/es')['ElTable']
ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
@@ -2,6 +2,8 @@
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { import {
Bell,
Connection,
Monitor, Monitor,
DataAnalysis, DataAnalysis,
Document, Document,
@@ -27,6 +29,7 @@ import { formatAdminDateTime } from '@/utils/admin-time'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const sidebarStorageKey = 'admin-sidebar-collapsed' const sidebarStorageKey = 'admin-sidebar-collapsed'
const isDevMode = import.meta.env.DEV
const isSidebarCollapsed = ref( const isSidebarCollapsed = ref(
typeof window !== 'undefined' && window.localStorage.getItem(sidebarStorageKey) === '1', typeof window !== 'undefined' && window.localStorage.getItem(sidebarStorageKey) === '1',
) )
@@ -40,8 +43,33 @@ const roleLabel = computed(() => {
return '普通运营' return '普通运营'
}) })
type AdminNavItem = {
to: string
label: string
icon: typeof DataAnalysis
children?: AdminNavItem[]
}
const platformConfigNavItems = computed<AdminNavItem[]>(() => {
const items: AdminNavItem[] = [
{ to: '/admin/platform-shops?tab=ninetyone', label: '91卡券', icon: Connection },
{ to: '/admin/platform-shops?tab=notifications', label: '内部通知', icon: Bell },
{ to: '/admin/platform-shops?tab=kuaishouEticket', label: '快手小店核销', icon: Tickets },
]
if (isDevMode) {
items.push({
to: '/admin/platform-shops?tab=cloudtentacles',
label: 'cloudtentacles',
icon: Setting,
})
}
return items
})
const navItems = computed(() => { const navItems = computed(() => {
const items: { to: string; label: string; icon: typeof DataAnalysis }[] = [ const items: AdminNavItem[] = [
{ to: '/admin/dashboard', label: '概览', icon: DataAnalysis }, { to: '/admin/dashboard', label: '概览', icon: DataAnalysis },
] ]
@@ -56,7 +84,12 @@ const navItems = computed(() => {
if (isAdmin.value) { if (isAdmin.value) {
items.push( items.push(
{ to: '/admin/platform-shops', label: '平台配置', icon: Setting }, {
to: '/admin/platform-shops',
label: '平台配置',
icon: Setting,
children: platformConfigNavItems.value,
},
{ to: '/admin/platform-fulfillment', label: '履约配置', icon: Tickets }, { to: '/admin/platform-fulfillment', label: '履约配置', icon: Tickets },
{ to: '/admin/audit-logs', label: '审计', icon: Monitor }, { to: '/admin/audit-logs', label: '审计', icon: Monitor },
) )
@@ -65,6 +98,15 @@ const navItems = computed(() => {
return items return items
}) })
const activeNavIndex = computed(() => {
if (route.path === '/admin/platform-shops') {
const tab = typeof route.query.tab === 'string' ? route.query.tab : 'ninetyone'
return `/admin/platform-shops?tab=${tab}`
}
return route.path
})
function toggleSidebarCollapse() { function toggleSidebarCollapse() {
isSidebarCollapsed.value = !isSidebarCollapsed.value isSidebarCollapsed.value = !isSidebarCollapsed.value
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -112,17 +154,37 @@ async function submitLogout() {
</div> </div>
<el-menu <el-menu
:default-active="route.path" :default-active="activeNavIndex"
:default-openeds="['/admin/platform-shops']"
router router
class="sidebar-menu" class="sidebar-menu"
:collapse="isSidebarCollapsed" :collapse="isSidebarCollapsed"
:collapse-transition="false" :collapse-transition="false"
:ellipsis="false" :ellipsis="false"
> >
<el-menu-item v-for="item in navItems" :key="item.to" :index="item.to" :title="item.label"> <template v-for="item in navItems" :key="item.to">
<el-sub-menu v-if="item.children?.length" :index="item.to">
<template #title>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</template>
<el-menu-item
v-for="child in item.children"
:key="child.to"
:index="child.to"
:title="child.label"
>
<el-icon><component :is="child.icon" /></el-icon>
<span>{{ child.label }}</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.to" :title="item.label">
<el-icon><component :is="item.icon" /></el-icon> <el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
</el-menu-item> </el-menu-item>
</template>
</el-menu> </el-menu>
<el-tooltip :disabled="!isSidebarCollapsed" content="退出登录" placement="right"> <el-tooltip :disabled="!isSidebarCollapsed" content="退出登录" placement="right">
@@ -235,18 +297,33 @@ async function submitLogout() {
width: 100%; width: 100%;
} }
.sidebar-menu :deep(.el-menu-item) { .sidebar-menu :deep(.el-menu-item),
.sidebar-menu :deep(.el-sub-menu__title) {
margin: 2px var(--space-2); margin: 2px var(--space-2);
border-radius: var(--radius-md); border-radius: var(--radius-md);
min-height: 44px; min-height: 44px;
line-height: 44px; line-height: 44px;
} }
.sidebar-menu :deep(.el-menu-item.is-active) { .sidebar-menu :deep(.el-menu-item.is-active),
.sidebar-menu :deep(.el-sub-menu.is-active > .el-sub-menu__title) {
color: var(--color-primary); color: var(--color-primary);
background: var(--color-primary-light); background: var(--color-primary-light);
} }
.sidebar-menu :deep(.el-sub-menu .el-menu-item) {
margin-left: var(--space-4);
min-height: 38px;
line-height: 38px;
color: var(--text-secondary);
font-size: var(--text-sm);
}
.sidebar-menu :deep(.el-sub-menu .el-menu-item.is-active) {
color: var(--color-primary);
font-weight: 700;
}
.logout-btn { .logout-btn {
margin: var(--space-3) var(--space-4) 0; margin: var(--space-3) var(--space-4) 0;
flex-shrink: 0; flex-shrink: 0;
@@ -263,13 +340,15 @@ async function submitLogout() {
margin-bottom: var(--space-5); margin-bottom: var(--space-5);
} }
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item) { .is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item),
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title) {
justify-content: center; justify-content: center;
margin: 4px 10px; margin: 4px 10px;
padding: 0; padding: 0;
} }
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item .el-icon) { .is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item .el-icon),
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title .el-icon) {
margin-right: 0; margin-right: 0;
} }
@@ -313,7 +392,8 @@ async function submitLogout() {
overflow: visible; overflow: visible;
} }
.is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item) { .is-sidebar-collapsed .sidebar-menu :deep(.el-menu-item),
.is-sidebar-collapsed .sidebar-menu :deep(.el-sub-menu__title) {
justify-content: flex-start; justify-content: flex-start;
margin: 2px var(--space-2); margin: 2px var(--space-2);
padding: 0 var(--space-5); padding: 0 var(--space-5);
@@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { RefreshRight } from '@element-plus/icons-vue' import { RefreshRight } from '@element-plus/icons-vue'
import AdminPageHeader from '@/components/admin/AdminPageHeader.vue' import AdminPageHeader from '@/components/admin/AdminPageHeader.vue'
@@ -23,7 +24,21 @@ import type { PlatformTab } from './composables/types'
const loading = ref(true) const loading = ref(true)
const isDevMode = import.meta.env.DEV const isDevMode = import.meta.env.DEV
const errorMessage = ref('') const errorMessage = ref('')
const activePlatform = ref<PlatformTab>('ninetyone') const route = useRoute()
const platformTabs: PlatformTab[] = ['ninetyone', 'notifications', 'kuaishouEticket']
if (isDevMode) {
platformTabs.push('cloudtentacles')
}
function normalizePlatformTab(value: unknown): PlatformTab {
return typeof value === 'string' && platformTabs.includes(value as PlatformTab)
? (value as PlatformTab)
: 'ninetyone'
}
const activePlatform = computed(() => normalizePlatformTab(route.query.tab))
const { const {
ninetyoneLoading, ninetyoneLoading,
@@ -147,42 +162,46 @@ const overviewCards = computed(() => {
{ {
key: 'notifications', key: 'notifications',
label: '内部通知', label: '内部通知',
tag: 'Bark / WPush', tag: '通知与监控',
value: value:
notificationStats.value.barkEnabledRecipientCount + notificationStats.value.barkEnabledRecipientCount +
notificationStats.value.wpushEnabledRecipientCount, notificationStats.value.wpushEnabledRecipientCount,
unit: '启用接收人', unit: '启用接收人',
meta: `Bark ${notificationStats.value.barkConfiguredRecipientCount} 人 · WPush ${notificationStats.value.wpushConfiguredRecipientCount} 人 · 最近成功 ${notificationStats.value.lastSuccessCount}`, meta: `Bark ${notificationStats.value.barkConfiguredRecipientCount} 人 · WPush ${notificationStats.value.wpushConfiguredRecipientCount} 人 · 最近成功 ${notificationStats.value.lastSuccessCount}`,
status: notificationStats.value.ready ? '已就绪' : '待配置',
}, },
{ {
key: 'ninetyone', key: 'ninetyone',
label: '91卡券', label: '91卡券',
tag: '接入 / 待补全', tag: '来源订单',
value: ninetyoneStats.value.pendingCount, value: ninetyoneStats.value.pendingCount,
unit: '待补全订单', unit: '待补全订单',
meta: `当前 ${ninetyoneStats.value.currentCount} 条 · 已生成任务 ${ninetyoneStats.value.taskReadyCount}`, meta: `当前 ${ninetyoneStats.value.currentCount} 条 · 已生成任务 ${ninetyoneStats.value.taskReadyCount}`,
status: ninetyoneStats.value.pendingCount > 0 ? '待处理' : '正常',
}, },
{ {
key: 'kuaishouEticket', key: 'kuaishouEticket',
label: '快手小店核销', label: '快手小店核销',
tag: 'Cookie / 券核销', tag: '店铺凭据',
value: kuaishouEticketStats.value.configuredShopCount, value: kuaishouEticketStats.value.configuredShopCount,
unit: '已配置店铺', unit: '已配置店铺',
meta: `Cookie 就绪 ${kuaishouEticketStats.value.cookieReadyCount} 家 · 当前店铺 ${ meta: `Cookie 就绪 ${kuaishouEticketStats.value.cookieReadyCount} 家 · 当前店铺 ${
kuaishouEticketStats.value.selectedShopReady ? '可调试' : '待补全' kuaishouEticketStats.value.selectedShopReady ? '可调试' : '待补全'
}`, }`,
status: kuaishouEticketStats.value.cookieReadyCount > 0 ? '已就绪' : '待配置',
}, },
...(isDevMode ...(isDevMode
? [ ? [
{ {
key: 'cloudtentacles', key: 'cloudtentacles',
label: 'cloudtentacles', label: 'cloudtentacles',
tag: '履约 / 登录', tag: '履约账号',
value: cloudtentaclesStats.value.hasCredential ? '已配置' : '未配置', value: cloudtentaclesStats.value.hasCredential ? '已配置' : '未配置',
unit: '履约凭据', unit: '履约凭据',
meta: `短信 ${cloudtentaclesStats.value.smsSent ? '已发送' : '未发送'} · 会话 ${ meta: `短信 ${cloudtentaclesStats.value.smsSent ? '已发送' : '未发送'} · 会话 ${
cloudtentaclesStats.value.validated ? '已验证' : '未验证' cloudtentaclesStats.value.validated ? '已验证' : '未验证'
}`, }`,
status: cloudtentaclesStats.value.validated ? '已验证' : '待验证',
}, },
] ]
: []), : []),
@@ -191,6 +210,11 @@ const overviewCards = computed(() => {
return cards return cards
}) })
const activeOverview = computed(
() =>
overviewCards.value.find((card) => card.key === activePlatform.value) || overviewCards.value[0],
)
async function loadConfigs() { async function loadConfigs() {
if (!hasAdminRole('admin')) { if (!hasAdminRole('admin')) {
loading.value = false loading.value = false
@@ -225,10 +249,6 @@ async function loadConfigs() {
} }
} }
function switchTab(tab: string) {
activePlatform.value = tab as PlatformTab
}
onMounted(loadConfigs) onMounted(loadConfigs)
</script> </script>
@@ -267,29 +287,24 @@ onMounted(loadConfigs)
<el-skeleton v-if="loading" :rows="8" animated /> <el-skeleton v-if="loading" :rows="8" animated />
<template v-else> <template v-else>
<!-- 概览卡片 --> <main class="platform-detail">
<div class="overview-grid"> <section v-if="activeOverview" class="active-summary">
<el-card <div class="active-summary-copy">
v-for="card in overviewCards" <div class="summary-kicker">{{ activeOverview.tag }}</div>
:key="card.key" <h2>{{ activeOverview.label }}</h2>
:class="['overview-card', { 'is-active': activePlatform === card.key }]" <p>{{ activeOverview.meta }}</p>
shadow="hover"
@click="switchTab(card.key)"
>
<div class="card-head">
<el-tag size="small" type="primary">{{ card.label }}</el-tag>
<el-tag size="small">{{ card.tag }}</el-tag>
</div> </div>
<div class="card-value">{{ card.value }}</div> <div class="summary-metrics">
<div class="card-unit">{{ card.unit }}</div> <div class="summary-metric">
<div class="card-meta">{{ card.meta }}</div> <span>{{ activeOverview.unit }}</span>
</el-card> <strong>{{ activeOverview.value }}</strong>
</div> </div>
<el-tag size="large" effect="plain">{{ activeOverview.status }}</el-tag>
</div>
</section>
<!-- 平台切换 Tab -->
<el-tabs v-model="activePlatform" type="card" class="platform-tabs">
<el-tab-pane label="91卡券接入" name="ninetyone">
<AdminPlatformNinetyoneSection <AdminPlatformNinetyoneSection
v-if="activePlatform === 'ninetyone'"
:ninetyone-loading="ninetyoneLoading" :ninetyone-loading="ninetyoneLoading"
:ninetyone-action-loading-id="ninetyoneActionLoadingId" :ninetyone-action-loading-id="ninetyoneActionLoadingId"
:ninetyone-result-error="ninetyoneResultError" :ninetyone-result-error="ninetyoneResultError"
@@ -301,10 +316,9 @@ onMounted(loadConfigs)
:handle-ninetyone-retry-order="handleNinetyoneRetryOrder" :handle-ninetyone-retry-order="handleNinetyoneRetryOrder"
:handle-ninetyone-fail-order="handleNinetyoneFailOrder" :handle-ninetyone-fail-order="handleNinetyoneFailOrder"
/> />
</el-tab-pane>
<el-tab-pane label="内部通知" name="notifications">
<AdminPlatformNotificationSection <AdminPlatformNotificationSection
v-else-if="activePlatform === 'notifications'"
:notification-file-path="notificationFilePath" :notification-file-path="notificationFilePath"
:notification-form="notificationForm" :notification-form="notificationForm"
:notification-recipients="notificationRecipients" :notification-recipients="notificationRecipients"
@@ -329,10 +343,9 @@ onMounted(loadConfigs)
:handle-scheduled-job-run-now="handleScheduledJobRunNow" :handle-scheduled-job-run-now="handleScheduledJobRunNow"
:get-scheduled-job-runtime="getScheduledJobRuntime" :get-scheduled-job-runtime="getScheduledJobRuntime"
/> />
</el-tab-pane>
<el-tab-pane label="快手小店核销" name="kuaishouEticket">
<AdminPlatformKuaishouEticketSection <AdminPlatformKuaishouEticketSection
v-else-if="activePlatform === 'kuaishouEticket'"
:kuaishou-eticket-file-path="kuaishouEticketFilePath" :kuaishou-eticket-file-path="kuaishouEticketFilePath"
:kuaishou-eticket-shops="kuaishouEticketShops" :kuaishou-eticket-shops="kuaishouEticketShops"
:kuaishou-eticket-form="kuaishouEticketForm" :kuaishou-eticket-form="kuaishouEticketForm"
@@ -360,10 +373,9 @@ onMounted(loadConfigs)
:handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail" :handle-kuaishou-eticket-query-detail="handleKuaishouEticketQueryDetail"
:handle-kuaishou-eticket-consume="handleKuaishouEticketConsume" :handle-kuaishou-eticket-consume="handleKuaishouEticketConsume"
/> />
</el-tab-pane>
<el-tab-pane v-if="isDevMode" label="cloudtentacles 履约平台" name="cloudtentacles">
<AdminPlatformCloudtentaclesSection <AdminPlatformCloudtentaclesSection
v-else-if="isDevMode && activePlatform === 'cloudtentacles'"
:cloudtentacles-file-path="cloudtentaclesFilePath" :cloudtentacles-file-path="cloudtentaclesFilePath"
:cloudtentacles-session-file-path="cloudtentaclesSessionFilePath" :cloudtentacles-session-file-path="cloudtentaclesSessionFilePath"
:cloudtentacles-form="cloudtentaclesForm" :cloudtentacles-form="cloudtentaclesForm"
@@ -400,17 +412,14 @@ onMounted(loadConfigs)
:handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack" :handle-cloudtentacles-fetch-knapsack="handleCloudtentaclesFetchKnapsack"
:handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList" :handle-cloudtentacles-fetch-vn-list="handleCloudtentaclesFetchVnList"
:handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn" :handle-cloudtentacles-appoint-vn="handleCloudtentaclesAppointVn"
:handle-cloudtentacles-generate-vn-login-code=" :handle-cloudtentacles-generate-vn-login-code="handleCloudtentaclesGenerateVnLoginCode"
handleCloudtentaclesGenerateVnLoginCode
"
:handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode" :handle-cloudtentacles-fetch-vn-code="handleCloudtentaclesFetchVnCode"
:handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode" :handle-cloudtentacles-verify-vn-code="handleCloudtentaclesVerifyVnCode"
:handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl" :handle-cloudtentacles-fetch-bind-url="handleCloudtentaclesFetchBindUrl"
:handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn" :handle-cloudtentacles-back-vn="handleCloudtentaclesBackVn"
:handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow" :handle-cloudtentacles-run-full-flow="handleCloudtentaclesRunFullFlow"
/> />
</el-tab-pane> </main>
</el-tabs>
</template> </template>
</template> </template>
</div> </div>
@@ -422,68 +431,85 @@ onMounted(loadConfigs)
gap: var(--space-4); gap: var(--space-4);
} }
/* 概览卡片网格 */ .platform-detail {
.overview-grid { min-width: 0;
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: var(--space-4); gap: var(--space-4);
} }
.overview-card { .active-summary {
cursor: pointer;
transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
}
.overview-card :deep(.el-card__body) {
padding: 18px;
}
.overview-card.is-active {
border-color: var(--border-hover);
box-shadow: var(--shadow-lg);
}
.card-head {
display: flex; display: flex;
gap: var(--space-2); justify-content: space-between;
margin-bottom: var(--space-3); gap: var(--space-4);
align-items: center;
padding: var(--space-4) var(--space-5);
border: 1px solid var(--border-default);
border-radius: var(--radius-lg);
background: rgba(255, 255, 255, 0.92);
box-shadow: var(--shadow-sm);
} }
.card-value { .active-summary-copy {
font-size: 26px; min-width: 0;
font-weight: 700;
color: var(--text-primary);
line-height: 1.1;
} }
.card-unit { .summary-kicker {
color: var(--text-muted); color: #2563eb;
font-size: var(--text-sm);
margin-top: var(--space-1);
}
.card-meta {
color: var(--text-secondary);
font-size: var(--text-xs); font-size: var(--text-xs);
margin-top: var(--space-2); font-weight: 800;
} }
/* Tab 切换 */ .active-summary h2 {
.platform-tabs { margin: 4px 0 0;
margin-top: var(--space-2); color: var(--text-primary);
font-size: var(--text-xl);
line-height: 1.25;
} }
@media (max-width: 900px) { .active-summary p {
.overview-grid { margin: var(--space-2) 0 0;
grid-template-columns: repeat(2, 1fr); color: var(--text-secondary);
font-size: var(--text-sm);
line-height: 1.6;
}
.summary-metrics {
flex: 0 0 auto;
display: flex;
gap: var(--space-3);
align-items: center;
}
.summary-metric {
min-width: 112px;
display: grid;
gap: 4px;
padding: 10px 12px;
border: 1px solid rgba(148, 163, 184, 0.18);
border-radius: var(--radius-sm);
background: #f8fafc;
}
.summary-metric span {
color: var(--text-muted);
font-size: var(--text-xs);
font-weight: 700;
}
.summary-metric strong {
color: var(--text-primary);
font-size: var(--text-xl);
line-height: 1;
}
@media (max-width: 720px) {
.active-summary {
align-items: stretch;
flex-direction: column;
} }
}
@media (max-width: 640px) { .summary-metrics {
.overview-grid { justify-content: space-between;
grid-template-columns: 1fr;
} }
} }
</style> </style>
@@ -28,21 +28,6 @@ defineProps<Props>()
<template> <template>
<div class="platform-section"> <div class="platform-section">
<!-- 说明信息 -->
<el-card class="section-card">
<template #header>
<span class="card-title">说明信息</span>
</template>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="职责">
接收 91卡券推送订单未命中履约配置时先进入待补全队列补完商品规则后可手动重试生成任务
</el-descriptions-item>
<el-descriptions-item label="处理建议">
先到"快手 Cloud 新履约"页为 91 productNo 配置规则再回到这里点击重试
</el-descriptions-item>
</el-descriptions>
</el-card>
<!-- 订单队列 --> <!-- 订单队列 -->
<el-card class="section-card"> <el-card class="section-card">
<template #header> <template #header>