Files
hfb_sys/frontend/src/layouts/AppLayout.vue
T

397 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import {
ArrowDown,
Bell,
ChatDotRound,
CirclePlus,
Coin,
DocumentChecked,
EditPen,
Finished,
House,
Search,
Service,
Shop,
Star,
Switch,
SwitchButton,
Tickets,
UserFilled,
Van,
VideoPlay,
Wallet,
} from '@element-plus/icons-vue'
import { ensureSupportChat, resolveSupportScene } from '@/features/chats/api/chats'
import { useChatUnreadCount } from '@/features/chats/composables/useChatUnreadCount'
import { useDistributionSiteStore } from '@/stores/distributionSite'
import { useSessionStore } from '@/stores/session'
const session = useSessionStore()
const siteStore = useDistributionSiteStore()
const router = useRouter()
const route = useRoute()
const { unreadCount, unreadLabel } = useChatUnreadCount(route)
const navItems = [
{ label: '首页', to: '/', icon: House },
{ label: '我的订单', to: '/orders', icon: Tickets },
{ label: '消息', to: '/messages', icon: ChatDotRound },
{ label: '钱包', to: '/wallet', icon: Wallet },
{ label: '公告', to: '/announcements', icon: Bell },
]
const buyerServiceItems = [
{ label: '待支付', icon: Coin, to: { path: '/orders', query: { tab: 'pending_payment' } } },
{ label: '待交接', icon: Van, to: { path: '/orders', query: { tab: 'pending_handoff' } } },
{ label: '使用中', icon: VideoPlay, to: { path: '/orders', query: { tab: 'renting' } } },
{ label: '已完成', icon: Finished, to: { path: '/orders', query: { tab: 'completed' } } },
]
const sellerServiceItems = computed(() => {
const items = [
{ label: '发布商品', icon: CirclePlus, to: '/seller/listings/create' },
{ label: '我的商品', icon: Shop, to: '/seller/listings' },
]
if (!siteStore.isDistribution) {
items.push({ label: '提号记录', icon: Switch, to: '/seller/pickups' })
}
items.push({ label: '提现/账单', icon: Wallet, to: '/wallet' })
return items
})
const showUserDropdown = ref(false)
const supportLoading = ref(false)
const topSearchKeyword = ref('')
const realnameBadge = computed(() => {
switch (session.realnameStatus) {
case 'verified':
return { text: '已实名', tone: 'verified' }
case 'pending':
return { text: '认证中', tone: 'pending' }
case 'rejected':
return { text: '认证失败', tone: 'rejected' }
default:
return { text: '未实名', tone: 'unverified' }
}
})
const growthBadgeTone = computed(() => {
switch (session.renterGrowthLevel) {
case 'peak':
return 'peak'
case 'diamond':
return 'diamond'
case 'platinum':
return 'platinum'
default:
return 'normal'
}
})
watch(
() => [route.path, route.query.keyword],
() => {
if (route.path !== '/') return
topSearchKeyword.value = normalizeRouteKeyword(route.query.keyword)
},
{ immediate: true }
)
function normalizeRouteKeyword(value: unknown) {
if (Array.isArray(value)) return String(value[0] || '')
return typeof value === 'string' ? value : ''
}
function handleTopSearchInput(event: Event) {
const value = (event.target as HTMLInputElement).value
topSearchKeyword.value = value
const keyword = value.trim()
router.replace({
path: '/',
query: {
...route.query,
keyword: keyword || undefined,
},
})
}
async function handleLogout() {
await session.logout()
showUserDropdown.value = false
router.replace('/')
}
async function handleSupportClick() {
if (!session.isLoggedIn) {
router.push({ path: '/login', query: { redirect: router.currentRoute.value.fullPath } })
return
}
if (supportLoading.value) return
supportLoading.value = true
try {
const scene = resolveSupportScene(route.path)
const chat = await ensureSupportChat(scene)
router.push(`/messages/${chat.id}`)
} catch {
ElMessage.error('联系客服失败,请稍后重试')
} finally {
supportLoading.value = false
}
}
</script>
<template>
<div class="pc-app-shell">
<header class="pc-topbar">
<RouterLink class="pc-brand" to="/">
<span class="pc-brand-mark">{{
siteStore.isDistribution ? siteStore.siteName.slice(0, 1) : '锤'
}}</span>
<div class="pc-brand-info">
<div class="pc-brand-title-row">
<span class="pc-brand-text">{{ siteStore.siteName }}</span>
<span v-if="siteStore.isDistribution" class="pc-brand-badge">官方分销</span>
</div>
<small class="pc-brand-sub">{{
siteStore.isDistribution ? '官方分销·平台保障' : '哈夫币租号'
}}</small>
</div>
</RouterLink>
<nav class="pc-nav">
<RouterLink v-for="item in navItems" :key="item.to" class="pc-nav-link" :to="item.to">
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
<em v-if="item.to === '/messages' && unreadCount > 0" class="pc-nav-badge">{{
unreadLabel
}}</em>
</RouterLink>
</nav>
<div class="pc-search">
<el-icon><Search /></el-icon>
<input
:value="topSearchKeyword"
placeholder="搜索编号 / 区服 / 段位 / 哈夫币数量 / 皮肤"
@input="handleTopSearchInput"
/>
</div>
<div class="pc-user-actions">
<button
class="pc-icon-link service-link"
type="button"
title="联系客服"
@click="handleSupportClick"
>
<el-icon><Service /></el-icon>
<span>{{ supportLoading ? '接入中' : '客服' }}</span>
</button>
<RouterLink
class="pc-post-link"
:to="session.isLoggedIn ? '/seller/listings/create' : '/login'"
>
<el-icon><CirclePlus /></el-icon>
<span>发布账号</span>
</RouterLink>
<!-- 未登录:显示登录/注册按钮 -->
<template v-if="!session.isLoggedIn">
<RouterLink class="pc-login-link" to="/login">
<el-icon><UserFilled /></el-icon>
<span>登录/注册</span>
</RouterLink>
</template>
<!-- 已登录:显示用户头像+昵称 + 下拉菜单 -->
<template v-else>
<div class="pc-user-block" @mouseleave="showUserDropdown = false">
<button
class="pc-user-trigger"
@mouseenter="showUserDropdown = true"
@click="showUserDropdown = !showUserDropdown"
:class="{ active: showUserDropdown }"
>
<span class="pc-avatar-chip">
<img
v-if="session.avatarUrl"
:src="session.avatarUrl"
alt=""
class="pc-avatar-img"
/>
<span v-else class="pc-avatar-text">{{ session.avatarText }}</span>
</span>
<span class="pc-user-name">{{ session.displayName }}</span>
<span
v-if="siteStore.isDistribution"
class="pc-growth-badge is-distribution"
>
分销专享
</span>
<span
v-else
class="pc-growth-badge"
:class="`is-${growthBadgeTone}`"
>
{{ session.renterGrowthLevelName }}
</span>
<span class="pc-realname-badge" :class="`is-${realnameBadge.tone}`">
{{ realnameBadge.text }}
</span>
<el-icon class="arrow-icon" :class="{ rotated: showUserDropdown }"
><ArrowDown
/></el-icon>
</button>
<Transition name="dropdown">
<div v-if="showUserDropdown" class="pc-user-dropdown-container">
<div class="pc-user-dropdown">
<div v-if="siteStore.isDistribution" class="dropdown-growth-panel is-distribution">
<div>
<span>所属商城</span>
<strong>{{ siteStore.siteName || '专属分销站' }}</strong>
</div>
<div>
<span>会员权益</span>
<strong>渠道特惠直租</strong>
</div>
</div>
<div v-else class="dropdown-growth-panel">
<div>
<span>租客成长等级</span>
<strong>{{ session.renterGrowthLevelName }}</strong>
</div>
<div>
<span>当前积分</span>
<strong>{{ session.renterGrowthPoints }}</strong>
</div>
</div>
<div class="dropdown-basic-grid">
<RouterLink
class="dropdown-basic-item"
to="/profile"
@click="showUserDropdown = false"
>
<el-icon><EditPen /></el-icon>
<span>个人资料</span>
</RouterLink>
<RouterLink
class="dropdown-basic-item"
to="/orders"
@click="showUserDropdown = false"
>
<el-icon><Tickets /></el-icon>
<span>我的订单</span>
</RouterLink>
<RouterLink
class="dropdown-basic-item"
to="/wallet"
@click="showUserDropdown = false"
>
<el-icon><Wallet /></el-icon>
<span>我的钱包</span>
</RouterLink>
<RouterLink
class="dropdown-basic-item"
to="/listings/collections"
@click="showUserDropdown = false"
>
<el-icon><Star /></el-icon>
<span>我的足迹</span>
</RouterLink>
<RouterLink
class="dropdown-basic-item"
to="/realname"
@click="showUserDropdown = false"
>
<el-icon><DocumentChecked /></el-icon>
<span>实名认证</span>
<span class="dropdown-status" :class="`is-${realnameBadge.tone}`">
{{ realnameBadge.text }}
</span>
</RouterLink>
</div>
<div class="dropdown-divider"></div>
<section class="dropdown-service-group">
<div class="dropdown-service-head">
<span>买家服务</span>
<RouterLink
class="dropdown-service-more"
:to="{ path: '/orders', query: { tab: 'all' } }"
@click="showUserDropdown = false"
>
全部订单
</RouterLink>
</div>
<div class="dropdown-service-grid">
<RouterLink
v-for="item in buyerServiceItems"
:key="item.label"
class="dropdown-service-card"
:to="item.to"
@click="showUserDropdown = false"
>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</RouterLink>
</div>
</section>
<section class="dropdown-service-group">
<div class="dropdown-service-head">
<span>卖家服务</span>
</div>
<div class="dropdown-seller-grid">
<RouterLink
v-for="item in sellerServiceItems"
:key="item.label"
class="dropdown-seller-card"
:to="item.to"
@click="showUserDropdown = false"
>
<el-icon><component :is="item.icon" /></el-icon>
<span>{{ item.label }}</span>
</RouterLink>
</div>
</section>
<div class="dropdown-divider"></div>
<button class="dropdown-item logout" @click="handleLogout">
<el-icon><SwitchButton /></el-icon>
<span>退出登录</span>
</button>
</div>
</div>
</Transition>
</div>
</template>
</div>
</header>
<main class="pc-main">
<slot />
</main>
<footer class="pc-footer">
<div class="pc-footer-inner">
<a
href="https://beian.miit.gov.cn/"
target="_blank"
rel="noopener noreferrer"
class="pc-footer-icp"
>湘ICP备2025146668号</a
>
<span class="pc-footer-divider">|</span>
<span class="pc-footer-copyright">版权所有 ©2026 {{ siteStore.siteName }}</span>
</div>
</footer>
</div>
</template>
<style scoped src="./AppLayout.css"></style>