Files
jczxw-pc/app/pages/expert/index.vue
T
2026-09-11 12:25:38 +08:00

581 lines
12 KiB
Vue

<template>
<div class="expert-page">
<div class="page-header">
<h1 class="page-title">{{ currentTitle }}</h1>
<p class="page-desc">{{ currentDesc }}</p>
</div>
<div class="expert-layout">
<aside class="category-sidebar">
<div class="category-sidebar-title">专家资讯</div>
<NuxtLink
v-for="item in expertNavItems"
:key="item.to"
:to="item.to"
class="category-item"
:class="{ active: isNavActive(item.to) }"
>
{{ item.label }}
</NuxtLink>
</aside>
<main class="expert-content">
<div class="toolbar">
<a-input-search
v-model:value="keywords"
:placeholder="showArticleList ? '搜索文章标题 / 摘要' : '搜索姓名 / 单位 / 研究领域'"
class="toolbar-search"
@search="handleSearch"
/>
<a-button type="primary" @click="navigateTo('/expert/apply')">专家申请</a-button>
</div>
<div v-if="loading" class="loading-placeholder">
<a-spin size="large" />
</div>
<template v-else-if="showArticleList">
<div class="content-summary">
<span>{{ currentTitle }}</span>
<span> {{ total }} 篇文章</span>
</div>
<div class="article-list">
<div v-for="item in articles" :key="item.id" class="article-item" @click="handleArticleView(item)">
<div v-if="item.image" class="article-thumb">
<img :src="item.image" :alt="item.title" />
</div>
<div class="article-main">
<h3 class="article-title">{{ item.title }}</h3>
<p class="article-overview">{{ item.overview }}</p>
<div class="article-meta">
<span>{{ item.source }}</span>
<span>{{ item.publishTime }}</span>
<span v-if="item.views">浏览 {{ item.views }}</span>
</div>
</div>
</div>
<div v-if="!articles.length" class="empty-placeholder">
<a-empty description="暂无内容" />
</div>
</div>
</template>
<div v-else class="expert-grid">
<div v-for="item in experts" :key="item.expertId" class="expert-card" @click="handleView(item)">
<div class="expert-avatar-wrap">
<img v-if="item.avatar" :src="item.avatar" :alt="item.name" class="expert-avatar" />
<div v-else class="expert-avatar-placeholder">{{ item.name?.charAt(0) }}</div>
</div>
<h3 class="expert-name">{{ item.name }}</h3>
<div class="expert-title">{{ item.title || '未填写职称' }}</div>
<div class="expert-org">{{ item.organization || '未填写单位' }}</div>
<p class="expert-research">{{ item.researchArea || '未填写研究领域' }}</p>
<div class="expert-meta">
<span>{{ item.education || '学历未填写' }}</span>
<span>{{ item.joinTime || '待补充' }}</span>
</div>
</div>
<div v-if="!experts.length" class="empty-placeholder">
<a-empty description="暂无专家数据" />
</div>
</div>
<div class="pagination-wrap" v-if="total > pageSize">
<a-pagination
v-model:current="currentPage"
:total="total"
:page-size="pageSize"
@change="handlePageChange"
/>
</div>
</main>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { mainNav } from '@/config/nav'
import { pageSiteArticles, pageSiteExperts, resolveSiteAssetUrl, type SiteArticle, type SiteExpert } from '@/api/site'
const router = useRouter()
const route = useRoute()
const type = computed(() => (route.query.type as string) || '')
const keywords = ref((route.query.keyword as string) || '')
const currentPage = ref(1)
const pageSize = ref(12)
const total = ref(0)
const loading = ref(false)
const experts = ref<SiteExpert[]>([])
const articles = ref<ArticleItem[]>([])
const showArticleList = computed(() => ['view', 'dynamic'].includes(type.value))
const expertNavItems = computed(() => mainNav.find((item) => item.key === 'expert')?.children || [])
const currentTitle = computed(() => {
if (type.value === 'view') return '专家视点'
if (type.value === 'dynamic') return '专家动态'
return '专家库'
})
const currentDesc = computed(() => {
if (type.value === 'view') return '聚合专家研究观点与专业观察'
if (type.value === 'dynamic') return '展示专家动态、活动资讯与相关报道'
return '汇聚各领域认证专家,展示研究方向与专业背景'
})
type ArticleItem = {
id: number
title: string
overview: string
image: string
source: string
publishTime: string
views: number
}
useHead({
title: computed(() => {
if (type.value === 'view') {
return '专家视点 - 决策咨询网'
}
if (type.value === 'dynamic') {
return '专家动态 - 决策咨询网'
}
return '专家库 - 决策咨询网'
})
})
function isNavActive(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
}
async function loadExperts() {
if (showArticleList.value) {
return
}
loading.value = true
try {
const data = await pageSiteExperts({
page: currentPage.value,
limit: pageSize.value,
keywords: keywords.value || undefined,
})
experts.value = data.list || []
total.value = data.count || 0
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '加载失败')
experts.value = []
total.value = 0
} finally {
loading.value = false
}
}
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),
}
}
async function loadArticles() {
if (!showArticleList.value) {
return
}
loading.value = true
try {
const data = await pageSiteArticles({
page: currentPage.value,
limit: pageSize.value,
keywords: keywords.value || undefined,
category: 'expert',
type: type.value,
})
articles.value = (data.list || []).map(normalizeArticle)
total.value = data.count || 0
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '加载失败')
articles.value = []
total.value = 0
} finally {
loading.value = false
}
}
function loadCurrentData() {
if (showArticleList.value) {
loadArticles()
return
}
loadExperts()
}
function handleSearch() {
currentPage.value = 1
router.replace({
query: {
...(type.value ? { type: type.value } : {}),
...(keywords.value ? { keyword: keywords.value } : {}),
},
})
loadCurrentData()
}
function handlePageChange(page: number) {
currentPage.value = page
loadCurrentData()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function handleView(item: SiteExpert) {
router.push(`/expert/${item.expertId}`)
}
function handleArticleView(item: ArticleItem) {
router.push(`/article/${item.id}`)
}
watch(() => route.query.keyword, (value) => {
keywords.value = (value as string) || ''
currentPage.value = 1
loadCurrentData()
})
watch(() => route.query.type, () => {
currentPage.value = 1
loadCurrentData()
})
onMounted(() => {
loadCurrentData()
})
</script>
<style scoped>
.expert-page {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.page-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
font-size: 32px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px;
}
.page-desc {
font-size: 16px;
color: #6b7280;
margin: 0;
}
.expert-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 28px;
align-items: start;
}
.category-sidebar {
position: sticky;
top: 80px;
overflow: hidden;
background: #fff;
border-radius: 8px;
border: 1px solid #edf2f7;
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.05);
}
.category-sidebar-title {
padding: 14px 18px;
background: #1e3a5f;
color: #fff;
font-size: 15px;
font-weight: 700;
}
.category-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;
}
.category-item:last-child {
border-bottom: 0;
}
.category-item:hover,
.category-item.active {
background: #eff6ff;
color: #1e3a5f;
font-weight: 700;
}
.expert-content {
min-width: 0;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 28px;
}
.toolbar-search {
max-width: 360px;
}
.content-summary {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding: 12px 16px;
background: #fff;
border-radius: 8px;
color: #1e3a5f;
font-weight: 700;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
}
.content-summary span:last-child {
color: #94a3b8;
font-size: 13px;
font-weight: 400;
}
.article-list {
display: grid;
gap: 12px;
}
.article-item {
display: flex;
gap: 18px;
padding: 18px;
background: #fff;
border-radius: 8px;
border: 1px solid #edf2f7;
cursor: pointer;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.article-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
}
.article-thumb {
width: 150px;
height: 100px;
flex-shrink: 0;
border-radius: 6px;
overflow: hidden;
background: #f8fafc;
}
.article-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.article-main {
min-width: 0;
}
.article-title {
margin: 0 0 8px;
color: #1f2937;
font-size: 17px;
font-weight: 700;
line-height: 1.45;
}
.article-overview {
margin: 0;
color: #64748b;
font-size: 13px;
line-height: 1.7;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.article-meta {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 12px;
color: #94a3b8;
font-size: 12px;
}
.expert-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 22px;
}
.expert-card {
background: #fff;
border-radius: 16px;
padding: 24px 20px;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.expert-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1);
}
.expert-avatar-wrap {
display: flex;
justify-content: center;
margin-bottom: 16px;
}
.expert-avatar,
.expert-avatar-placeholder {
width: 96px;
height: 96px;
border-radius: 50%;
}
.expert-avatar {
object-fit: cover;
}
.expert-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e3a5f, #3498db);
color: #fff;
font-size: 36px;
font-weight: 700;
}
.expert-name {
margin: 0 0 8px;
text-align: center;
font-size: 20px;
color: #1f2937;
}
.expert-title,
.expert-org,
.expert-research,
.expert-meta {
text-align: center;
}
.expert-title {
color: #2563eb;
font-size: 14px;
font-weight: 600;
margin-bottom: 8px;
}
.expert-org {
color: #475569;
font-size: 14px;
margin-bottom: 8px;
}
.expert-research {
min-height: 44px;
color: #6b7280;
font-size: 13px;
line-height: 1.7;
margin: 0 0 12px;
}
.expert-meta {
display: flex;
justify-content: center;
gap: 12px;
color: #94a3b8;
font-size: 12px;
}
.loading-placeholder,
.empty-placeholder {
padding: 60px 0;
text-align: center;
grid-column: 1 / -1;
}
.pagination-wrap {
margin-top: 32px;
text-align: center;
}
@media (max-width: 1024px) {
.expert-layout {
grid-template-columns: 1fr;
}
.category-sidebar {
position: static;
}
.expert-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar-search {
max-width: none;
}
.article-item {
flex-direction: column;
}
.article-thumb {
width: 100%;
height: 180px;
}
.expert-grid {
grid-template-columns: 1fr;
}
}
</style>