更新首页
This commit is contained in:
@@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div class="list-page">
|
||||
<!-- 页面头部 Banner -->
|
||||
<div class="page-banner" :style="{ background: config.bannerGradient }">
|
||||
<div class="mx-auto max-w-screen-xl px-4">
|
||||
<h1 class="banner-title">{{ config.title }}</h1>
|
||||
<p class="banner-desc">{{ config.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<PortalHeader v-if="config.useSiteHeader" />
|
||||
<!-- <div v-else class="page-banner" :style="{ background: config.bannerGradient }" />-->
|
||||
|
||||
<div class="mx-auto max-w-screen-xl px-4 py-8">
|
||||
<a-row :gutter="[32, 0]">
|
||||
@@ -84,13 +79,26 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue'
|
||||
import { pageSiteArticles, resolveSiteAssetUrl, type SiteArticle } from '@/api/site'
|
||||
|
||||
interface PageConfig {
|
||||
title: string
|
||||
desc: string
|
||||
bannerGradient: string
|
||||
categories: Array<{ type: string; label: string }>
|
||||
baseRoute: string
|
||||
baseRoute?: string
|
||||
useSiteHeader?: boolean
|
||||
}
|
||||
|
||||
interface ArticleItem {
|
||||
id: number
|
||||
title: string
|
||||
overview: string
|
||||
image: string
|
||||
source: string
|
||||
publishTime: string
|
||||
views: number
|
||||
type?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -104,7 +112,8 @@ const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const articles = ref<any[]>([])
|
||||
const articles = ref<ArticleItem[]>([])
|
||||
const keyword = computed(() => (route.query.keyword as string) || '')
|
||||
|
||||
const currentCategoryLabel = computed(() => {
|
||||
if (!activeType.value) return '全部文章'
|
||||
@@ -120,29 +129,44 @@ function getCategoryLabel(type: string) {
|
||||
function selectType(type: string) {
|
||||
activeType.value = type
|
||||
currentPage.value = 1
|
||||
router.replace({ query: type ? { type } : {} })
|
||||
router.replace({
|
||||
query: {
|
||||
...(keyword.value ? { keyword: keyword.value } : {}),
|
||||
...(type ? { type } : {}),
|
||||
}
|
||||
})
|
||||
loadArticles()
|
||||
}
|
||||
|
||||
function normalizeArticle(item: SiteArticle): ArticleItem {
|
||||
return {
|
||||
id: item.articleId || item.id,
|
||||
title: item.title,
|
||||
overview: item.overview || '暂无摘要',
|
||||
image: resolveSiteAssetUrl(item.image),
|
||||
source: item.source || '广西决策咨询网',
|
||||
publishTime: item.publishTime || item.createTime || '',
|
||||
views: Number(item.views || item.actualViews || 0),
|
||||
type: item.type,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadArticles() {
|
||||
loading.value = true
|
||||
try {
|
||||
// TODO: 接入实际API
|
||||
// const res = await listArticles({ category: props.config.baseRoute, type: activeType.value, page: currentPage.value })
|
||||
// Fallback mock data
|
||||
total.value = 35
|
||||
articles.value = Array.from({ length: Math.min(pageSize.value, 35 - (currentPage.value - 1) * pageSize.value) }, (_, i) => ({
|
||||
id: (currentPage.value - 1) * pageSize.value + i + 1,
|
||||
title: `${currentCategoryLabel.value}文章标题 ${(currentPage.value - 1) * pageSize.value + i + 1}:广西政策研究成果发布`,
|
||||
overview: '摘要内容:本文就广西经济社会发展中的若干重大问题进行深入研究,提出了切实可行的政策建议和对策措施,为相关决策提供参考依据...',
|
||||
image: `https://picsum.photos/200/130?random=${(currentPage.value - 1) * pageSize.value + i + 1}`,
|
||||
source: '广西决策咨询中心',
|
||||
publishTime: `2024-12-${String(20 - i).padStart(2, '0')}`,
|
||||
views: Math.floor(Math.random() * 2000) + 100,
|
||||
type: activeType.value || props.config.categories[i % props.config.categories.length]?.type,
|
||||
}))
|
||||
} catch (e: any) {
|
||||
message.error('加载失败')
|
||||
const data = await pageSiteArticles({
|
||||
page: currentPage.value,
|
||||
limit: pageSize.value,
|
||||
keywords: keyword.value || undefined,
|
||||
category: props.config.baseRoute || undefined,
|
||||
type: activeType.value || undefined,
|
||||
})
|
||||
total.value = data.count || 0
|
||||
articles.value = (data.list || []).map(normalizeArticle)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载失败')
|
||||
total.value = 0
|
||||
articles.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -154,7 +178,7 @@ function handlePageChange(page: number) {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function handleView(article: any) {
|
||||
function handleView(article: ArticleItem) {
|
||||
router.push(`/article/${article.id}`)
|
||||
}
|
||||
|
||||
@@ -164,6 +188,11 @@ watch(() => route.query.type, (newType) => {
|
||||
loadArticles()
|
||||
})
|
||||
|
||||
watch(() => route.query.keyword, () => {
|
||||
currentPage.value = 1
|
||||
loadArticles()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
loadArticles()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<aside class="expert-nav-sidebar">
|
||||
<div class="expert-nav-title">专家资讯</div>
|
||||
<NuxtLink
|
||||
v-for="item in expertNavItems"
|
||||
:key="item.to"
|
||||
:to="item.to"
|
||||
class="expert-nav-item"
|
||||
:class="{ active: isActive(item.to) }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</NuxtLink>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { mainNav } from '@/config/nav'
|
||||
|
||||
const route = useRoute()
|
||||
const expertNavItems = computed(() => mainNav.find((item) => item.key === 'expert')?.children || [])
|
||||
const type = computed(() => (route.query.type as string) || '')
|
||||
|
||||
function isActive(to: string) {
|
||||
if (to === '/expert') {
|
||||
return route.path === '/expert' && !type.value
|
||||
}
|
||||
if (to.startsWith('/expert?type=')) {
|
||||
return route.path === '/expert' && type.value === to.split('type=')[1]
|
||||
}
|
||||
return route.path === to || route.path.startsWith(`${to}/`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.expert-nav-sidebar {
|
||||
position: sticky;
|
||||
top: 80px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #edf2f7;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.expert-nav-title {
|
||||
padding: 14px 18px;
|
||||
background: #1e3a5f;
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.expert-nav-item {
|
||||
display: block;
|
||||
padding: 13px 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
color: #334155;
|
||||
font-size: 14px;
|
||||
transition: color 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.expert-nav-item:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.expert-nav-item:hover,
|
||||
.expert-nav-item.active {
|
||||
background: #eff6ff;
|
||||
color: #1e3a5f;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.expert-nav-sidebar {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div class="module-detail-page">
|
||||
<PortalHeader :active-path="config.baseRoute" />
|
||||
|
||||
<main class="container module-detail-main">
|
||||
<div class="breadcrumb-bar">
|
||||
<NuxtLink to="/">首页</NuxtLink>
|
||||
<span>/</span>
|
||||
<NuxtLink :to="config.baseRoute">{{ config.title }}</NuxtLink>
|
||||
<span>/</span>
|
||||
<span>{{ entry.title || `${config.title}详情` }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state-wrap">
|
||||
<a-skeleton active :paragraph="{ rows: 10 }" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!entry.entryId" class="state-wrap">
|
||||
<a-result status="404" title="内容不存在" sub-title="您查找的内容不存在或已下线">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="navigateTo(config.baseRoute)">返回列表</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
|
||||
<article v-else class="module-detail-card">
|
||||
<div class="detail-head">
|
||||
<div>
|
||||
<h1>{{ entry.title }}</h1>
|
||||
<p>{{ entry.publishTime || entry.createTime || '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="config.showImage" class="detail-image">
|
||||
<img :src="resolveSiteAssetUrl(entry.image) || fallbackImage" :alt="entry.title" />
|
||||
</div>
|
||||
|
||||
<div v-if="entry.summary" class="detail-summary">
|
||||
<strong>摘要</strong>
|
||||
<p>{{ entry.summary }}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-content" v-html="formattedContent" />
|
||||
|
||||
<div v-if="entry.attachmentPath" class="detail-attachment">
|
||||
<a :href="resolveSiteAssetUrl(entry.attachmentPath)" target="_blank">
|
||||
{{ entry.attachmentName || '下载附件' }}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<PortalFooter />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { getSiteModuleEntry, resolveSiteAssetUrl, type SiteModuleEntry } from '@/api/site'
|
||||
|
||||
type ModuleDetailConfig = {
|
||||
title: string
|
||||
baseRoute: string
|
||||
showImage?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
config: ModuleDetailConfig
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const entryId = computed(() => route.params.id as string)
|
||||
const fallbackImage = '/images/%E6%97%A0%E5%9B%BE%E7%89%87.png'
|
||||
|
||||
const loading = ref(true)
|
||||
const entry = ref<Partial<SiteModuleEntry>>({})
|
||||
|
||||
const formattedContent = computed(() => {
|
||||
const content = entry.value.content || ''
|
||||
if (!content) {
|
||||
return '<p>暂无正文内容</p>'
|
||||
}
|
||||
if (/<[a-z][\s\S]*>/i.test(content)) {
|
||||
return content
|
||||
}
|
||||
return content
|
||||
.split(/\n+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
.map((item) => `<p>${item}</p>`)
|
||||
.join('')
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: computed(() => `${entry.value.title || `${props.config.title}详情`} - 广西决策咨询网`),
|
||||
})
|
||||
|
||||
async function loadEntry() {
|
||||
loading.value = true
|
||||
try {
|
||||
entry.value = await getSiteModuleEntry(entryId.value)
|
||||
} catch (error) {
|
||||
entry.value = {}
|
||||
if (!(error instanceof Error && /404/.test(error.message))) {
|
||||
message.error(error instanceof Error ? error.message : '加载失败')
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(entryId, loadEntry)
|
||||
onMounted(loadEntry)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.module-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(1200px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.module-detail-main {
|
||||
padding: 28px 0 48px;
|
||||
}
|
||||
|
||||
.breadcrumb-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 24px;
|
||||
color: #7d8a98;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.breadcrumb-bar a {
|
||||
color: #4a6fa1;
|
||||
}
|
||||
|
||||
.module-detail-card {
|
||||
padding: 32px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5ebf2;
|
||||
}
|
||||
|
||||
.detail-head h1 {
|
||||
margin: 0 0 10px;
|
||||
color: #1f2d3d;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.detail-head p {
|
||||
margin: 0;
|
||||
color: #7d8a98;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-image {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.detail-image img {
|
||||
width: 100%;
|
||||
max-height: 360px;
|
||||
object-fit: contain;
|
||||
background: #f7f9fb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-summary {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.detail-summary strong {
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
color: #1f2d3d;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.detail-summary p,
|
||||
.detail-content {
|
||||
color: #495463;
|
||||
font-size: 15px;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
.detail-content :deep(p) {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
.detail-attachment {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.detail-attachment a {
|
||||
color: #1f4fa3;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
background: #1f4fa3;
|
||||
}
|
||||
|
||||
.footer-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 36px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.footer-nav-inner a {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<div class="module-list-page">
|
||||
<PortalHeader :active-path="config.baseRoute" />
|
||||
|
||||
<main class="container module-list-main">
|
||||
<div class="module-content-layout" :class="{ 'with-sidebar': config.sidebarNavKey === 'expert' }">
|
||||
<ExpertNavSidebar v-if="config.sidebarNavKey === 'expert'" />
|
||||
|
||||
<div class="module-content-main">
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>{{ config.title }}</h1>
|
||||
<p>{{ config.description }}</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
:placeholder="`搜索${config.title}`"
|
||||
enter-button="搜索"
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="state-wrap">
|
||||
<a-skeleton v-for="i in 6" :key="i" active :paragraph="{ rows: 3 }" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!entries.length" class="state-wrap">
|
||||
<a-empty :description="`暂无${config.title}`" />
|
||||
</div>
|
||||
|
||||
<div v-else class="entry-grid" :class="{ 'no-image': !config.showImage }">
|
||||
<article
|
||||
v-for="item in entries"
|
||||
:key="item.entryId || item.id"
|
||||
class="entry-card"
|
||||
@click="goDetail(item)"
|
||||
>
|
||||
<div v-if="config.showImage" class="entry-card-image">
|
||||
<img :src="resolveSiteAssetUrl(item.image) || fallbackImage" :alt="item.title" />
|
||||
</div>
|
||||
<div class="entry-card-body">
|
||||
<div class="entry-card-top">
|
||||
<h3>{{ item.title }}</h3>
|
||||
<span>{{ item.publishTime || item.createTime?.slice(0, 10) || '' }}</span>
|
||||
</div>
|
||||
<p v-if="config.showSummary !== false">{{ item.summary || item.content || '暂无简介' }}</p>
|
||||
<div v-if="config.showAttachmentName !== false" class="entry-card-bottom">
|
||||
<span v-if="item.attachmentName">{{ item.attachmentName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="total > pageSize" class="pagination-wrap">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
show-quick-jumper
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<PortalFooter />
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { message } from 'ant-design-vue'
|
||||
import ExpertNavSidebar from '@/components/ExpertNavSidebar.vue'
|
||||
import { pageSiteModuleEntries, resolveSiteAssetUrl, type SiteModuleEntry } from '@/api/site'
|
||||
|
||||
type ModuleListConfig = {
|
||||
type: string
|
||||
title: string
|
||||
description: string
|
||||
baseRoute: string
|
||||
showImage?: boolean
|
||||
showSummary?: boolean
|
||||
showAttachmentName?: boolean
|
||||
sidebarNavKey?: 'expert'
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
config: ModuleListConfig
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const fallbackImage = '/images/%E6%97%A0%E5%9B%BE%E7%89%87.png'
|
||||
const keyword = ref((route.query.keyword as string) || '')
|
||||
const currentPage = ref(Math.max(1, Number(route.query.page || 1)))
|
||||
const pageSize = 12
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const entries = ref<SiteModuleEntry[]>([])
|
||||
|
||||
async function loadEntries() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await pageSiteModuleEntries({
|
||||
page: currentPage.value,
|
||||
limit: pageSize,
|
||||
type: props.config.type,
|
||||
keywords: keyword.value || undefined,
|
||||
})
|
||||
total.value = data.count || 0
|
||||
entries.value = data.list || []
|
||||
} catch (error) {
|
||||
total.value = 0
|
||||
entries.value = []
|
||||
message.error(error instanceof Error ? error.message : '加载失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function updateRoute() {
|
||||
router.replace({
|
||||
query: {
|
||||
...(keyword.value ? { keyword: keyword.value } : {}),
|
||||
...(currentPage.value > 1 ? { page: currentPage.value } : {}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage.value = 1
|
||||
updateRoute()
|
||||
loadEntries()
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page
|
||||
updateRoute()
|
||||
loadEntries()
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
function goDetail(item: SiteModuleEntry) {
|
||||
router.push(`${props.config.baseRoute}/${item.entryId || item.id}`)
|
||||
}
|
||||
|
||||
watch(() => route.query.keyword, (value) => {
|
||||
keyword.value = (value as string) || ''
|
||||
})
|
||||
|
||||
watch(() => route.query.page, (value) => {
|
||||
currentPage.value = Math.max(1, Number(value || 1))
|
||||
loadEntries()
|
||||
})
|
||||
|
||||
onMounted(loadEntries)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.module-list-page {
|
||||
min-height: 100vh;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: min(1200px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.module-list-main {
|
||||
padding: 32px 0 48px;
|
||||
}
|
||||
|
||||
.module-content-layout.with-sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: 220px minmax(0, 1fr);
|
||||
gap: 28px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.module-content-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.page-head h1 {
|
||||
margin: 0 0 10px;
|
||||
color: #1f2d3d;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.page-head p {
|
||||
margin: 0;
|
||||
color: #697586;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
width: min(360px, 100%);
|
||||
}
|
||||
|
||||
.entry-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.entry-grid.no-image .entry-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: 18px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e6ecf2;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.entry-card:hover {
|
||||
border-color: #9fb7d5;
|
||||
box-shadow: 0 12px 30px rgba(25, 62, 111, 0.08);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.entry-card-image img {
|
||||
width: 100%;
|
||||
height: 136px;
|
||||
object-fit: contain;
|
||||
background: #f7f9fb;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.entry-card-top {
|
||||
display: flex;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.entry-card-top h3 {
|
||||
margin: 0;
|
||||
color: #1f2d3d;
|
||||
font-size: 20px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.entry-card-top span,
|
||||
.entry-card-bottom {
|
||||
color: #7d8a98;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.entry-card-body p {
|
||||
margin: 0 0 16px;
|
||||
color: #556371;
|
||||
font-size: 14px;
|
||||
line-height: 1.8;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.state-wrap {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 12px 0 24px;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
background: #1f4fa3;
|
||||
}
|
||||
|
||||
.footer-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 36px;
|
||||
min-height: 50px;
|
||||
}
|
||||
|
||||
.footer-nav-inner a {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.module-list-main {
|
||||
padding: 24px 0 40px;
|
||||
}
|
||||
|
||||
.module-content-layout.with-sidebar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.entry-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+252
-10
@@ -1,25 +1,236 @@
|
||||
<template>
|
||||
<div class="container footer-main">
|
||||
<div class="footer-left">
|
||||
<p>扫一扫公众号</p>
|
||||
<p>了解我们的动态</p>
|
||||
<img src="/images/qrcode-mp-official.jpg" alt="公众号二维码" />
|
||||
<div>
|
||||
<div class="footer-nav">
|
||||
<div class="container footer-nav-inner">
|
||||
<span class="footer-nav-title">友情链接</span>
|
||||
<div v-if="friendLinkGroups.length" class="friend-link-groups">
|
||||
<div
|
||||
v-for="group in friendLinkGroups"
|
||||
:key="group.categoryKey"
|
||||
class="friend-link-group"
|
||||
tabindex="0"
|
||||
>
|
||||
<button type="button" class="friend-link-trigger">
|
||||
<span>{{ group.categoryName }}</span>
|
||||
<span class="friend-link-arrow">▾</span>
|
||||
</button>
|
||||
<div class="friend-link-menu">
|
||||
<a
|
||||
v-for="item in group.links"
|
||||
:key="item.linkId || item.id"
|
||||
:href="item.linkUrl"
|
||||
:target="Number(item.target) === 1 ? '_blank' : undefined"
|
||||
:rel="Number(item.target) === 1 ? 'noopener noreferrer' : undefined"
|
||||
>
|
||||
{{ item.title }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="footer-nav-empty">暂无友情链接</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-center">
|
||||
<h3>广西决策咨询网</h3>
|
||||
<p>Guangxi Decision-Making Consulting Network</p>
|
||||
<p>运营方:广西决策咨询网有限公司</p>
|
||||
<p>地址:广西壮族自治区南宁市中柬路XX号 XX楼 XX号房</p>
|
||||
|
||||
<div class="container footer-main">
|
||||
<div class="footer-left">
|
||||
<p>扫一扫公众号</p>
|
||||
<p>了解我们的动态</p>
|
||||
<img :src="wechatQrcode || '/images/qrcode-mp-official.jpg'" alt="公众号二维码" />
|
||||
</div>
|
||||
<div class="footer-center">
|
||||
<h3>广西决策咨询网</h3>
|
||||
<p>承办单位:{{ operatorName }}</p>
|
||||
<p>地址:{{ address }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-record">
|
||||
<a
|
||||
href="https://beian.miit.gov.cn/"
|
||||
target="_blank"
|
||||
rel="nofollow noopener noreferrer"
|
||||
>桂ICP备2026004011号-1</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listSiteDictionaryItems, listSiteFriendLinks, resolveSiteAssetUrl, type SiteFriendLink } from '@/api/site'
|
||||
|
||||
const operatorName = ref('广西决策咨询网有限公司')
|
||||
const address = ref('广西壮族自治区南宁市良庆区五象大道401号五象航洋城3号楼')
|
||||
const wechatQrcode = ref('')
|
||||
const friendLinks = ref<SiteFriendLink[]>([])
|
||||
|
||||
const friendLinkGroups = computed(() => {
|
||||
const groupMap = new Map<string, { categoryKey: string; categoryName: string; categorySort: number; links: SiteFriendLink[] }>()
|
||||
|
||||
friendLinks.value.forEach((item) => {
|
||||
const categoryName = (item.categoryName || '其他链接').trim() || '其他链接'
|
||||
const categoryKey = item.categoryId ? `${item.categoryId}` : categoryName
|
||||
const current = groupMap.get(categoryKey)
|
||||
|
||||
if (current) {
|
||||
current.links.push(item)
|
||||
return
|
||||
}
|
||||
|
||||
groupMap.set(categoryKey, {
|
||||
categoryKey,
|
||||
categoryName,
|
||||
categorySort: Number(item.categorySort || 0),
|
||||
links: [item],
|
||||
})
|
||||
})
|
||||
|
||||
return Array.from(groupMap.values()).sort((left, right) => {
|
||||
const sortCompare = right.categorySort - left.categorySort
|
||||
return sortCompare || left.categoryName.localeCompare(right.categoryName, 'zh-Hans-CN')
|
||||
})
|
||||
})
|
||||
|
||||
async function loadDictionary() {
|
||||
try {
|
||||
const items = await listSiteDictionaryItems({ dictCode: 'site_contact' })
|
||||
const itemMap = Object.fromEntries((items || []).map((item) => [item.itemKey, item.itemValue]))
|
||||
|
||||
operatorName.value = itemMap.operator_name || operatorName.value
|
||||
address.value = itemMap.address || address.value
|
||||
wechatQrcode.value = resolveSiteAssetUrl(itemMap.wechat_qrcode || '')
|
||||
} catch (error) {
|
||||
console.error('加载站点联系字典失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFriendLinks() {
|
||||
try {
|
||||
friendLinks.value = await listSiteFriendLinks()
|
||||
} catch (error) {
|
||||
console.error('加载友情链接失败', error)
|
||||
friendLinks.value = []
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDictionary()
|
||||
loadFriendLinks()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
width: min(1200px, calc(100% - 32px));
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer-nav {
|
||||
background: #1f4fa3;
|
||||
}
|
||||
|
||||
.footer-nav-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 24px;
|
||||
min-height: 58px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.footer-nav-title {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.friend-link-groups {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.friend-link-group {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.friend-link-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: rgba(255, 255, 255, 0.94);
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.friend-link-arrow {
|
||||
font-size: 12px;
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.friend-link-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(100% + 8px);
|
||||
display: none;
|
||||
min-width: 168px;
|
||||
max-width: min(280px, calc(100vw - 32px));
|
||||
padding: 8px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
box-shadow: 0 12px 32px rgba(14, 37, 72, 0.18);
|
||||
}
|
||||
|
||||
.friend-link-menu::after {
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
top: -6px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #fff;
|
||||
transform: rotate(45deg);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.friend-link-menu a {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
padding: 9px 10px;
|
||||
border-radius: 4px;
|
||||
color: #31445a;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.friend-link-menu a:hover {
|
||||
background: #eef5ff;
|
||||
color: #1f4fa3;
|
||||
}
|
||||
|
||||
.friend-link-group:hover .friend-link-menu,
|
||||
.friend-link-group:focus-within .friend-link-menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.friend-link-group:hover .friend-link-arrow,
|
||||
.friend-link-group:focus-within .friend-link-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.footer-nav-empty {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.footer-main {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -61,6 +272,22 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.footer-record {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #e7ebef;
|
||||
color: #697680;
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-record a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.footer-record a:hover {
|
||||
color: #1f4fa3;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.footer-main {
|
||||
flex-direction: column;
|
||||
@@ -72,5 +299,20 @@
|
||||
.container {
|
||||
width: min(100%, calc(100% - 24px));
|
||||
}
|
||||
|
||||
.footer-nav-inner {
|
||||
gap: 18px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.friend-link-groups {
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.friend-link-menu {
|
||||
left: 0;
|
||||
top: calc(100% + 8px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -77,7 +77,7 @@ const navItems: NavItem[] = [
|
||||
{ label: '党中央国务院信息', to: '/news?type=central' },
|
||||
{ label: '自治区党委政府信息', to: '/news?type=region' },
|
||||
{ label: '其他(厅委办)信息', to: '/news?type=department' },
|
||||
{ label: '最新发布', to: '/news?type=latest' }
|
||||
{ label: '最新', to: '/news?type=latest' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -86,6 +86,8 @@ const navItems: NavItem[] = [
|
||||
children: [
|
||||
{ label: '政策原文', to: '/reference?type=policy' },
|
||||
{ label: '深度解读', to: '/reference?type=analysis' },
|
||||
{ label: '研究成果', to: '/reference?type=research' },
|
||||
{ label: '专题研究', to: '/reference?type=special' },
|
||||
{ label: '东盟研究', to: '/reference?type=asean' },
|
||||
{ label: '数据服务', to: '/reference?type=data' }
|
||||
]
|
||||
@@ -97,16 +99,21 @@ const navItems: NavItem[] = [
|
||||
{ label: '市县决策', to: '/consultation?type=city' },
|
||||
{ label: '前沿观察', to: '/consultation?type=frontier' },
|
||||
{ label: '行业资讯', to: '/consultation?type=industry' },
|
||||
{ label: '企业动态', to: '/consultation?type=enterprise' }
|
||||
{ label: '企业动态', to: '/consultation?type=enterprise' },
|
||||
{ label: '研究热点', to: '/consultation?type=research' },
|
||||
{ label: '学术活动', to: '/consultation?type=academic' },
|
||||
{ label: '其他汇编', to: '/consultation?type=other' }
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '专家资讯',
|
||||
to: '/expert',
|
||||
children: [
|
||||
{ label: '专家库', to: '/expert' },
|
||||
{ label: '专家视点', to: '/expert?type=view' },
|
||||
{ label: '专家动态', to: '/expert?type=dynamic' },
|
||||
{ label: '专家申请', to: '/expert/apply' }
|
||||
{ label: '专家申请', to: '/expert/apply' },
|
||||
{ label: '资料下载', to: '/expert/downloads' }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -134,7 +141,8 @@ const navItems: NavItem[] = [
|
||||
{ label: '学会简介', to: '/about' },
|
||||
{ label: '组织机构', to: '/about/organization' },
|
||||
{ label: '学会章程', to: '/about/charter' },
|
||||
{ label: '咨询服务', to: '/about/consultation' }
|
||||
{ label: '咨询服务', to: '/about/consultation' },
|
||||
{ label: '加入我们', to: '/about/join' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -40,46 +40,46 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧操作区 -->
|
||||
<div class="flex items-center gap-2 flex-shrink-0 nav-right">
|
||||
<!-- PC 端:登录/头像 -->
|
||||
<div class="hidden md:flex items-center gap-3">
|
||||
<template v-if="!isAuthed">
|
||||
<a-button type="primary" @click="navigateTo('/login')">{{ '登录' }}</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 用户头像 -->
|
||||
<a-dropdown :trigger="['hover']" placement="bottomRight">
|
||||
<a-space>
|
||||
<a-avatar :src="userAvatar" :size="32">
|
||||
<template v-if="!userAvatar" #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<span class="text-gray-100">{{ userName }}</span>
|
||||
</a-space>
|
||||
<template #overlay>
|
||||
<a-menu @click="onUserMenuClick">
|
||||
<a-menu-item key="profile"><ProfileOutlined style="margin-right: 8px" />个人信息</a-menu-item>
|
||||
<a-menu-item key="my-suggestions"><MessageOutlined style="margin-right: 8px" />我的建言</a-menu-item>
|
||||
<template v-if="isSuperAdmin">
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="admin">⚙️ 后台管理</a-menu-item>
|
||||
</template>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout">{{ '退出登录' }}</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</div>
|
||||
<!-- <div class="flex items-center gap-2 flex-shrink-0 nav-right">-->
|
||||
<!-- <!– PC 端:登录/头像 –>-->
|
||||
<!-- <div class="hidden md:flex items-center gap-3">-->
|
||||
<!-- <template v-if="!isAuthed">-->
|
||||
<!-- <a-button type="primary" @click="navigateTo('/login')">{{ '登录' }}</a-button>-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else>-->
|
||||
<!-- <!– 用户头像 –>-->
|
||||
<!-- <a-dropdown :trigger="['hover']" placement="bottomRight">-->
|
||||
<!-- <a-space>-->
|
||||
<!-- <a-avatar :src="userAvatar" :size="32">-->
|
||||
<!-- <template v-if="!userAvatar" #icon>-->
|
||||
<!-- <UserOutlined />-->
|
||||
<!-- </template>-->
|
||||
<!-- </a-avatar>-->
|
||||
<!-- <span class="text-gray-100">{{ userName }}</span>-->
|
||||
<!-- </a-space>-->
|
||||
<!-- <template #overlay>-->
|
||||
<!-- <a-menu @click="onUserMenuClick">-->
|
||||
<!-- <a-menu-item key="profile"><ProfileOutlined style="margin-right: 8px" />个人信息</a-menu-item>-->
|
||||
<!-- <a-menu-item key="my-suggestions"><MessageOutlined style="margin-right: 8px" />我的建言</a-menu-item>-->
|
||||
<!-- <template v-if="isSuperAdmin">-->
|
||||
<!-- <a-menu-divider />-->
|
||||
<!-- <a-menu-item key="admin">⚙️ 后台管理</a-menu-item>-->
|
||||
<!-- </template>-->
|
||||
<!-- <a-menu-divider />-->
|
||||
<!-- <a-menu-item key="logout">{{ '退出登录' }}</a-menu-item>-->
|
||||
<!-- </a-menu>-->
|
||||
<!-- </template>-->
|
||||
<!-- </a-dropdown>-->
|
||||
<!-- </template>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 移动端:汉堡菜单按钮 -->
|
||||
<button class="md:hidden flex flex-col justify-center items-center w-10 h-10 gap-1.5 rounded-lg bg-white/10 hover:bg-white/20 border border-white/20 transition-colors" @click="open = true">
|
||||
<span class="block w-5 h-0.5 bg-white rounded-full"></span>
|
||||
<span class="block w-5 h-0.5 bg-white rounded-full"></span>
|
||||
<span class="block w-5 h-0.5 bg-white rounded-full"></span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- <!– 移动端:汉堡菜单按钮 –>-->
|
||||
<!-- <button class="md:hidden flex flex-col justify-center items-center w-10 h-10 gap-1.5 rounded-lg bg-white/10 hover:bg-white/20 border border-white/20 transition-colors" @click="open = true">-->
|
||||
<!-- <span class="block w-5 h-0.5 bg-white rounded-full"></span>-->
|
||||
<!-- <span class="block w-5 h-0.5 bg-white rounded-full"></span>-->
|
||||
<!-- <span class="block w-5 h-0.5 bg-white rounded-full"></span>-->
|
||||
<!-- </button>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</a-layout-header>
|
||||
</a-affix>
|
||||
@@ -102,14 +102,14 @@
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
<div class="mt-4">
|
||||
<a-button v-if="!isAuthed" block type="primary" @click="onNav('/login')">{{ '登录' || '登录' }}</a-button>
|
||||
<template v-else>
|
||||
<a-button block type="primary" class="mb-2" @click="onNav('/profile')">个人中心</a-button>
|
||||
<a-button v-if="isSuperAdmin" block @click="onNav('/admin')">⚙️ 后台管理</a-button>
|
||||
<a-button block danger class="mt-2" @click="logout">{{ '退出登录' || '退出登录' }}</a-button>
|
||||
</template>
|
||||
</div>
|
||||
<!-- <div class="mt-4">-->
|
||||
<!-- <a-button v-if="!isAuthed" block type="primary" @click="onNav('/login')">{{ '登录' || '登录' }}</a-button>-->
|
||||
<!-- <template v-else>-->
|
||||
<!-- <a-button block type="primary" class="mb-2" @click="onNav('/profile')">个人中心</a-button>-->
|
||||
<!-- <a-button v-if="isSuperAdmin" block @click="onNav('/admin')">⚙️ 后台管理</a-button>-->
|
||||
<!-- <a-button block danger class="mt-2" @click="logout">{{ '退出登录' || '退出登录' }}</a-button>-->
|
||||
<!-- </template>-->
|
||||
<!-- </div>-->
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
@@ -128,10 +128,26 @@ const open = ref(false)
|
||||
|
||||
const selectedKeys = computed(() => {
|
||||
const currentPath = route.path
|
||||
const currentType = typeof route.query.type === 'string' ? route.query.type : ''
|
||||
|
||||
const exactChildHit = nav.value
|
||||
.flatMap(item => item.children || [])
|
||||
.find((item) => item.to === route.fullPath || buildPathWithQuery(item.to) === route.fullPath)
|
||||
if (exactChildHit) return [exactChildHit.key]
|
||||
|
||||
const exactHit = nav.value.find((item) => item.to === currentPath)
|
||||
if (exactHit) return [exactHit.key]
|
||||
|
||||
if (currentType) {
|
||||
const childTypeHit = nav.value
|
||||
.flatMap(item => item.children || [])
|
||||
.find((item) => {
|
||||
const child = normalizeNavTarget(item.to)
|
||||
return child.path === currentPath && child.type === currentType
|
||||
})
|
||||
if (childTypeHit) return [childTypeHit.key]
|
||||
}
|
||||
|
||||
const prefixHit = nav.value.find((item) => item.to !== '/' && currentPath.startsWith(`${item.to}/`))
|
||||
if (prefixHit) return [prefixHit.key]
|
||||
|
||||
@@ -141,6 +157,20 @@ const selectedKeys = computed(() => {
|
||||
return ['home']
|
||||
})
|
||||
|
||||
function normalizeNavTarget(to: string) {
|
||||
const [path, search = ''] = to.split('?')
|
||||
const params = new URLSearchParams(search)
|
||||
return {
|
||||
path,
|
||||
type: params.get('type') || '',
|
||||
}
|
||||
}
|
||||
|
||||
function buildPathWithQuery(to: string) {
|
||||
const normalized = normalizeNavTarget(to)
|
||||
return normalized.type ? `${normalized.path}?type=${normalized.type}` : normalized.path
|
||||
}
|
||||
|
||||
// 获取 badge 样式类
|
||||
function getBadgeClass(badge: string) {
|
||||
const baseClass = 'ml-1.5 px-1.5 py-0.5 text-xs font-medium rounded'
|
||||
@@ -157,7 +187,7 @@ const token = ref('')
|
||||
const user = ref<User | null>(null)
|
||||
const isAuthed = computed(() => !!token.value)
|
||||
const userName = computed(() => String(user.value?.nickname || user.value?.username || '已登录'))
|
||||
const isSuperAdmin = computed(() => !!(user.value as any)?.isAdmin)
|
||||
const isSuperAdmin = computed(() => !!(user.value as User & { isAdmin?: boolean } | null)?.isAdmin)
|
||||
const userAvatar = computed(() => {
|
||||
const candidate =
|
||||
user.value?.avatarUrl ||
|
||||
|
||||
Reference in New Issue
Block a user