更新首页

This commit is contained in:
2026-09-11 12:25:38 +08:00
parent 9e24153eac
commit c29c3be942
326 changed files with 6564 additions and 44307 deletions
+126 -682
View File
@@ -1,16 +1,9 @@
<template>
<div class="about-subpage">
<!-- 顶部 Banner -->
<div class="about-banner">
<div class="mx-auto max-w-screen-xl px-4">
<h1 class="banner-title">关于我们</h1>
<p class="banner-desc">广西决策咨询网 · 汇聚智慧服务决策</p>
</div>
</div>
<!-- <div class="about-banner"></div>-->
<div class="mx-auto max-w-screen-xl px-4 py-8">
<a-row :gutter="[32, 0]">
<!-- 左侧导航 -->
<a-col :xs="24" :lg="6">
<div class="side-nav">
<div class="side-nav-title">关于我们</div>
@@ -27,227 +20,24 @@
</div>
</a-col>
<!-- 右侧内容 -->
<a-col :xs="24" :lg="18">
<!-- 学会简介 -->
<div v-show="currentSection === 'intro'" class="content-card">
<h2 class="content-title">学会简介</h2>
<div class="content-body">
<p>广西决策咨询学会广西决策咨询中心是在中共广西壮族自治区委员会广西壮族自治区人民政府的领导下由全区各高校科研机构政府部门从事决策咨询研究的专家学者和实际工作者自愿组成的学术性非营利性社会组织</p>
<p>学会以服务党政决策为核心使命围绕广西经济社会发展中的重大问题开展战略性综合性前瞻性研究为自治区党委政府重大决策提供智力支撑</p>
<div class="info-highlight">
<div class="highlight-item">
<div class="highlight-number">200+</div>
<div class="highlight-label">签约专家</div>
</div>
<div class="highlight-item">
<div class="highlight-number">20</div>
<div class="highlight-label">服务历史</div>
</div>
<div class="highlight-item">
<div class="highlight-number">1000+</div>
<div class="highlight-label">咨询报告</div>
</div>
<div class="highlight-item">
<div class="highlight-number">50+</div>
<div class="highlight-label">重大课题</div>
</div>
</div>
<h3>主要职能</h3>
<div class="function-list">
<div class="function-item" v-for="item in mainFunctions" :key="item.title">
<div class="function-icon">{{ item.icon }}</div>
<div class="function-content">
<h4>{{ item.title }}</h4>
<p>{{ item.desc }}</p>
</div>
</div>
</div>
<div class="content-card">
<div v-if="loading" class="content-loading">
<a-skeleton active :paragraph="{ rows: 10 }" />
</div>
</div>
<!-- 组织机构 -->
<div v-show="currentSection === 'organization'" class="content-card">
<h2 class="content-title">组织机构</h2>
<div class="content-body">
<div class="org-chart">
<div class="org-level org-top">
<div class="org-box org-primary">理事会</div>
</div>
<div class="org-connector"></div>
<div class="org-level org-mid">
<div class="org-box org-secondary">常务理事会</div>
</div>
<div class="org-connector"></div>
<div class="org-level org-bottom">
<div class="org-box org-third">学术委员会</div>
<div class="org-box org-third">秘书处</div>
<div class="org-box org-third">专家委员会</div>
</div>
</div>
<h3 class="mt-8">主要领导</h3>
<div class="leader-grid">
<div v-for="leader in leaders" :key="leader.name" class="leader-card">
<div class="leader-avatar">{{ leader.name.charAt(0) }}</div>
<div class="leader-info">
<div class="leader-name">{{ leader.name }}</div>
<div class="leader-pos">{{ leader.position }}</div>
</div>
</div>
</div>
<h3 class="mt-8">专家委员会成员</h3>
<div class="committee-tags">
<a-tag v-for="name in committeeMembers" :key="name" color="blue" style="margin-bottom:8px">{{ name }}</a-tag>
</div>
<div v-else-if="!hasCurrentArticle" class="content-empty">
<a-empty description="暂无内容" />
</div>
</div>
<!-- 学会章程 -->
<div v-show="currentSection === 'charter'" class="content-card">
<h2 class="content-title">学会章程</h2>
<div class="content-body charter-body">
<div v-for="chapter in charter" :key="chapter.title" class="charter-chapter">
<h3>{{ chapter.title }}</h3>
<div v-for="(item, idx) in chapter.items" :key="idx" class="charter-item">
<span class="charter-no">{{ idx + 1 }}</span>
<span>{{ item }}</span>
</div>
<template v-else>
<h2 class="content-title">{{ currentArticle.title || currentNavLabel }}</h2>
<div class="content-meta">
<span v-if="currentArticle.publishTime">发布时间{{ currentArticle.publishTime }}</span>
<span v-if="currentArticle.source">来源{{ currentArticle.source }}</span>
</div>
</div>
</div>
<!-- 咨询服务 -->
<div v-show="currentSection === 'consultation'" class="content-card">
<h2 class="content-title">咨询服务</h2>
<div class="content-body">
<p class="service-intro">广西决策咨询网为各级政府机构科研单位及企业提供专业系统的决策咨询服务涵盖政策研究战略规划项目评估等多个领域</p>
<div class="service-cards">
<div v-for="service in consultationServices" :key="service.title" class="service-card">
<div class="service-icon">{{ service.icon }}</div>
<h3>{{ service.title }}</h3>
<p>{{ service.desc }}</p>
<div class="service-tags-wrap">
<a-tag v-for="tag in service.tags" :key="tag">{{ tag }}</a-tag>
</div>
</div>
</div>
<div class="contact-box">
<h3>联系我们</h3>
<div class="contact-grid">
<div class="contact-item">
<span class="contact-icon">📞</span>
<div>
<div class="contact-label">联系电话</div>
<div class="contact-value">0771-5386339</div>
</div>
</div>
<div class="contact-item">
<span class="contact-icon">📧</span>
<div>
<div class="contact-label">电子邮箱</div>
<div class="contact-value">gxjzxzx@126.com</div>
</div>
</div>
<div class="contact-item">
<span class="contact-icon">📍</span>
<div>
<div class="contact-label">办公地址</div>
<div class="contact-value">广西南宁市良庆区五象大道401号</div>
</div>
</div>
<div class="contact-item">
<span class="contact-icon"></span>
<div>
<div class="contact-label">工作时间</div>
<div class="contact-value">周一至周五 9:00-17:30</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 加入我们 -->
<div v-show="currentSection === 'join'" class="content-card">
<h2 class="content-title">加入我们</h2>
<div class="content-body">
<p>我们热忱欢迎符合条件的单位和个人加入广西决策咨询学会共同推动广西决策咨询事业高质量发展</p>
<div class="join-cards">
<div class="join-card enterprise-card">
<div class="join-card-icon">🏢</div>
<h3>企业会员</h3>
<div class="join-qualifications">
<h4>入会资格</h4>
<ul>
<li>在广西依法注册具有法人资格的企事业单位</li>
<li>认同学会章程支持学会工作</li>
<li>具有一定规模和社会影响力</li>
</ul>
<h4>所需材料</h4>
<ul>
<li>入会申请表加盖公章</li>
<li>营业执照副本</li>
<li>法人代表身份证</li>
<li>单位简介</li>
</ul>
</div>
<a-button type="primary" block size="large" @click="navigateTo('/about/join/enterprise')">
企业会员申请
</a-button>
</div>
<div class="join-card personal-card">
<div class="join-card-icon">👤</div>
<h3>个人会员</h3>
<div class="join-qualifications">
<h4>入会资格</h4>
<ul>
<li>热爱决策咨询研究认同学会章程</li>
<li>大学本科及以上学历</li>
<li>具有相关专业工作经历</li>
</ul>
<h4>所需材料</h4>
<ul>
<li>入会申请表本人签字</li>
<li>个人简介及研究成果</li>
<li>职称证书或学历证书</li>
<li>身份证复印件</li>
</ul>
</div>
<a-button block size="large" @click="navigateTo('/about/join/personal')">
个人会员申请
</a-button>
</div>
</div>
<div class="download-section">
<h3>📥 资料下载</h3>
<div class="download-list">
<a href="#" class="download-item">
<span class="download-icon">📄</span>
<span class="download-name">企业会员入会申请表.docx</span>
<a-button size="small" type="primary" ghost>下载</a-button>
</a>
<a href="#" class="download-item">
<span class="download-icon">📄</span>
<span class="download-name">个人会员入会申请表.docx</span>
<a-button size="small" type="primary" ghost>下载</a-button>
</a>
<a href="#" class="download-item">
<span class="download-icon">📋</span>
<span class="download-name">广西决策咨询学会章程.pdf</span>
<a-button size="small" type="primary" ghost>下载</a-button>
</a>
</div>
</div>
</div>
<div class="content-body article-body" v-html="currentArticleContent" />
</template>
</div>
</a-col>
</a-row>
@@ -256,9 +46,12 @@
</template>
<script setup lang="ts">
import { pageSiteArticles, type SiteArticle } from '@/api/site'
useHead({ title: '关于我们 - 决策咨询网' })
const route = useRoute()
const router = useRouter()
const navItems = [
{ key: 'intro', label: '学会简介', icon: '🏛️' },
@@ -268,7 +61,6 @@ const navItems = [
{ key: 'join', label: '加入我们', icon: '🤝' },
]
// 根据路由path判断当前section
const sectionMap: Record<string, string> = {
'/about': 'intro',
'/about/organization': 'organization',
@@ -283,8 +75,76 @@ const currentSection = ref(
'intro'
)
const loading = ref(false)
const articleMap = ref<Record<string, SiteArticle | null>>({})
const currentArticle = computed(() => articleMap.value[currentSection.value] || null)
const hasCurrentArticle = computed(() => Boolean(currentArticle.value?.articleId))
const currentNavLabel = computed(() => {
return navItems.find((item) => item.key === currentSection.value)?.label || '关于我们'
})
const currentArticleContent = computed(() => {
const article = currentArticle.value
if (!article) {
return ''
}
if (article.content && /<[a-z][\s\S]*>/i.test(article.content)) {
return article.content
}
const text = article.content || article.overview || ''
if (!text) {
return '<p>暂无内容</p>'
}
return text
.split(/\n+/)
.map((item) => item.trim())
.filter(Boolean)
.map((item) => `<p>${escapeHtml(item)}</p>`)
.join('')
})
function escapeHtml(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function switchSection(key: string) {
currentSection.value = key
if (key === 'join') {
router.push('/about/join')
return
}
const target = Object.entries(sectionMap).find(([, value]) => value === key)?.[0] || '/about'
router.push(target)
}
async function loadArticles() {
loading.value = true
try {
const resultEntries = await Promise.all(
navItems.map(async (item) => {
const data = await pageSiteArticles({
page: 1,
limit: 1,
category: 'about',
type: item.key,
})
return [item.key, data.list?.[0] || null] as const
})
)
articleMap.value = Object.fromEntries(resultEntries)
} finally {
loading.value = false
}
}
watch(() => route.path, (newPath) => {
@@ -295,55 +155,9 @@ watch(() => route.query.section, (sec) => {
if (sec) currentSection.value = sec as string
})
const mainFunctions = [
{ icon: '🔬', title: '决策咨询研究', desc: '围绕广西经济社会发展重大问题,开展战略性、综合性、前瞻性研究' },
{ icon: '📝', title: '政策建议提供', desc: '为各级政府提供有参考价值的政策建议和咨询报告' },
{ icon: '👥', title: '专家交流合作', desc: '搭建区内外专家学者交流合作平台,推动学术思想碰撞' },
{ icon: '📡', title: '成果宣传推广', desc: '多渠道发布和推广决策咨询研究成果,服务社会各界' },
]
const leaders = [
{ name: '陈某某', position: '会长' },
{ name: '李某某', position: '副会长' },
{ name: '王某某', position: '副会长' },
{ name: '张某某', position: '秘书长' },
]
const committeeMembers = ['张教授', '李研究员', '王专家', '刘学者', '赵教授', '黄学者', '林研究员', '吴教授']
const charter = [
{
title: '第一章 总则',
items: [
'广西决策咨询学会是由全区从事决策咨询研究的专家学者和实际工作者自愿组成的学术性、非营利性社会组织。',
'学会的宗旨是:以服务党政决策为核心使命,汇聚全区高端智慧,围绕经济社会发展重大问题开展研究,为科学决策提供智力支撑。',
]
},
{
title: '第二章 业务范围',
items: [
'开展决策咨询理论与应用研究,撰写决策咨询报告。',
'组织学术交流、研讨会议等活动,促进学科发展。',
'为政府机构、企事业单位提供专业决策咨询服务。',
'培养决策咨询专业人才,开展业务培训。',
]
},
{
title: '第三章 会员',
items: [
'凡符合本章程规定,经申请并经理事会审议通过,即为本学会会员。',
'会员分为单位会员(企业会员)和个人会员两类。',
'会员有权出席会员大会,参与学会活动,享受学会提供的服务和资源。',
]
},
]
const consultationServices = [
{ icon: '🎯', title: '政策研究', desc: '深入研究党中央国务院及自治区重要政策,提供权威解读和实施建议', tags: ['政策解读', '战略规划'] },
{ icon: '🗺️', title: '规划咨询', desc: '为地方政府、园区和企业提供区域规划、产业规划、专项规划编制咨询', tags: ['区域规划', '产业规划'] },
{ icon: '📊', title: '项目评估', desc: '对重大投资项目开展可行性研究、风险评估和后评价服务', tags: ['可行性研究', '风险评估'] },
{ icon: '🔍', title: '专题调研', desc: '根据委托需求开展实地调研,形成翔实的调研报告和对策建议', tags: ['实地调研', '对策建议'] },
]
onMounted(() => {
loadArticles()
})
</script>
<style scoped>
@@ -362,21 +176,6 @@ const consultationServices = [
opacity: 0.1;
}
.banner-title {
color: #fff;
font-size: 36px;
font-weight: 700;
margin: 0 0 12px;
position: relative;
}
.banner-desc {
color: rgba(255,255,255,0.75);
font-size: 16px;
margin: 0;
position: relative;
}
.side-nav {
background: #fff;
border-radius: 12px;
@@ -434,415 +233,60 @@ const consultationServices = [
font-size: 24px;
font-weight: 700;
color: #1e3a5f;
margin: 0 0 24px;
margin: 0 0 16px;
padding-bottom: 16px;
border-bottom: 2px solid #e8f0fe;
}
.content-body h3 {
font-size: 18px;
font-weight: 600;
.content-meta {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 20px;
color: #94a3b8;
font-size: 13px;
}
.content-loading,
.content-empty {
padding: 8px 0;
}
.article-body {
color: #334155;
font-size: 15px;
line-height: 1.9;
}
.article-body :deep(h2),
.article-body :deep(h3),
.article-body :deep(h4) {
color: #1f2937;
margin: 24px 0 12px;
}
.content-body p {
font-size: 15px;
color: #4b5563;
line-height: 1.8;
margin: 0 0 12px;
text-indent: 2em;
.article-body :deep(p) {
margin: 0 0 14px;
}
/* 数据亮点 */
.info-highlight {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin: 24px 0;
padding: 24px;
background: linear-gradient(135deg, #eff6ff, #dbeafe);
border-radius: 12px;
.article-body :deep(img) {
max-width: 100%;
height: auto;
}
.highlight-item {
text-align: center;
}
.highlight-number {
font-size: 32px;
font-weight: 800;
color: #1e3a5f;
}
.highlight-label {
font-size: 13px;
color: #6b7280;
margin-top: 4px;
}
/* 职能列表 */
.function-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.function-item {
display: flex;
gap: 12px;
padding: 16px;
background: #f9fafb;
border-radius: 10px;
}
.function-icon {
font-size: 28px;
flex-shrink: 0;
}
.function-content h4 {
font-size: 15px;
font-weight: 600;
color: #1f2937;
margin: 0 0 4px;
}
.function-content p {
font-size: 13px;
color: #6b7280;
margin: 0;
text-indent: 0;
line-height: 1.5;
}
/* 组织结构图 */
.org-chart {
text-align: center;
padding: 20px;
}
.org-level {
display: flex;
justify-content: center;
gap: 20px;
}
.org-connector {
height: 24px;
width: 2px;
background: #d1d5db;
margin: 0 auto;
}
.org-box {
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
}
.org-primary {
background: #1e3a5f;
color: #fff;
min-width: 120px;
}
.org-secondary {
background: #3b82f6;
color: #fff;
min-width: 140px;
}
.org-third {
background: #eff6ff;
color: #1e3a5f;
border: 1px solid #bfdbfe;
min-width: 110px;
}
.leader-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.leader-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px;
background: #f9fafb;
border-radius: 10px;
}
.leader-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #1e3a5f;
color: #fff;
font-size: 20px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
}
.leader-name {
font-size: 15px;
font-weight: 600;
color: #1f2937;
}
.leader-pos {
font-size: 12px;
color: #6b7280;
}
.committee-tags {
margin-top: 8px;
}
/* 章程 */
.charter-body .charter-chapter {
margin-bottom: 24px;
}
.charter-chapter h3 {
font-size: 17px;
font-weight: 700;
color: #1e3a5f;
background: #eff6ff;
padding: 10px 16px;
border-radius: 6px;
margin: 0 0 12px;
}
.charter-item {
display: flex;
gap: 12px;
padding: 10px 16px;
font-size: 14px;
color: #4b5563;
line-height: 1.8;
border-bottom: 1px dashed #f0f0f0;
}
.charter-no {
color: #1e3a5f;
font-weight: 600;
flex-shrink: 0;
width: 48px;
}
/* 咨询服务 */
.service-intro {
text-indent: 2em;
}
.service-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin: 24px 0;
}
.service-card {
padding: 24px;
background: #f9fafb;
border-radius: 12px;
border: 1px solid #e5e7eb;
transition: all 0.2s;
}
.service-card:hover {
border-color: #1e3a5f;
box-shadow: 0 4px 12px rgba(30,58,95,0.1);
}
.service-icon {
font-size: 36px;
margin-bottom: 12px;
}
.service-card h3 {
font-size: 16px;
font-weight: 700;
color: #1e3a5f;
margin: 0 0 8px;
}
.service-card p {
font-size: 13px;
color: #6b7280;
margin: 0 0 12px;
text-indent: 0;
line-height: 1.6;
}
.service-tags-wrap {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.contact-box {
background: #1e3a5f;
border-radius: 16px;
padding: 32px;
margin-top: 24px;
}
.contact-box h3 {
color: #fff;
font-size: 18px;
margin: 0 0 20px;
}
.contact-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.contact-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: rgba(255,255,255,0.08);
border-radius: 10px;
}
.contact-icon {
font-size: 20px;
}
.contact-label {
font-size: 12px;
color: rgba(255,255,255,0.65);
margin-bottom: 4px;
}
.contact-value {
font-size: 14px;
color: #fff;
font-weight: 500;
}
/* 加入我们 */
.join-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin: 24px 0;
}
.join-card {
padding: 28px;
border-radius: 16px;
border: 2px solid transparent;
}
.enterprise-card {
background: #eff6ff;
border-color: #bfdbfe;
}
.personal-card {
background: #f0fdf4;
border-color: #bbf7d0;
}
.join-card-icon {
font-size: 40px;
margin-bottom: 12px;
}
.join-card h3 {
font-size: 20px;
font-weight: 700;
color: #1f2937;
.article-body :deep(ul),
.article-body :deep(ol) {
padding-left: 22px;
margin: 0 0 16px;
}
.join-qualifications h4 {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 12px 0 8px;
}
@media (max-width: 920px) {
.content-card {
padding: 24px;
}
.join-qualifications ul {
padding-left: 18px;
margin: 0 0 12px;
}
.join-qualifications li {
font-size: 13px;
color: #4b5563;
line-height: 2;
}
.download-section {
background: #f9fafb;
border-radius: 12px;
padding: 24px;
margin-top: 24px;
}
.download-section h3 {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin: 0 0 16px;
}
.download-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.download-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
text-decoration: none;
color: #374151;
transition: all 0.2s;
}
.download-item:hover {
border-color: #1e3a5f;
}
.download-icon {
font-size: 18px;
}
.download-name {
flex: 1;
font-size: 14px;
color: #374151;
}
.mt-6 { margin-top: 24px; }
.mt-8 { margin-top: 32px; }
@media (max-width: 768px) {
.info-highlight { grid-template-columns: repeat(2, 1fr); }
.function-list { grid-template-columns: 1fr; }
.service-cards { grid-template-columns: 1fr; }
.join-cards { grid-template-columns: 1fr; }
.leader-grid { grid-template-columns: repeat(2, 1fr); }
.contact-grid { grid-template-columns: 1fr; }
.content-card { padding: 20px; }
.about-banner {
padding: 36px 0 20px;
}
}
</style>
+178
View File
@@ -0,0 +1,178 @@
<template>
<div class="downloads-page">
<div class="page-header">
<h1 class="page-title">入会资料下载</h1>
<p class="page-desc">下载企业会员个人会员入会申请材料完成填写盖章或签字后上传审核</p>
</div>
<div class="downloads-grid">
<div v-for="item in downloadItems" :key="item.title" class="download-card">
<div class="download-icon">{{ item.icon }}</div>
<div class="download-body">
<h3>{{ item.title }}</h3>
<p>{{ item.desc }}</p>
<div class="download-tags">
<a-tag v-for="tag in item.tags" :key="tag" color="blue">{{ tag }}</a-tag>
</div>
</div>
<div class="download-actions">
<a-button type="primary" ghost @click="handleDownload(item.title)">下载模板</a-button>
</div>
</div>
</div>
<div class="review-section">
<h2>资格审核与资料上传</h2>
<p>请选择对应身份进入申请系统填写表单并上传盖章或签字后的申请表及证明材料</p>
<div class="review-actions">
<a-button type="primary" size="large" @click="navigateTo('/about/join/enterprise')">企业会员申请</a-button>
<a-button size="large" @click="navigateTo('/about/join/personal')">个人会员申请</a-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
useHead({ title: '入会资料下载 - 决策咨询网' })
const downloadItems = [
{
icon: '🏢',
title: '企业会员入会申请表',
desc: '适用于企业会员入会申请,需填写完整信息并加盖单位公章后上传。',
tags: ['企业会员', '盖章上传'],
},
{
icon: '👤',
title: '个人会员入会申请表',
desc: '适用于个人会员入会申请,需本人签字后连同证明材料一并提交审核。',
tags: ['个人会员', '签字上传'],
},
{
icon: '📋',
title: '学会章程与入会须知',
desc: '提交申请前请先阅读章程与入会须知,确认资格条件与资料清单。',
tags: ['章程', '资格说明'],
},
]
function handleDownload(name: string) {
message.info(`资料「${name}」下载功能待接入实际文件,当前已预留下载入口`)
}
</script>
<style scoped>
.downloads-page {
max-width: 1100px;
margin: 0 auto;
padding: 40px 20px 64px;
}
.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: 15px;
color: #6b7280;
margin: 0;
}
.downloads-grid {
display: grid;
gap: 20px;
}
.download-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background: #fff;
border-radius: 16px;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
}
.download-icon {
width: 64px;
height: 64px;
border-radius: 16px;
background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
flex-shrink: 0;
}
.download-body {
flex: 1;
}
.download-body h3 {
margin: 0 0 8px;
font-size: 18px;
color: #1f2937;
}
.download-body p {
margin: 0 0 12px;
color: #6b7280;
line-height: 1.7;
}
.download-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.download-actions {
flex-shrink: 0;
}
.review-section {
margin-top: 32px;
padding: 32px;
border-radius: 16px;
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
color: #fff;
}
.review-section h2 {
margin: 0 0 10px;
font-size: 24px;
}
.review-section p {
margin: 0 0 20px;
color: rgba(255, 255, 255, 0.82);
}
.review-actions {
display: flex;
gap: 16px;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.download-card {
flex-direction: column;
align-items: flex-start;
}
.download-actions {
width: 100%;
}
}
</style>
+53 -9
View File
@@ -65,6 +65,17 @@
<h3 class="section-title">资质证明材料</h3>
<p class="section-desc">请上传以下材料以便我们审核您的入会资格</p>
<div class="materials-tip">
<div>企业会员需提交入会申请表加盖公章营业执照法人身份证企业简介</div>
<a-button type="link" @click="navigateTo('/about/join/downloads')">前往资料下载</a-button>
</div>
<a-form-item label="入会申请表(加盖公章)">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('applicationForm')">
<a-button><UploadOutlined /> 上传申请表</a-button>
</a-upload>
</a-form-item>
<a-form-item label="营业执照">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('license')">
<a-button><UploadOutlined /> 上传营业执照</a-button>
@@ -115,9 +126,22 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { CheckCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
import { submitSiteJoinApplication, uploadSiteJoinApplicationFile } from '@/api/site'
useHead({ title: '企业会员申请 - 决策咨询网' })
type UploadRequestOption = {
file: File
onSuccess: (response?: unknown) => void
onError: (error?: unknown) => void
}
type UploadResult = {
path: string
url: string
name: string
}
const currentStep = ref(0)
const submitting = ref(false)
@@ -129,6 +153,7 @@ const formData = reactive({
email: '',
address: '',
bio: '',
applicationForm: '',
license: '',
idCard: '',
intro: '',
@@ -144,14 +169,17 @@ function beforeUpload(file: File) {
}
function handleUpload(type: string) {
return async (option: any) => {
return async (option: UploadRequestOption) => {
try {
// TODO: 调用上传API
option.onSuccess()
const form = new FormData()
form.append('file', option.file)
const data = await uploadSiteJoinApplicationFile(form)
formData[type as keyof typeof formData] = data.path
option.onSuccess(data)
message.success('上传成功')
} catch {
option.onError()
message.error('上传失败')
} catch (e: unknown) {
option.onError(e)
message.error(e instanceof Error ? e.message : '上传失败')
}
}
}
@@ -169,11 +197,14 @@ function handleNext() {
async function handleSubmit() {
submitting.value = true
try {
// TODO: 调用API提交申请
await submitSiteJoinApplication({
type: 'enterprise',
...formData,
})
message.success('提交成功,请等待审核')
navigateTo('/about/join')
} catch (e: any) {
message.error(e?.message || '提交失败')
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '提交失败')
} finally {
submitting.value = false
}
@@ -229,6 +260,19 @@ async function handleSubmit() {
margin: -10px 0 20px;
}
.materials-tip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
padding: 14px 16px;
background: #f8fafc;
border: 1px solid #dbeafe;
border-radius: 12px;
color: #475569;
}
.upload-hint {
font-size: 12px;
color: #9ca3af;
+127
View File
@@ -0,0 +1,127 @@
<template>
<div class="join-entry-page">
<div class="page-header">
<h1 class="page-title">加入我们</h1>
<p class="page-desc">请选择企业会员或个人会员申请入口按要求提交资料审核</p>
</div>
<div class="join-cards">
<div class="join-card enterprise-card">
<div class="join-card-icon">🏢</div>
<h3>企业会员申请</h3>
<p>适用于依法注册具有法人资格的企事业单位</p>
<ul>
<li>填写企业备案信息</li>
<li>上传申请表营业执照等材料</li>
<li>提交后等待后台审核</li>
</ul>
<a-button type="primary" block size="large" @click="navigateTo('/about/join/enterprise')">
进入企业会员申请
</a-button>
</div>
<div class="join-card personal-card">
<div class="join-card-icon">👤</div>
<h3>个人会员申请</h3>
<p>适用于具备相应专业背景认同学会章程的个人申请者</p>
<ul>
<li>填写个人备案信息</li>
<li>上传签字申请表及证明材料</li>
<li>提交后等待后台审核</li>
</ul>
<a-button block size="large" @click="navigateTo('/about/join/personal')">
进入个人会员申请
</a-button>
</div>
</div>
<div class="entry-actions">
<a-button @click="navigateTo('/about/join/downloads')">资料下载</a-button>
<a-button @click="navigateTo('/about')">返回关于我们</a-button>
</div>
</div>
</template>
<script setup lang="ts">
useHead({ title: '加入我们 - 决策咨询网' })
</script>
<style scoped>
.join-entry-page {
max-width: 1100px;
margin: 0 auto;
padding: 40px 20px 64px;
}
.page-header {
text-align: center;
margin-bottom: 36px;
}
.page-title {
margin: 0 0 12px;
font-size: 32px;
font-weight: 700;
color: #1f2937;
}
.page-desc {
margin: 0;
color: #6b7280;
font-size: 15px;
}
.join-cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.join-card {
padding: 28px;
border-radius: 18px;
background: #fff;
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
}
.join-card-icon {
font-size: 36px;
margin-bottom: 16px;
}
.join-card h3 {
margin: 0 0 10px;
font-size: 22px;
color: #1f2937;
}
.join-card p {
margin: 0 0 16px;
color: #6b7280;
line-height: 1.8;
}
.join-card ul {
margin: 0 0 20px;
padding-left: 20px;
color: #475569;
line-height: 1.8;
}
.entry-actions {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 28px;
}
@media (max-width: 820px) {
.join-cards {
grid-template-columns: 1fr;
}
.entry-actions {
flex-direction: column;
}
}
</style>
+83 -12
View File
@@ -89,6 +89,23 @@
<h3 class="section-title">资质证明材料</h3>
<p class="section-desc">请上传相关证明材料以便我们审核您的入会资格</p>
<div class="materials-tip">
<div>个人会员需提交入会申请表签字简介研究成果职称证书学历证书身份证研究成果或获奖证明</div>
<a-button type="link" @click="navigateTo('/about/join/downloads')">前往资料下载</a-button>
</div>
<a-form-item label="入会申请表(签字)">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('applicationForm')">
<a-button><UploadOutlined /> 上传申请表</a-button>
</a-upload>
</a-form-item>
<a-form-item label="个人简介">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('profile')">
<a-button><UploadOutlined /> 上传简介</a-button>
</a-upload>
</a-form-item>
<a-form-item label="身份证">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('idCard')">
<a-button><UploadOutlined /> 上传身份证</a-button>
@@ -143,25 +160,41 @@
</template>
<script setup lang="ts">
import dayjs, { type Dayjs } from 'dayjs'
import { message } from 'ant-design-vue'
import { CheckCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
import { submitSiteJoinApplication, uploadSiteJoinApplicationFile } from '@/api/site'
useHead({ title: '个人会员申请 - 决策咨询网' })
type UploadRequestOption = {
file: File
onSuccess: (response?: unknown) => void
onError: (error?: unknown) => void
}
type UploadResult = {
path: string
url: string
name: string
}
const currentStep = ref(0)
const submitting = ref(false)
const formData = reactive({
name: '',
gender: undefined,
birthday: undefined,
education: undefined,
gender: undefined as string | undefined,
birthday: undefined as Dayjs | undefined,
education: undefined as string | undefined,
title: '',
organization: '',
phone: '',
email: '',
researchArea: '',
bio: '',
applicationForm: '',
profile: '',
idCard: '',
diploma: '',
certificate: '',
@@ -178,14 +211,21 @@ function beforeUpload(file: File) {
}
function handleUpload(type: string) {
return async (option: any) => {
return async (option: UploadRequestOption) => {
try {
// TODO: 调用上传API
option.onSuccess()
const form = new FormData()
form.append('file', option.file)
const data = await uploadSiteJoinApplicationFile(form)
if (type === 'achievements') {
formData.achievements.push(data.path)
} else {
formData[type as Exclude<keyof typeof formData, 'achievements' | 'birthday'>] = data.path as never
}
option.onSuccess(data)
message.success('上传成功')
} catch {
option.onError()
message.error('上传失败')
} catch (e: unknown) {
option.onError(e)
message.error(e instanceof Error ? e.message : '上传失败')
}
}
}
@@ -203,11 +243,29 @@ function handleNext() {
async function handleSubmit() {
submitting.value = true
try {
// TODO: 调用API提交申请
await submitSiteJoinApplication({
type: 'personal',
name: formData.name,
gender: formData.gender,
birthday: formData.birthday ? dayjs(formData.birthday).format('YYYY-MM-DD') : '',
education: formData.education,
title: formData.title,
organization: formData.organization,
phone: formData.phone,
email: formData.email,
researchArea: formData.researchArea,
bio: formData.bio,
applicationForm: formData.applicationForm,
profile: formData.profile,
idCard: formData.idCard,
diploma: formData.diploma,
certificate: formData.certificate,
achievements: formData.achievements,
})
message.success('提交成功,请等待审核')
navigateTo('/about/join')
} catch (e: any) {
message.error(e?.message || '提交失败')
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '提交失败')
} finally {
submitting.value = false
}
@@ -263,6 +321,19 @@ async function handleSubmit() {
margin: -10px 0 20px;
}
.materials-tip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
padding: 14px 16px;
background: #f8fafc;
border: 1px solid #dbeafe;
border-radius: 12px;
color: #475569;
}
.upload-hint {
font-size: 12px;
color: #9ca3af;
+17
View File
@@ -0,0 +1,17 @@
<template>
<ModuleEntryDetailPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryDetailPage from '@/components/ModuleEntryDetailPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
title: '成果摘编',
baseRoute: '/achievement-digests',
showImage: true,
}
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<ModuleEntryListPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryListPage from '@/components/ModuleEntryListPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
type: 'achievement_digest',
title: '成果摘编',
description: '浏览成果摘编相关内容',
baseRoute: '/achievement-digests',
showImage: true,
}
</script>
+1 -1
View File
@@ -163,7 +163,7 @@ const dataSource = ref<any[]>([
{ id: 11, name: '党中央国务院信息', slug: 'news-central', sort: 1, status: 1, articleCount: 42, isSystem: true },
{ id: 12, name: '自治区党委政府信息', slug: 'news-region', sort: 2, status: 1, articleCount: 35, isSystem: true },
{ id: 13, name: '其他厅委办信息', slug: 'news-department', sort: 3, status: 1, articleCount: 29, isSystem: true },
{ id: 14, name: '最新发布', slug: 'news-latest', sort: 4, status: 1, articleCount: 22, isSystem: true },
{ id: 14, name: '最新', slug: 'news-latest', sort: 4, status: 1, articleCount: 22, isSystem: true },
]
},
{
+92 -61
View File
@@ -133,14 +133,6 @@
</main>
<footer class="site-footer">
<div class="footer-nav">
<div class="container footer-nav-inner">
<NuxtLink to="/about">友情链接</NuxtLink>
<NuxtLink to="/consultation">咨询服务</NuxtLink>
<NuxtLink to="/reference">决策成果研究文库</NuxtLink>
<NuxtLink to="/expert">政府智库学者库</NuxtLink>
</div>
</div>
<PortalFooter />
</footer>
</div>
@@ -148,6 +140,7 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { getSiteArticle, pageSiteArticles, resolveSiteAssetUrl, type SiteArticle } from '@/api/site'
import { usePageSeo } from '@/composables/usePageSeo'
definePageMeta({
@@ -161,11 +154,12 @@ type ArticleAttachment = {
}
type ArticleRecord = {
id?: string
id?: number
title: string
cover?: string
categoryName?: string
categoryPath?: string
categoryId?: number
source?: string
author?: string
publishTime?: string
@@ -191,13 +185,7 @@ const article = ref<ArticleRecord>({ title: '' })
const prevArticle = ref<ArticleLink | null>(null)
const nextArticle = ref<ArticleLink | null>(null)
const relatedArticles = ref<ArticleLink[]>([])
const hotArticles = ref<ArticleLink[]>([
{ id: 1, title: '广西数字经济发展报告(2024', rank: 1 },
{ id: 2, title: '自治区关于优化营商环境的实施意见', rank: 2 },
{ id: 3, title: '面向东盟的产业合作政策解读', rank: 3 },
{ id: 4, title: '广西乡村振兴战略实施进展报告', rank: 4 },
{ id: 5, title: '北部湾经济区发展最新动态', rank: 5 }
])
const hotArticles = ref<ArticleLink[]>([])
usePageSeo({
title: '文章详情',
@@ -211,58 +199,101 @@ useHead({
]
})
function escapeHtml(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
function formatContent(content?: string) {
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>${escapeHtml(item)}</p>`)
.join('')
}
function normalizeArticle(item: SiteArticle): ArticleRecord {
return {
id: item.articleId || item.id,
title: item.title,
cover: resolveSiteAssetUrl(item.image),
categoryId: item.categoryId,
categoryName: item.categoryName,
categoryPath: item.categoryPath,
source: item.source,
author: item.author,
publishTime: item.publishTime,
views: Number(item.views || item.actualViews || 0),
summary: item.overview,
content: formatContent(item.content),
}
}
async function loadArticle() {
loading.value = true
try {
article.value = {
id: articleId.value,
title: '广西自治区党委政府关于加快数字经济发展的实施意见',
cover: `https://picsum.photos/900/400?random=${articleId.value}`,
categoryName: '政策要闻',
categoryPath: '/news',
source: '广西壮族自治区人民政府',
author: '政策研究处',
publishTime: '2024-12-20 09:30:00',
views: 1286,
summary: '本意见旨在深入贯彻党中央、国务院关于发展数字经济的战略部署,结合广西实际,加快推进数字产业化和产业数字化,培育壮大数字经济新动能。',
content: `
<h2>一、总体要求</h2>
<p>以习近平新时代中国特色社会主义思想为指导,全面贯彻党的二十大精神,围绕建设数字中国战略部署,立足广西比较优势,坚持创新驱动、数据赋能、融合发展,加快推动数字经济与实体经济深度融合,着力打造面向东盟的数字经济发展高地。</p>
const current = await getSiteArticle(articleId.value)
article.value = normalizeArticle(current)
<h2>二、主要目标</h2>
<p>到2026年,数字经济核心产业增加值占GDP比重达到12%,数字经济总量突破1万亿元,数字化转型企业数量超过5000家,建成5G基站15万座。</p>
const [relatedPage, hotPage, siblingPage] = await Promise.all([
pageSiteArticles({
page: 1,
limit: 6,
categoryId: current.categoryId,
}),
pageSiteArticles({
page: 1,
limit: 5,
recommend: 1,
}),
pageSiteArticles({
page: 1,
limit: 100,
categoryId: current.categoryId,
}),
])
<h2>三、重点任务</h2>
<h3>(一)加快数字基础设施建设</h3>
<p>系统推进新型基础设施建设,加快5G、大数据中心、工业互联网等数字基础设施部署,构建高速、泛在、天地一体、云网融合、智能敏捷、绿色低碳的新型数字基础设施体系。</p>
relatedArticles.value = (relatedPage.list || [])
.filter((item) => (item.articleId || item.id) !== current.articleId)
.slice(0, 5)
.map((item) => ({
id: item.articleId || item.id,
title: item.title,
}))
<h3>(二)深化数字技术与实体经济融合</h3>
<p>推动制造业、农业、服务业数字化转型,加快工业互联网创新应用,推进数字农业农村建设,促进数字技术与传统产业深度融合。</p>
hotArticles.value = (hotPage.list || []).slice(0, 5).map((item, index) => ({
id: item.articleId || item.id,
title: item.title,
rank: index + 1,
}))
<h3>(三)培育壮大数字经济核心产业</h3>
<p>重点发展软件和信息技术服务业、大数据、云计算、人工智能、区块链等核心产业,打造广西数字经济核心产业集群。</p>
<h2>四、保障措施</h2>
<p>加强组织领导,完善工作机制,强化政策支持,健全评估体系,确保各项任务落到实处。</p>
`,
tags: ['数字经济', '政策解读', '广西'],
attachments: [
{ name: '广西数字经济发展实施意见(全文).pdf', url: '#', size: '2.4MB' },
{ name: '附件:实施细则.docx', url: '#', size: '856KB' }
]
}
prevArticle.value = { id: Number(articleId.value) - 1 || 1, title: '广西人工智能产业发展三年行动计划解读' }
nextArticle.value = { id: Number(articleId.value) + 1 || 2, title: '关于推动平台经济规范健康持续发展的若干措施' }
relatedArticles.value = [
{ id: 11, title: '广西数字政府建设重点任务清单发布' },
{ id: 12, title: '自治区大数据发展战略阶段性成果综述' },
{ id: 13, title: '面向东盟的数据跨境合作政策观察' },
{ id: 14, title: '产业数字化转型标杆案例分析' },
{ id: 15, title: '推动数字基础设施提质增效的实施路径' }
]
} catch {
message.error('加载失败')
const siblings = (siblingPage.list || []).map((item) => ({
id: item.articleId || item.id,
title: item.title,
}))
const currentIndex = siblings.findIndex((item) => Number(item.id) === Number(current.articleId))
prevArticle.value = currentIndex > 0 ? siblings[currentIndex - 1] : null
nextArticle.value = currentIndex >= 0 && currentIndex < siblings.length - 1 ? siblings[currentIndex + 1] : null
} catch (e) {
article.value = { title: '' }
prevArticle.value = null
nextArticle.value = null
relatedArticles.value = []
hotArticles.value = []
message.error(e instanceof Error ? e.message : '加载失败')
} finally {
loading.value = false
}
+14 -112
View File
@@ -1,121 +1,23 @@
<template>
<main class="min-h-screen bg-gray-50 p-8">
<div class="mx-auto max-w-5xl space-y-6">
<a-card title="文章列表 (pageAppArticle)" class="shadow-sm">
<div class="flex flex-wrap items-center gap-3">
<a-input-password
v-model:value="token"
placeholder="Authorization (AccessToken)"
class="w-96"
/>
<a-button :disabled="pending" @click="applyToken">设置Token</a-button>
<a-button :disabled="pending" danger @click="clearToken">清除Token</a-button>
<a-input
v-model:value="keywords"
placeholder="关键词 keywords"
class="w-72"
@pressEnter="doSearch"
/>
<a-button type="primary" :loading="pending" @click="doSearch">查询</a-button>
<a-button :disabled="pending" @click="refresh">刷新</a-button>
<div class="text-sm text-gray-500">
TenantId: {{ tenantId }}
</div>
</div>
<a-alert
v-if="error"
class="mt-4"
show-icon
type="error"
:message="String(error)"
/>
<a-table
class="mt-4"
:data-source="list"
:loading="pending"
:pagination="false"
row-key="articleId"
size="middle"
>
<a-table-column title="ID" data-index="articleId" width="90" />
<a-table-column title="标题" data-index="title" />
<a-table-column title="编号" data-index="code" width="220" />
<a-table-column title="栏目" data-index="categoryName" width="160" />
<a-table-column title="创建时间" data-index="createTime" width="180" />
</a-table>
<div class="mt-4 flex items-center justify-end">
<a-pagination
:current="page"
:page-size="limit"
:total="total"
show-size-changer
:page-size-options="['10', '20', '50', '100']"
@change="onPageChange"
@showSizeChange="onPageSizeChange"
/>
</div>
</a-card>
</div>
</main>
<ArticleListPage :config="pageConfig" />
</template>
<script setup lang="ts">
import { pageAppArticle as pageCmsArticle } from '@/api/app/article'
import { getToken, removeToken, setToken } from '@/utils/token-util'
const config = useRuntimeConfig()
const tenantId = computed(() => String(config.public.tenantId))
const page = ref(1)
const limit = ref(10)
const keywords = ref('')
const token = ref('')
onMounted(() => {
token.value = getToken()
definePageMeta({
layout: 'default',
hideLayoutHeader: true,
})
const { data, pending, error, refresh } = useAsyncData(
'app-article-page',
() =>
pageCmsArticle({
page: page.value,
limit: limit.value,
keywords: keywords.value || undefined
}),
{ server: false }
)
useHead({ title: '站内搜索 - 决策咨询网' })
const list = computed(() => data.value?.list ?? [])
const total = computed(() => data.value?.count ?? 0)
function applyToken() {
setToken(token.value, true)
refresh()
}
function clearToken() {
removeToken()
token.value = ''
refresh()
}
function doSearch() {
page.value = 1
refresh()
}
function onPageChange(nextPage: number) {
page.value = nextPage
refresh()
}
function onPageSizeChange(_current: number, nextSize: number) {
limit.value = nextSize
page.value = 1
refresh()
const pageConfig = {
title: '站内搜索',
desc: '按关键词检索全站文章内容',
bannerGradient: 'linear-gradient(135deg, #334155 0%, #0f172a 100%)',
baseRoute: '',
useSiteHeader: true,
categories: [
{ type: '', label: '全部结果' },
]
}
</script>
+55 -11
View File
@@ -18,7 +18,7 @@
<div class="info-icon">📍</div>
<div class="info-content">
<div class="info-label">办公地址</div>
<div class="info-value">广西·南宁·良庆区 五象大道401号 五象航洋城3号楼</div>
<div class="info-value">{{ contactInfo.address }}</div>
</div>
</div>
@@ -26,7 +26,7 @@
<div class="info-icon">📞</div>
<div class="info-content">
<div class="info-label">联系电话</div>
<div class="info-value">0771-5386339</div>
<div class="info-value">{{ contactInfo.phone }}</div>
</div>
</div>
@@ -34,7 +34,7 @@
<div class="info-icon">📧</div>
<div class="info-content">
<div class="info-label">电子邮箱</div>
<div class="info-value">gxjzxzx@126.com</div>
<div class="info-value">{{ contactInfo.email }}</div>
</div>
</div>
@@ -42,7 +42,7 @@
<div class="info-icon"></div>
<div class="info-content">
<div class="info-label">服务时间</div>
<div class="info-value">周一至周五 9:00-17:00</div>
<div class="info-value">{{ contactInfo.serviceHours }}</div>
</div>
</div>
@@ -50,7 +50,7 @@
<div class="info-icon">📮</div>
<div class="info-content">
<div class="info-label">邮政编码</div>
<div class="info-value">530200</div>
<div class="info-value">{{ contactInfo.postalCode }}</div>
</div>
</div>
@@ -60,7 +60,10 @@
<div class="social-title">关注我们</div>
<div class="social-items">
<a-tooltip title="微信公众号">
<div class="social-item">📱</div>
<div class="social-item">
<img v-if="contactInfo.wechatQrcode" :src="contactInfo.wechatQrcode" alt="微信公众号二维码" class="social-qrcode" />
<span v-else>📱</span>
</div>
</a-tooltip>
</div>
</div>
@@ -146,10 +149,19 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { listSiteDictionaryItems, resolveSiteAssetUrl, submitSiteConsultationMessage } from '@/api/site'
useHead({ title: '联系我们 - 广西决策咨询网' })
const submitting = ref(false)
const contactInfo = reactive({
address: '广西·南宁·良庆区 五象大道401号 五象航洋城3号楼',
phone: '0771-5386339',
email: 'gxjzxzx@126.com',
serviceHours: '周一至周五 9:00-17:00',
postalCode: '530200',
wechatQrcode: '',
})
const form = reactive({
name: '',
@@ -165,6 +177,22 @@ const rules = {
content: [{ required: true, message: '请输入咨询内容' }],
}
async function loadContactDictionary() {
try {
const items = await listSiteDictionaryItems({ dictCode: 'site_contact' })
const itemMap = Object.fromEntries((items || []).map((item) => [item.itemKey, item.itemValue]))
contactInfo.address = itemMap.address || contactInfo.address
contactInfo.phone = itemMap.phone || contactInfo.phone
contactInfo.email = itemMap.email || contactInfo.email
contactInfo.serviceHours = itemMap.service_hours || contactInfo.serviceHours
contactInfo.postalCode = itemMap.postal_code || contactInfo.postalCode
contactInfo.wechatQrcode = resolveSiteAssetUrl(itemMap.wechat_qrcode || '')
} catch (error) {
console.error('加载联系字典失败', error)
}
}
async function handleSubmit() {
if (!form.name || !form.phone || !form.content) {
message.warning('请填写必填项')
@@ -172,13 +200,17 @@ async function handleSubmit() {
}
submitting.value = true
try {
// TODO: 接入实际API提交咨询
// await submitContact(form)
await new Promise(resolve => setTimeout(resolve, 500))
await submitSiteConsultationMessage({
name: form.name.trim(),
phone: form.phone.trim(),
organization: form.organization.trim(),
type: form.type,
content: form.content.trim(),
})
message.success('咨询已提交,我们会尽快与您联系!')
handleReset()
} catch (e: any) {
message.error(e?.message || '提交失败')
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '提交失败')
} finally {
submitting.value = false
}
@@ -193,6 +225,10 @@ function handleReset() {
content: '',
})
}
onMounted(() => {
loadContactDictionary()
})
</script>
<style scoped>
@@ -258,6 +294,14 @@ function handleReset() {
line-height: 1.6;
}
.social-qrcode {
width: 96px;
height: 96px;
object-fit: contain;
border-radius: 10px;
background: #fff;
}
.info-divider {
height: 1px;
background: #f0f0f0;
+17
View File
@@ -0,0 +1,17 @@
<template>
<ModuleEntryDetailPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryDetailPage from '@/components/ModuleEntryDetailPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
title: '资料下载',
baseRoute: '/downloads',
showImage: false,
}
</script>
+21
View File
@@ -0,0 +1,21 @@
<template>
<ModuleEntryListPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryListPage from '@/components/ModuleEntryListPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
type: 'download',
title: '资料下载',
description: '浏览和下载公开资料文件',
baseRoute: '/downloads',
showImage: false,
showSummary: false,
showAttachmentName: false,
}
</script>
+120 -199
View File
@@ -1,10 +1,9 @@
<template>
<div class="expert-detail-page">
<div class="mx-auto max-w-screen-xl px-4 py-8">
<!-- 面包屑 -->
<a-breadcrumb class="mb-6">
<a-breadcrumb-item><NuxtLink to="/">首页</NuxtLink></a-breadcrumb-item>
<a-breadcrumb-item><NuxtLink to="/expert">专家资讯</NuxtLink></a-breadcrumb-item>
<a-breadcrumb-item><NuxtLink to="/expert">专家</NuxtLink></a-breadcrumb-item>
<a-breadcrumb-item>专家详情</a-breadcrumb-item>
</a-breadcrumb>
@@ -12,9 +11,12 @@
<a-skeleton active avatar :paragraph="{ rows: 6 }" />
</div>
<div v-else-if="!expert.expertId">
<a-result status="404" title="专家不存在" sub-title="您查找的专家不存在或尚未通过审核" />
</div>
<div v-else>
<a-row :gutter="[32, 24]">
<!-- 专家信息卡片 -->
<a-col :xs="24" :lg="7">
<div class="expert-card">
<div class="expert-avatar-wrapper">
@@ -22,8 +24,8 @@
<div v-else class="expert-avatar-placeholder">{{ expert.name?.charAt(0) }}</div>
</div>
<h2 class="expert-name">{{ expert.name }}</h2>
<div class="expert-title-tag">{{ expert.title }}</div>
<div class="expert-org">{{ expert.organization }}</div>
<div class="expert-title-tag">{{ expert.title || '未填写职称/职务' }}</div>
<div class="expert-org">{{ expert.organization || '未填写工作单位' }}</div>
<div class="expert-info-list">
<div class="info-item" v-if="expert.researchArea">
@@ -46,34 +48,29 @@
</div>
</a-col>
<!-- 专家详情 -->
<a-col :xs="24" :lg="17">
<div class="expert-content-card">
<a-tabs v-model:activeKey="activeTab">
<a-tabs v-model:active-key="activeTab">
<a-tab-pane key="intro" tab="专家简介">
<div class="tab-content">
<h3 class="section-title">个人简介</h3>
<p class="intro-text">{{ expert.introduction || '暂无简介' }}</p>
<h3 class="section-title mt-6">主要成就</h3>
<ul class="achievement-list">
<li v-for="(item, idx) in expert.achievements" :key="idx">{{ item }}</li>
</ul>
<h3 class="section-title mt-6">荣誉奖项</h3>
<div class="honors-grid">
<div v-if="expert.honors?.length" class="honors-grid">
<div v-for="(honor, idx) in expert.honors" :key="idx" class="honor-item">
<span class="honor-icon">🏆</span>
<span>{{ honor }}</span>
</div>
</div>
<a-empty v-else description="暂无荣誉奖项" />
</div>
</a-tab-pane>
<a-tab-pane key="articles" tab="专家文章">
<div class="tab-content">
<div v-if="expertArticles.length === 0" class="empty-state">
<a-empty description="暂无文章" />
<a-empty description="暂无相关文章" />
</div>
<div class="article-list">
<div
@@ -97,15 +94,20 @@
<a-tab-pane key="research" tab="研究成果">
<div class="tab-content">
<div v-if="expert.researchResults && expert.researchResults.length">
<div v-if="expert.researchResults?.length">
<div v-for="(result, idx) in expert.researchResults" :key="idx" class="research-item">
<span class="research-year">{{ result.year }}</span>
<span class="research-year">{{ result.year || '-' }}</span>
<div class="research-content">
<h4>{{ result.title }}</h4>
<p>{{ result.description }}</p>
<h4>{{ result.title || '未命名成果' }}</h4>
<p>{{ result.description || '暂无描述' }}</p>
</div>
</div>
</div>
<div v-else-if="achievementFiles.length" class="attachment-list">
<a v-for="(file, idx) in achievementFiles" :key="idx" :href="file" target="_blank" rel="noreferrer">
📎 {{ fileNameOf(file) }}
</a>
</div>
<a-empty v-else description="暂无研究成果" />
</div>
</a-tab-pane>
@@ -120,6 +122,7 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { getSiteExpert, pageSiteArticles, type SiteExpert } from '@/api/site'
const route = useRoute()
const router = useRouter()
@@ -127,75 +130,46 @@ const expertId = computed(() => route.params.id as string)
const loading = ref(true)
const activeTab = ref('intro')
const expert = ref<any>({})
const expertArticles = ref<any[]>([])
const expert = ref<SiteExpert | Record<string, never>>({})
const expertArticles = ref<Array<{ id: number; title: string; overview: string; date: string; image?: string }>>([])
const achievementFiles = computed(() => {
const value = (expert.value as SiteExpert).achievements
return Array.isArray(value) ? value : []
})
useHead({
title: computed(() => `${expert.value?.name || '专家详情'} - 决策咨询网`),
title: computed(() => `${(expert.value as SiteExpert)?.name || '专家详情'} - 决策咨询网`),
})
function fileNameOf(url: string) {
const parts = url.split('/')
return decodeURIComponent(parts[parts.length - 1] || url)
}
async function loadExpert() {
loading.value = true
try {
// TODO: 接入实际API
expert.value = {
id: expertId.value,
name: '张教授',
avatar: `https://picsum.photos/200/200?random=${expertId.value}`,
title: '研究员/教授',
organization: '广西社会科学院',
researchArea: '区域经济、数字经济、产业政策',
education: '经济学博士',
joinTime: '2022-06-15',
introduction: '张教授长期从事区域经济和产业政策研究,主持多项国家级和省部级科研项目,发表学术论文80余篇,著有《广西经济发展战略研究》等专著,是广西省级决策咨询委员会专家委员。',
achievements: [
'主持国家社科基金重点项目"面向东盟的广西产业协同发展研究"',
'参与编制《广西"十四五"经济发展规划》',
'提出"广西向海经济发展战略"并获省政府采纳',
'为南宁、柳州、桂林等城市提供产业规划咨询服务',
],
honors: [
'广西优秀专家',
'省级社科研究成果一等奖',
'国务院政府特殊津贴享受者',
'广西"十百千"人才',
],
researchResults: [
{
year: '2024',
title: '广西数字经济与传统产业融合研究',
description: '发表于《经济学研究》,探讨数字技术赋能广西传统制造业转型升级路径。'
},
{
year: '2023',
title: '面向东盟的广西跨境产业协同模式研究',
description: '国家社科基金资助项目结项报告,构建"中国-东盟产业链合作新模式"理论框架。'
},
{
year: '2022',
title: '西部陆海新通道经济带产业布局优化',
description: '广西社科规划重点课题,提出沿线城市产业差异化发展建议。'
},
]
}
expertArticles.value = [
{
id: 1,
title: '张教授:关于广西产业升级的几点建议',
overview: '从经济发展规律角度分析广西产业转型升级面临的机遇与挑战,提出针对性建议...',
date: '2024-12-10',
image: 'https://picsum.photos/120/80?random=1'
},
{
id: 2,
title: '数字经济时代广西制造业高质量发展路径探析',
overview: '以数字化转型为切入点,系统梳理广西制造业现状,提出差异化发展策略...',
date: '2024-10-28',
image: 'https://picsum.photos/120/80?random=2'
}
]
} catch (e: any) {
message.error('加载失败')
const data = await getSiteExpert(expertId.value)
expert.value = data
const articlePage = await pageSiteArticles({
page: 1,
limit: 6,
category: 'expert',
keywords: data.name,
})
expertArticles.value = (articlePage.list || []).map((item) => ({
id: item.articleId || item.id,
title: item.title,
overview: item.overview || '暂无摘要',
date: item.publishTime || item.createTime || '',
image: item.image,
}))
} catch (e: unknown) {
expert.value = {}
expertArticles.value = []
message.error(e instanceof Error ? e.message : '加载失败')
} finally {
loading.value = false
}
@@ -205,13 +179,17 @@ function handleConsult() {
message.info('请先联系我们预约咨询服务')
}
function goArticle(item: any) {
function goArticle(item: { id: number }) {
router.push(`/article/${item.id}`)
}
onMounted(() => {
loadExpert()
})
watch(expertId, () => {
loadExpert()
})
</script>
<style scoped>
@@ -258,112 +236,87 @@ onMounted(() => {
}
.expert-name {
font-size: 24px;
margin: 0 0 8px;
font-size: 28px;
font-weight: 700;
color: #1f2937;
margin: 0 0 8px;
}
.expert-title-tag {
display: inline-block;
padding: 4px 16px;
padding: 6px 16px;
background: #eff6ff;
color: #1e40af;
font-size: 13px;
border-radius: 20px;
margin-bottom: 8px;
color: #2563eb;
border-radius: 999px;
font-size: 14px;
margin-bottom: 10px;
}
.expert-org {
font-size: 14px;
color: #6b7280;
margin-bottom: 20px;
font-size: 15px;
}
.expert-info-list {
margin-top: 24px;
display: flex;
flex-direction: column;
gap: 12px;
text-align: left;
border-top: 1px solid #f0f0f0;
padding-top: 16px;
}
.info-item {
display: flex;
gap: 8px;
padding: 8px 0;
border-bottom: 1px dashed #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
justify-content: space-between;
gap: 12px;
font-size: 14px;
}
.info-label {
font-size: 12px;
color: #9ca3af;
width: 65px;
flex-shrink: 0;
color: #64748b;
}
.info-value {
font-size: 13px;
color: #374151;
flex: 1;
color: #1f2937;
text-align: right;
}
.expert-content-card {
background: #fff;
border-radius: 16px;
padding: 24px;
padding: 28px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.tab-content {
padding: 8px 0;
}
.section-title {
font-size: 16px;
font-weight: 700;
color: #1e3a5f;
margin: 0 0 12px;
padding-left: 10px;
border-left: 3px solid #1e3a5f;
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0 0 14px;
}
.intro-text {
font-size: 15px;
color: #4b5563;
line-height: 2;
text-indent: 2em;
}
.achievement-list {
padding-left: 20px;
}
.achievement-list li {
font-size: 14px;
color: #4b5563;
line-height: 2;
margin: 4px 0;
color: #475569;
line-height: 1.9;
margin: 0;
}
.honors-grid {
display: flex;
flex-wrap: wrap;
display: grid;
gap: 12px;
}
.honor-item {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #fffbeb;
border: 1px solid #fef3c7;
border-radius: 8px;
font-size: 13px;
color: #92400e;
gap: 10px;
padding: 12px 14px;
background: #f8fafc;
border-radius: 12px;
}
.honor-icon {
font-size: 18px;
}
.article-list {
@@ -375,30 +328,14 @@ onMounted(() => {
.article-item {
display: flex;
gap: 16px;
padding: 16px;
border-radius: 10px;
background: #f9fafb;
cursor: pointer;
transition: all 0.2s;
}
.article-item:hover {
background: #eff6ff;
transform: translateX(4px);
}
.article-thumb {
width: 100px;
height: 68px;
border-radius: 6px;
overflow: hidden;
flex-shrink: 0;
}
.article-thumb img {
width: 100%;
height: 100%;
width: 120px;
height: 80px;
object-fit: cover;
border-radius: 10px;
}
.article-info {
@@ -406,72 +343,56 @@ onMounted(() => {
}
.article-title {
font-size: 15px;
font-weight: 600;
margin: 0 0 8px;
color: #1f2937;
margin: 0 0 6px;
line-height: 1.4;
font-size: 16px;
}
.article-overview {
font-size: 13px;
color: #6b7280;
margin: 0 0 8px;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
color: #6b7280;
line-height: 1.7;
}
.article-date {
color: #94a3b8;
font-size: 12px;
color: #9ca3af;
}
.research-item {
display: flex;
display: grid;
grid-template-columns: 90px 1fr;
gap: 16px;
padding: 16px 0;
border-bottom: 1px dashed #f0f0f0;
}
.research-item:last-child {
border-bottom: none;
padding: 14px 0;
border-bottom: 1px dashed #e5e7eb;
}
.research-year {
display: flex;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
background: #1e3a5f;
color: #fff;
border-radius: 50%;
font-size: 14px;
color: #2563eb;
font-weight: 700;
flex-shrink: 0;
}
.research-content h4 {
font-size: 15px;
font-weight: 600;
margin: 0 0 8px;
color: #1f2937;
margin: 0 0 6px;
}
.research-content p {
font-size: 13px;
color: #6b7280;
margin: 0;
line-height: 1.6;
color: #6b7280;
line-height: 1.8;
}
.attachment-list {
display: grid;
gap: 10px;
}
.attachment-list a {
color: #2563eb;
}
.empty-state {
padding: 40px 0;
padding: 28px 0;
}
.mt-4 { margin-top: 16px; }
.mt-6 { margin-top: 24px; }
</style>
+435 -86
View File
@@ -1,103 +1,274 @@
<template>
<div class="expert-apply-page">
<div class="page-header">
<h1 class="page-title">专家申请</h1>
<p class="page-desc">成为平台认证专家展示研究成果分享专业观点</p>
<h1 class="page-title">专家申请备案</h1>
<p class="page-desc">请按备案表要求填写个人信息并上传证明材料</p>
</div>
<div class="apply-form-wrap">
<a-steps :current="currentStep" class="steps-wrap">
<a-step title="填写信息" />
<a-step title="上传资料" />
<a-step title="提交审核" />
</a-steps>
<div class="expert-apply-layout">
<ExpertNavSidebar />
<div class="apply-form-wrap">
<a-steps :current="currentStep" class="steps-wrap">
<a-step title="填写信息" />
<a-step title="上传材料" />
<a-step title="确认提交" />
</a-steps>
<a-form :model="formData" layout="vertical" class="apply-form">
<div v-show="currentStep === 0">
<h3 class="section-title">备案信息</h3>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="姓名" required>
<a-input v-model:value="formData.name" placeholder="请输入姓名" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="出生日期">
<a-date-picker v-model:value="formData.birthDate" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="性别">
<a-select v-model:value="formData.gender" placeholder="请选择">
<a-select-option value="男"></a-select-option>
<a-select-option value="女"></a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form
:model="formData"
:rules="rules"
layout="vertical"
class="apply-form"
>
<!-- 基本信息 -->
<div v-show="currentStep === 0">
<h3 class="section-title">基本信息</h3>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="姓名" name="name">
<a-input v-model:value="formData.name" placeholder="请输入您的姓名" />
<a-col :span="8">
<a-form-item label="民族">
<a-select v-model:value="formData.ethnicity" placeholder="请选择民族">
<a-select-option v-for="item in ethnicityOptions" :key="item" :value="item">
{{ item }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="职称/职务" name="title">
<a-input v-model:value="formData.title" placeholder="如:教授、研究员" />
<a-col :span="8">
<a-form-item label="政治面貌">
<a-select v-model:value="formData.politicalStatus" placeholder="请选择政治面貌">
<a-select-option v-for="item in politicalStatusOptions" :key="item" :value="item">
{{ item }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="国籍">
<a-select v-model:value="formData.nationality" placeholder="请选择国籍">
<a-select-option v-for="item in nationalityOptions" :key="item" :value="item">
{{ item }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="工作单位" name="organization">
<a-input v-model:value="formData.organization" placeholder="请输入您的工作单位" />
<a-col :span="8">
<a-form-item label="文化程度">
<a-select v-model:value="formData.education" placeholder="请选择文化程度">
<a-select-option v-for="item in educationOptions" :key="item" :value="item">
{{ item }}
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="研究领域" name="researchArea">
<a-input v-model:value="formData.researchArea" placeholder="请输入您的研究领域" />
<a-col :span="8">
<a-form-item label="社团职务">
<a-input v-model:value="formData.societyPosition" placeholder="请输入社团职务" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="社团专兼职">
<a-radio-group v-model:value="formData.societyEmploymentType">
<a-radio :value="0">专职</a-radio>
<a-radio :value="1">兼职</a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
<a-col :span="8">
<a-form-item label="职称">
<a-input v-model:value="formData.title" placeholder="请输入职称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="联系电话" name="phone">
<a-col :span="8">
<a-form-item label="身份证号">
<a-input v-model:value="formData.idCardNumber" placeholder="请输入身份证号" />
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="联系电话" required>
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="个人简介" name="bio">
<a-textarea v-model:value="formData.bio" :rows="4" placeholder="请简要介绍您的学术背景和工作经历" />
<a-form-item label="住址">
<a-input v-model:value="formData.address" placeholder="请输入住址" />
</a-form-item>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="工作单位">
<a-input v-model:value="formData.organization" placeholder="请输入工作单位" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工作职务">
<a-input v-model:value="formData.workDuty" placeholder="请输入工作职务" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="其他社会职务">
<a-input v-model:value="formData.otherSocialPosition" placeholder="请输入其他社会职务" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="研究领域">
<a-input v-model:value="formData.researchArea" placeholder="请输入研究领域" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="8">
<a-form-item label="担任其他社团职务">
<a-radio-group v-model:value="formData.hasOtherSocietyPosition">
<a-radio :value="0"></a-radio>
<a-radio :value="1"></a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="在职国家工作人员">
<a-radio-group v-model:value="formData.isPublicServant">
<a-radio :value="0"></a-radio>
<a-radio :value="1"></a-radio>
</a-radio-group>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="离退休干部">
<a-radio-group v-model:value="formData.isRetired">
<a-radio :value="0"></a-radio>
<a-radio :value="1"></a-radio>
</a-radio-group>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="工作单位类型">
<a-select v-model:value="formData.unitType" placeholder="请选择">
<a-select-option value="行政机关">行政机关</a-select-option>
<a-select-option value="事业单位">事业单位</a-select-option>
<a-select-option value="国有企业">国有企业</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="工作单位级别">
<a-select v-model:value="formData.unitLevel" placeholder="请选择">
<a-select-option value="省部级及以上">省部级及以上</a-select-option>
<a-select-option value="地厅级">地厅级</a-select-option>
<a-select-option value="县处级">县处级</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="邮箱">
<a-input v-model:value="formData.email" placeholder="选填,便于联系" />
</a-form-item>
<a-form-item label="主要工作简历">
<div class="experience-list">
<div v-for="(item, index) in formData.workExperience" :key="index" class="experience-row">
<a-row :gutter="12">
<a-col :span="8">
<a-input v-model:value="item.period" placeholder="何年月至何年月" />
</a-col>
<a-col :span="14">
<a-input v-model:value="item.organization" placeholder="在何地何单位" />
</a-col>
<a-col :span="2">
<a-button danger @click="removeWorkExperience(index)">删除</a-button>
</a-col>
</a-row>
</div>
<a-button block @click="addWorkExperience">新增一行</a-button>
</div>
</a-form-item>
<a-form-item label="主要研究成果">
<a-textarea
v-model:value="formData.mainResearchAchievements"
:rows="6"
placeholder="请输入主要研究成果"
/>
</a-form-item>
<a-form-item label="个人简介">
<a-textarea v-model:value="formData.bio" :rows="4" placeholder="可补充个人简介" />
</a-form-item>
</div>
<!-- 上传资料 -->
<div v-show="currentStep === 1">
<h3 class="section-title">资质证明材料</h3>
<p class="section-desc">上传相关证明材料以便我们审核您的专家资质</p>
<h3 class="section-title">附件材料</h3>
<p class="section-desc">按备案表要求上传申请表及相关证明材料</p>
<a-form-item label="个人简历">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('resume')">
<a-button><UploadOutlined /> 上传简历</a-button>
<div class="materials-tip">
<div>附件身份证学位证职称证书研究成果证明课题合同结题证书获奖证书其他证明</div>
<a-button type="link" @click="navigateTo('/expert/downloads')">前往资料下载</a-button>
</div>
<a-form-item label="专家申请备案表">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('applicationForm')">
<a-button><UploadOutlined /> 上传备案表</a-button>
</a-upload>
<div class="upload-hint">支持 PDFWord 格式不超过 10MB</div>
</a-form-item>
<a-form-item label="职称/学历证明">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('certificate')">
<a-button><UploadOutlined /> 上传证</a-button>
<a-form-item label="身份证">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('idCard')">
<a-button><UploadOutlined /> 上传身份</a-button>
</a-upload>
<div class="upload-hint">支持 JPGPNGPDF 格式</div>
</a-form-item>
<a-form-item label="研究成果或获奖证明">
<a-form-item label="学位证">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('degreeCertificate')">
<a-button><UploadOutlined /> 上传学位证</a-button>
</a-upload>
</a-form-item>
<a-form-item label="职称证书">
<a-upload :before-upload="beforeUpload" :custom-request="handleUpload('titleCertificate')">
<a-button><UploadOutlined /> 上传职称证书</a-button>
</a-upload>
</a-form-item>
<a-form-item label="研究成果证明">
<a-upload multiple :before-upload="beforeUpload" :custom-request="handleUpload('achievements')">
<a-button><UploadOutlined /> 上传材料</a-button>
<a-button><UploadOutlined /> 上传证明材料</a-button>
</a-upload>
<div class="upload-hint">可上传多份材料</div>
<div class="upload-hint">可上传多份材料支持 PDFWord图片格式不超过 10MB</div>
</a-form-item>
</div>
<!-- 确认提交 -->
<div v-show="currentStep === 2" class="confirm-section">
<a-result
title="确认提交申请"
sub-title="请确认您填写的信息和上传的材料准确无误"
>
<a-result title="确认提交申请" sub-title="请确认信息和材料准确无误">
<template #icon>
<CheckCircleOutlined style="font-size: 80px; color: #52c41a" />
</template>
@@ -109,43 +280,166 @@
</a-result>
</div>
<!-- 步骤按钮 -->
<div class="step-actions">
<a-button v-if="currentStep > 0" @click="currentStep--">一步</a-button>
<a-button v-if="currentStep < 2" type="primary" @click="handleNext">下一步</a-button>
</div>
</a-form>
<div class="step-actions">
<a-button v-if="currentStep > 0" @click="currentStep--">上一步</a-button>
<a-button v-if="currentStep < 2" type="primary" @click="handleNext">一步</a-button>
</div>
</a-form>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import dayjs, { type Dayjs } from 'dayjs'
import { message } from 'ant-design-vue'
import { CheckCircleOutlined, UploadOutlined } from '@ant-design/icons-vue'
import ExpertNavSidebar from '@/components/ExpertNavSidebar.vue'
import { submitSiteExpertApplication, uploadSiteExpertFile } from '@/api/site'
useHead({ title: '专家申请 - 决策咨询网' })
useHead({ title: '专家申请备案 - 决策咨询网' })
type UploadRequestOption = {
file: File
onSuccess: (response?: unknown) => void
onError: (error?: unknown) => void
}
type WorkExperienceItem = {
period: string
organization: string
}
const currentStep = ref(0)
const submitting = ref(false)
const ethnicityOptions = [
'汉族',
'壮族',
'满族',
'回族',
'苗族',
'维吾尔族',
'土家族',
'彝族',
'蒙古族',
'藏族',
'布依族',
'侗族',
'瑶族',
'朝鲜族',
'白族',
'哈尼族',
'哈萨克族',
'黎族',
'傣族',
'畲族',
'傈僳族',
'仡佬族',
'东乡族',
'高山族',
'拉祜族',
'水族',
'佤族',
'纳西族',
'羌族',
'土族',
'仫佬族',
'锡伯族',
'柯尔克孜族',
'达斡尔族',
'景颇族',
'毛南族',
'撒拉族',
'布朗族',
'塔吉克族',
'阿昌族',
'普米族',
'鄂温克族',
'怒族',
'京族',
'基诺族',
'德昂族',
'保安族',
'俄罗斯族',
'裕固族',
'乌孜别克族',
'门巴族',
'鄂伦春族',
'独龙族',
'塔塔尔族',
'赫哲族',
'珞巴族'
]
const politicalStatusOptions = [
'中共党员',
'中共预备党员',
'共青团员',
'民革党员',
'民盟盟员',
'民建会员',
'民进会员',
'农工党党员',
'致公党党员',
'九三学社社员',
'台盟盟员',
'无党派人士',
'群众'
]
const educationOptions = [
'高中及以下',
'中专',
'大专',
'本科',
'硕士研究生',
'博士研究生',
'博士后'
]
const nationalityOptions = [
'中国',
'中国香港',
'中国澳门',
'中国台湾',
'其他'
]
const formData = reactive({
name: '',
birthDate: undefined as Dayjs | undefined,
gender: '',
ethnicity: ethnicityOptions[0],
politicalStatus: '',
nationality: nationalityOptions[0],
education: '',
societyPosition: '',
societyEmploymentType: 0,
title: '',
idCardNumber: '',
address: '',
phone: '',
organization: '',
workDuty: '',
otherSocialPosition: '',
hasOtherSocietyPosition: 0,
isPublicServant: 0,
unitType: '',
unitLevel: '',
isRetired: 0,
researchArea: '',
email: '',
phone: '',
bio: '',
resume: '',
certificate: '',
workExperience: [{ period: '', organization: '' }] as WorkExperienceItem[],
mainResearchAchievements: '',
applicationForm: '',
idCard: '',
degreeCertificate: '',
titleCertificate: '',
achievements: [] as string[],
})
const rules = {
name: [{ required: true, message: '请输入姓名' }],
email: [{ required: true, type: 'email', message: '请输入正确的邮箱' }],
}
function beforeUpload(file: File) {
const isLt10M = file.size / 1024 / 1024 < 10
if (!isLt10M) {
@@ -155,28 +449,42 @@ function beforeUpload(file: File) {
return true
}
function addWorkExperience() {
formData.workExperience.push({ period: '', organization: '' })
}
function removeWorkExperience(index: number) {
if (formData.workExperience.length === 1) {
formData.workExperience[0] = { period: '', organization: '' }
return
}
formData.workExperience.splice(index, 1)
}
function handleUpload(type: string) {
return async (option: any) => {
return async (option: UploadRequestOption) => {
try {
const form = new FormData()
form.append('file', option.file)
// TODO: 调用上传API
// const res = await uploadFile(form)
// if (type === 'resume') formData.resume = res.url
// if (type === 'certificate') formData.certificate = res.url
option.onSuccess()
const data = await uploadSiteExpertFile(form)
if (type === 'achievements') {
formData.achievements.push(data.path)
} else {
formData[type as Exclude<keyof typeof formData, 'achievements' | 'workExperience' | 'birthDate'>] = data.path as never
}
option.onSuccess(data)
message.success('上传成功')
} catch (e) {
option.onError()
message.error('上传失败')
} catch (e: unknown) {
option.onError(e)
message.error(e instanceof Error ? e.message : '上传失败')
}
}
}
function handleNext() {
if (currentStep.value === 0) {
if (!formData.name || !formData.email) {
message.warning('请填写必填项')
if (!formData.name.trim() || !formData.phone.trim()) {
message.warning('请至少填写姓名和联系电话')
return
}
}
@@ -186,12 +494,15 @@ function handleNext() {
async function handleSubmit() {
submitting.value = true
try {
// TODO: 调用API提交申请
// await submitExpertApplication(formData)
await submitSiteExpertApplication({
...formData,
birthDate: formData.birthDate ? dayjs(formData.birthDate).format('YYYY-MM-DD') : '',
workExperience: formData.workExperience.filter((item) => item.period || item.organization),
})
message.success('提交成功,请等待审核')
navigateTo('/expert')
} catch (e: any) {
message.error(e?.message || '提交失败')
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '提交失败')
} finally {
submitting.value = false
}
@@ -200,7 +511,7 @@ async function handleSubmit() {
<style scoped>
.expert-apply-page {
max-width: 800px;
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
@@ -223,7 +534,15 @@ async function handleSubmit() {
margin: 0;
}
.expert-apply-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 28px;
align-items: start;
}
.apply-form-wrap {
min-width: 0;
background: #fff;
border-radius: 16px;
padding: 40px;
@@ -247,6 +566,30 @@ async function handleSubmit() {
margin: -10px 0 20px;
}
.materials-tip {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
padding: 14px 16px;
background: #f8fafc;
border: 1px solid #dbeafe;
border-radius: 12px;
color: #475569;
}
.experience-list {
display: grid;
gap: 12px;
}
.experience-row {
padding: 12px 14px;
background: #f8fafc;
border-radius: 12px;
}
.upload-hint {
font-size: 12px;
color: #9ca3af;
@@ -265,4 +608,10 @@ async function handleSubmit() {
padding-top: 24px;
border-top: 1px solid #f0f0f0;
}
@media (max-width: 1024px) {
.expert-apply-layout {
grid-template-columns: 1fr;
}
}
</style>
+17
View File
@@ -0,0 +1,17 @@
<template>
<ModuleEntryDetailPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryDetailPage from '@/components/ModuleEntryDetailPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
title: '专家资料下载',
baseRoute: '/expert/downloads',
showImage: false,
}
</script>
+20
View File
@@ -0,0 +1,20 @@
<template>
<ModuleEntryListPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryListPage from '@/components/ModuleEntryListPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
type: 'expert_download',
title: '专家资料下载',
description: '',
baseRoute: '/expert/downloads',
showImage: false,
sidebarNavKey: 'expert',
}
</script>
+475 -108
View File
@@ -1,99 +1,267 @@
<template>
<div class="expert-page">
<div class="page-header">
<h1 class="page-title">专家资讯</h1>
<p class="page-desc">汇聚各领域权威专家提供专业视角与研究成果</p>
<h1 class="page-title">{{ currentTitle }}</h1>
<p class="page-desc">{{ currentDesc }}</p>
</div>
<!-- 分类标签 -->
<div class="category-tabs">
<a-radio-group v-model:value="activeType" button-style="solid" @change="handleTypeChange">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="view">专家视点</a-radio-button>
<a-radio-button value="dynamic">专家动态</a-radio-button>
</a-radio-group>
</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>
<!-- 专家/文章列表 -->
<div class="expert-list">
<div v-for="item in items" :key="item.id" class="expert-item" @click="handleView(item)">
<div class="expert-avatar" v-if="item.avatar">
<img :src="item.avatar" :alt="item.expertName" />
<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 class="expert-default-avatar" v-else>{{ item.expertName?.charAt(0) }}</div>
<div class="expert-content">
<h3 class="expert-title">{{ item.title }}</h3>
<div class="expert-meta">
<span class="meta-item">{{ item.expertName }}</span>
<span class="meta-item">{{ item.expertTitle }}</span>
<span class="meta-item">{{ item.publishTime }}</span>
<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>
<p class="expert-overview">{{ item.overview }}</p>
</div>
</div>
<div v-if="loading" class="loading-placeholder">
<a-spin size="large" />
</div>
<div v-if="!loading && items.length === 0" 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 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'
useHead({ title: '专家资讯 - 决策咨询网' })
import { mainNav } from '@/config/nav'
import { pageSiteArticles, pageSiteExperts, resolveSiteAssetUrl, type SiteArticle, type SiteExpert } from '@/api/site'
const router = useRouter()
const activeType = ref((useRoute().query.type as string) || '')
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 items = ref<any[]>([])
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
}
async function loadItems() {
loading.value = true
try {
// TODO: 接入实际API
} catch (e: any) {
message.error('加载失败')
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 handleTypeChange() {
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
loadItems()
router.replace({
query: {
...(type.value ? { type: type.value } : {}),
...(keywords.value ? { keyword: keywords.value } : {}),
},
})
loadCurrentData()
}
function handlePageChange(page: number) {
currentPage.value = page
loadItems()
loadCurrentData()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function handleView(item: any) {
router.push(`/expert/${item.id}`)
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(() => {
loadItems()
loadCurrentData()
})
</script>
@@ -106,7 +274,7 @@ onMounted(() => {
.page-header {
text-align: center;
margin-bottom: 40px;
margin-bottom: 32px;
}
.page-title {
@@ -122,92 +290,291 @@ onMounted(() => {
margin: 0;
}
.category-tabs {
margin-bottom: 32px;
text-align: center;
.expert-layout {
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
gap: 28px;
align-items: start;
}
.expert-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.expert-item {
display: flex;
gap: 20px;
padding: 20px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
cursor: pointer;
transition: all 0.2s;
}
.expert-item:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transform: translateY(-2px);
}
.expert-avatar,
.expert-default-avatar {
width: 80px;
height: 80px;
border-radius: 50%;
flex-shrink: 0;
.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);
}
.expert-avatar img {
.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;
}
.expert-default-avatar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-size: 32px;
.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-content {
flex: 1;
.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 {
font-size: 18px;
color: #2563eb;
font-size: 14px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px;
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;
color: #6b7280;
margin-bottom: 8px;
}
.expert-overview {
font-size: 14px;
color: #6b7280;
margin: 0;
line-height: 1.6;
}
.loading-placeholder,
.empty-placeholder {
padding: 60px 0;
text-align: center;
grid-column: 1 / -1;
}
.pagination-wrap {
margin-top: 40px;
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>
+678 -162
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
<template>
<ModuleEntryDetailPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryDetailPage from '@/components/ModuleEntryDetailPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
title: '申报模板',
baseRoute: '/material-templates',
showImage: false,
}
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<ModuleEntryListPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryListPage from '@/components/ModuleEntryListPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
type: 'material_template',
title: '申报模板',
description: '浏览申报模板与申报说明材料',
baseRoute: '/material-templates',
showImage: false,
}
</script>
+247
View File
@@ -0,0 +1,247 @@
<template>
<div class="member-detail-page">
<PortalHeader active-path="/membership" />
<main class="container member-detail-main">
<div class="breadcrumb-bar">
<NuxtLink to="/">首页</NuxtLink>
<span>/</span>
<NuxtLink to="/membership">会员服务</NuxtLink>
<span>/</span>
<span>{{ service.title || "会员服务详情" }}</span>
</div>
<div v-if="loading" class="state-wrap">
<a-skeleton active :paragraph="{ rows: 10 }" />
</div>
<div v-else-if="!service.serviceId" class="state-wrap">
<a-result status="404" title="会员服务不存在" sub-title="您查找的会员服务不存在或已下线">
<template #extra>
<a-button type="primary" @click="navigateTo('/membership')">返回会员服务</a-button>
</template>
</a-result>
</div>
<article v-else class="member-detail-card">
<div class="member-head">
<div>
<div class="service-badge">{{ service.categoryName }}</div>
<h1>{{ service.title }}</h1>
<p>{{ service.summary || "会员服务详情" }}</p>
</div>
<span v-if="service.publishTime" class="join-time">发布时间{{ service.publishTime }}</span>
</div>
<div v-if="service.tags?.length" class="tag-list">
<span v-for="tag in service.tags" :key="tag">{{ tag }}</span>
</div>
<section class="content-section">
<h2>服务介绍</h2>
<div v-if="service.content" class="content-body" v-html="service.content" />
<div v-else class="content-body">
<p>{{ service.summary || "暂无详情。" }}</p>
</div>
</section>
</article>
</main>
<footer class="site-footer">
<PortalFooter />
</footer>
</div>
</template>
<script setup lang="ts">
import { message } from "ant-design-vue"
import { getSiteMembershipService, type SiteMembershipService } from "@/api/site"
import { usePageSeo } from "@/composables/usePageSeo"
definePageMeta({
layout: "blank"
})
usePageSeo({
title: "会员服务详情",
description: "广西决策咨询网会员服务详情页"
})
const route = useRoute()
const memberId = computed(() => route.params.id as string)
const loading = ref(true)
const service = ref<Partial<SiteMembershipService>>({})
useHead({
title: computed(() => `${service.value.title || "会员服务详情"} - 广西决策咨询网`),
meta: [
{ name: "description", content: computed(() => service.value.summary || "广西决策咨询网会员服务详情页") }
]
})
async function loadMember() {
loading.value = true
try {
service.value = await getSiteMembershipService(memberId.value)
} catch (error) {
service.value = {}
if (!(error instanceof Error && /404/.test(error.message))) {
message.error(error instanceof Error ? error.message : "加载失败")
}
} finally {
loading.value = false
}
}
watch(memberId, () => {
loadMember()
})
onMounted(() => {
loadMember()
})
</script>
<style scoped>
.member-detail-page {
min-height: 100vh;
background: #f5f7fa;
}
.container {
width: min(1200px, calc(100% - 32px));
margin: 0 auto;
}
.member-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;
}
.member-detail-card {
padding: 32px;
background: #fff;
border: 1px solid #e5ebf2;
}
.member-head {
display: flex;
align-items: start;
justify-content: space-between;
gap: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #edf1f5;
}
.service-badge {
display: inline-flex;
margin-bottom: 14px;
padding: 6px 14px;
border-radius: 999px;
background: #eef5ff;
color: #2d7af0;
font-size: 13px;
}
.member-head h1 {
margin: 0 0 10px;
color: #1f2d3d;
font-size: 34px;
line-height: 1.2;
}
.member-head p {
margin: 0;
color: #64707d;
font-size: 15px;
}
.join-time {
flex-shrink: 0;
color: #7d8a98;
font-size: 14px;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 24px 0 10px;
}
.tag-list span {
padding: 7px 14px;
border: 1px solid #9fc3ff;
border-radius: 12px;
color: #2d7af0;
font-size: 14px;
}
.content-section {
padding-top: 8px;
}
.content-section h2 {
margin: 0 0 16px;
color: #1f2d3d;
font-size: 22px;
}
.content-body {
color: #495463;
font-size: 15px;
line-height: 2;
}
.content-body :deep(p) {
margin: 0 0 16px;
}
.content-body :deep(*:last-child) {
margin-bottom: 0;
}
.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: 32px;
min-height: 58px;
}
.footer-nav-inner a {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
@media (max-width: 768px) {
.member-head {
flex-direction: column;
}
}
</style>
+249 -176
View File
@@ -1,149 +1,137 @@
<template>
<div class="membership-page">
<div class="page-header">
<h1 class="page-title">会员服务</h1>
<p class="page-desc">为企业会员和个人会员提供专业高效的咨询服务</p>
</div>
<PortalHeader active-path="/membership" />
<!-- 分类标签 -->
<div class="category-tabs">
<a-radio-group v-model:value="activeType" button-style="solid" @change="handleTypeChange">
<a-radio-button value="">全部</a-radio-button>
<a-radio-button value="consult">企业咨询</a-radio-button>
<a-radio-button value="service">专项服务</a-radio-button>
</a-radio-group>
</div>
<!-- 服务列表 -->
<div class="service-grid">
<div v-for="service in services" :key="service.id" class="service-card" @click="handleView(service)">
<div class="service-icon">{{ service.icon }}</div>
<h3 class="service-title">{{ service.title }}</h3>
<p class="service-desc">{{ service.description }}</p>
<div class="service-tags">
<a-tag v-for="tag in service.tags" :key="tag" color="blue">{{ tag }}</a-tag>
<main class="container membership-main">
<div class="page-head">
<div>
<h1>会员服务</h1>
<p>企业会员和个人会员提供专业高效的咨询服务</p>
</div>
<div class="page-summary">
<span>{{ total }} 项服务内容</span>
</div>
</div>
<div v-if="loading" class="loading-placeholder">
<a-spin size="large" />
<div class="filter-bar">
<button
v-for="item in tabs"
:key="item.key"
type="button"
class="filter-btn"
:class="{ active: activeTab === item.key }"
@click="handleTabChange(item.key)"
>
{{ item.label }}
</button>
</div>
<div v-if="!loading && services.length === 0" class="empty-placeholder">
<a-empty description="暂无服务" />
<div v-if="loading" class="state-wrap">
<a-skeleton v-for="i in 6" :key="i" active :paragraph="{ rows: 3 }" />
</div>
</div>
<!-- 联系我们 -->
<div class="contact-section">
<h2>联系我们</h2>
<p>如有疑问或需要帮助请随时与我们联系</p>
<a-space size="large" direction="vertical">
<a-space size="large">
<span>📞</span>
<span>联系电话0771-5386339</span>
</a-space>
<a-space size="large">
<span>📧</span>
<span>咨询邮箱gxjzxzx@126.com</span>
</a-space>
<a-space size="large">
<span></span>
<span>服务时间周一至周五 9:00-17:00</span>
</a-space>
</a-space>
<div style="margin-top: 20px;">
<a-button type="primary" size="large" @click="navigateTo('/about/consultation')">
了解咨询服务详情
</a-button>
<div v-else-if="!services.length" class="state-wrap">
<a-empty description="暂无会员服务" />
</div>
</div>
<div v-else class="service-grid">
<article
v-for="item in services"
:key="item.serviceId || item.id"
class="service-card"
@click="goDetail(item)"
>
<div class="service-card-icon">{{ item.icon || "•" }}</div>
<div class="service-card-top">
<h3>{{ item.title }}</h3>
<span>{{ item.categoryName }}</span>
</div>
<p>{{ item.summary || "暂无简介" }}</p>
<div v-if="item.tags?.length" class="service-card-tags">
<span v-for="tag in item.tags" :key="tag">{{ tag }}</span>
</div>
</article>
</div>
</main>
<footer class="site-footer">
<PortalFooter />
</footer>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { message } from "ant-design-vue"
import { pageSiteMembershipServices, type SiteMembershipService } from "@/api/site"
import { usePageSeo } from "@/composables/usePageSeo"
useHead({ title: '会员服务 - 决策咨询网' })
definePageMeta({
layout: "blank"
})
const activeType = ref((useRoute().query.type as string) || '')
const loading = ref(false)
const services = ref<any[]>([])
usePageSeo({
title: "会员服务",
description: "广西决策咨询网会员服务"
})
const mockServices = [
{
id: 1,
type: 'consult',
icon: '🏢',
title: '企业决策咨询',
description: '为企业提供战略规划、政策解读、市场分析等专业决策咨询服务,助力企业把握发展机遇。',
tags: ['企业咨询', '战略规划'],
},
{
id: 2,
type: 'service',
icon: '📊',
title: '专题研究报告',
description: '提供行业专题研究、政策分析报告、区域发展研究等专业研究成果。',
tags: ['研究报告', '深度分析'],
},
{
id: 3,
type: 'consult',
icon: '🎯',
title: '政策合规指导',
description: '协助企业理解最新政策法规,确保企业运营符合政策要求,规避合规风险。',
tags: ['政策合规', '风险规避'],
},
{
id: 4,
type: 'service',
icon: '📋',
title: '专家论证会',
description: '组织相关领域专家为企业重大决策提供专业论证和咨询建议。',
tags: ['专家论证', '专业咨询'],
},
{
id: 5,
type: 'service',
icon: '🌐',
title: '数据服务',
description: '提供决策所需的经济数据、行业数据、区域数据等专业数据服务(仅限会员)。',
tags: ['数据服务', '会员专享'],
},
{
id: 6,
type: 'consult',
icon: '💼',
title: '培训与讲座',
description: '为企业及个人提供政策解读、决策方法等专题培训和讲座服务。',
tags: ['培训讲座', '能力提升'],
},
const route = useRoute()
const router = useRouter()
const tabs = [
{ key: "all", label: "全部" },
{ key: "consult", label: "企业咨询" },
{ key: "service", label: "专项服务" },
]
const total = ref(0)
const loading = ref(false)
const services = ref<SiteMembershipService[]>([])
const activeTab = ref(((route.query.type as string) || "all"))
async function loadServices() {
loading.value = true
try {
// TODO: 接入实际API获取会员服务列表
// 暂时使用模拟数据
await new Promise(resolve => setTimeout(resolve, 300))
const type = activeType.value
services.value = type ? mockServices.filter(s => s.type === type) : mockServices
} catch (e: any) {
message.error('加载失败')
const data = await pageSiteMembershipServices({
page: 1,
limit: 60,
categoryKey: activeTab.value === "all" ? undefined : activeTab.value,
})
total.value = data.count || 0
services.value = data.list || []
} catch (error) {
services.value = []
total.value = 0
message.error(error instanceof Error ? error.message : "加载会员服务失败")
} finally {
loading.value = false
}
}
function handleTypeChange() {
function updateRoute(type: string) {
router.replace({
query: {
...(type !== "all" ? { type } : {}),
}
})
}
function handleTabChange(type: string) {
if (activeTab.value === type) {
return
}
activeTab.value = type
updateRoute(type)
loadServices()
}
function handleView(service: any) {
message.info(`服务「${service.title}」详情页开发中,请联系工作人员获取更多信息`)
function goDetail(item: SiteMembershipService) {
router.push(`/membership/${item.serviceId || item.id}`)
}
watch(() => route.query.type, (value) => {
activeTab.value = (value as string) || "all"
loadServices()
})
onMounted(() => {
loadServices()
})
@@ -151,116 +139,201 @@ onMounted(() => {
<style scoped>
.membership-page {
max-width: 1200px;
min-height: 100vh;
background: #f5f7fa;
}
.container {
width: min(1200px, calc(100% - 32px));
margin: 0 auto;
padding: 40px 20px;
}
.page-header {
text-align: center;
margin-bottom: 40px;
.membership-main {
padding: 32px 0 48px;
}
.page-title {
font-size: 32px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px;
.page-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 24px;
margin-bottom: 22px;
}
.page-desc {
font-size: 16px;
color: #6b7280;
.page-head h1 {
margin: 0 0 10px;
color: #1f2d3d;
font-size: 34px;
line-height: 1.2;
}
.page-head p {
margin: 0;
color: #697586;
font-size: 15px;
}
.category-tabs {
margin-bottom: 32px;
text-align: center;
.page-summary {
color: #7d8a98;
font-size: 14px;
}
.filter-bar {
display: inline-flex;
margin-bottom: 36px;
border: 1px solid #d8dee7;
border-radius: 14px;
overflow: hidden;
}
.filter-btn {
min-width: 132px;
padding: 12px 20px;
border: none;
background: #fff;
color: #233245;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s ease, color 0.2s ease;
}
.filter-btn + .filter-btn {
border-left: 1px solid #d8dee7;
}
.filter-btn.active {
background: #2d7af0;
color: #fff;
}
.service-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
}
.service-card {
min-height: 240px;
padding: 34px 38px;
border: 1px solid #eef2f7;
border-radius: 22px;
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
cursor: pointer;
transition: all 0.2s;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
box-shadow: 0 18px 40px rgba(17, 34, 68, 0.06);
}
.service-card:hover {
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transform: translateY(-4px);
border-color: #cfe0ff;
box-shadow: 0 20px 42px rgba(36, 82, 156, 0.12);
transform: translateY(-2px);
}
.service-icon {
font-size: 48px;
margin-bottom: 16px;
.service-card-icon {
margin-bottom: 18px;
color: #4b6ea8;
font-size: 56px;
line-height: 1;
}
.service-title {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px;
.service-card-top {
display: flex;
align-items: start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.service-desc {
.service-card-top h3 {
margin: 0;
color: #1f2d3d;
font-size: 24px;
line-height: 1.4;
}
.service-card-top span {
flex-shrink: 0;
color: #7c8eab;
font-size: 14px;
color: #6b7280;
margin: 0 0 16px;
line-height: 1.6;
}
.service-tags {
.service-card p {
margin: 0 0 18px;
color: #66768a;
font-size: 16px;
line-height: 1.85;
}
.service-card-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: 12px;
}
.contact-section {
margin-top: 60px;
padding: 40px;
background: #f9fafb;
border-radius: 16px;
text-align: center;
}
.contact-section h2 {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px;
}
.contact-section p {
.service-card-tags span {
padding: 6px 14px;
border: 1px solid #99c1ff;
border-radius: 12px;
color: #2d7af0;
font-size: 14px;
color: #6b7280;
margin: 0 0 16px;
line-height: 1.2;
}
.loading-placeholder,
.empty-placeholder {
grid-column: 1 / -1;
padding: 60px 0;
text-align: center;
.state-wrap {
display: grid;
gap: 20px;
}
.site-footer {
margin-top: 24px;
}
.footer-nav {
background: #1f4fa3;
}
.footer-nav-inner {
display: flex;
align-items: center;
justify-content: center;
gap: 32px;
min-height: 58px;
}
.footer-nav-inner a {
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
}
@media (max-width: 1024px) {
.service-grid {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
@media (max-width: 768px) {
.membership-main {
padding-top: 24px;
}
.page-head {
flex-direction: column;
align-items: stretch;
}
.filter-bar {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.service-grid {
grid-template-columns: 1fr;
}
.service-card {
min-height: auto;
padding: 28px 22px;
}
}
</style>
+4 -2
View File
@@ -4,7 +4,8 @@
<script setup lang="ts">
definePageMeta({
layout: 'default'
layout: 'default',
hideLayoutHeader: true,
})
useHead({ title: '政策要闻 - 决策咨询网' })
@@ -14,12 +15,13 @@ const pageConfig = {
desc: '汇聚党中央国务院、自治区党委政府、各厅委办最新政策信息',
bannerGradient: 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)',
baseRoute: 'news',
useSiteHeader: true,
categories: [
{ type: '', label: '全部资讯' },
{ type: 'central', label: '党中央国务院信息' },
{ type: 'region', label: '自治区党委政府信息' },
{ type: 'department', label: '其他(厅委办)信息' },
{ type: 'latest', label: '最新发布' },
{ type: 'latest', label: '最新' },
]
}
</script>
+2 -2
View File
@@ -148,13 +148,13 @@
<div v-show="activeTab === 'suggestions'" class="content-panel">
<div class="panel-header">
<h3>我的建言</h3>
<a-button type="primary" @click="navigateTo('/suggestions')">提交新建言</a-button>
<a-button type="primary" @click="navigateTo('/suggestions/submit')">提交新建言</a-button>
</div>
<div v-if="mySuggestions.length === 0" class="empty-state">
<a-empty description="暂无建言记录">
<template #extra>
<a-button type="primary" @click="navigateTo('/suggestions')">立即建言</a-button>
<a-button type="primary" @click="navigateTo('/suggestions/submit')">立即建言</a-button>
</template>
</a-empty>
</div>
+252
View File
@@ -0,0 +1,252 @@
<template>
<div class="suggestion-detail-page">
<PortalHeader active-path="/suggestions" />
<main class="container suggestion-detail-main">
<div class="breadcrumb-bar">
<NuxtLink to="/">首页</NuxtLink>
<span>/</span>
<NuxtLink to="/suggestions">建言献策</NuxtLink>
<span>/</span>
<span>{{ suggestion.title || '建言详情' }}</span>
</div>
<div v-if="loading" class="state-wrap">
<a-skeleton active :paragraph="{ rows: 10 }" />
</div>
<div v-else-if="!suggestion.suggestionId" class="state-wrap">
<a-result status="404" title="建言不存在" sub-title="您查找的建言不存在或未公开显示">
<template #extra>
<a-button type="primary" @click="navigateTo('/suggestions')">返回列表</a-button>
</template>
</a-result>
</div>
<article v-else class="suggestion-detail-card">
<div class="detail-head">
<div>
<h1>{{ suggestion.title }}</h1>
<p>公开建言内容详情</p>
</div>
<div class="detail-side">
<a-tag :color="Number(suggestion.status) === 2 ? 'green' : 'blue'">
{{ Number(suggestion.status) === 2 ? '已采纳' : '已处理' }}
</a-tag>
<span v-if="suggestion.createTime">提交时间{{ suggestion.createTime }}</span>
<span v-if="suggestion.processedTime">处理时间{{ suggestion.processedTime }}</span>
</div>
</div>
<section class="content-section">
<h2>建言内容</h2>
<div class="content-body">
<p>{{ suggestion.content || '暂无内容。' }}</p>
</div>
</section>
<section v-if="suggestion.reply" class="content-section">
<h2>处理说明</h2>
<div class="reply-box">
{{ suggestion.reply }}
</div>
</section>
</article>
</main>
<footer class="site-footer">
<PortalFooter />
</footer>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { getSiteSuggestion, type SiteSuggestion } from '@/api/site'
import { usePageSeo } from '@/composables/usePageSeo'
definePageMeta({
layout: 'blank'
})
usePageSeo({
title: '建言详情',
description: '广西决策咨询网建言详情页'
})
const route = useRoute()
const suggestionId = computed(() => route.params.id as string)
const loading = ref(true)
const suggestion = ref<Partial<SiteSuggestion>>({})
useHead({
title: computed(() => `${suggestion.value.title || '建言详情'} - 广西决策咨询网`),
meta: [
{ name: 'description', content: computed(() => suggestion.value.content || '广西决策咨询网建言详情页') }
]
})
async function loadSuggestion() {
loading.value = true
try {
suggestion.value = await getSiteSuggestion(suggestionId.value)
} catch (error) {
suggestion.value = {}
if (!(error instanceof Error && /404/.test(error.message))) {
message.error(error instanceof Error ? error.message : '加载失败')
}
} finally {
loading.value = false
}
}
watch(suggestionId, () => {
loadSuggestion()
})
onMounted(() => {
loadSuggestion()
})
</script>
<style scoped>
.suggestion-detail-page {
min-height: 100vh;
background: #f5f7fa;
}
.container {
width: min(1200px, calc(100% - 32px));
margin: 0 auto;
}
.suggestion-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;
}
.suggestion-detail-card {
padding: 32px;
background: #fff;
border: 1px solid #e5ebf2;
}
.detail-head {
display: flex;
align-items: start;
justify-content: space-between;
gap: 24px;
padding-bottom: 24px;
border-bottom: 1px solid #edf1f5;
}
.detail-head h1 {
margin: 0 0 10px;
color: #1f2d3d;
font-size: 34px;
line-height: 1.2;
}
.detail-head p {
margin: 0;
color: #64707d;
font-size: 15px;
}
.detail-side {
display: flex;
flex-direction: column;
align-items: end;
gap: 10px;
color: #7d8a98;
font-size: 14px;
}
.content-section {
padding-top: 24px;
}
.content-section h2 {
margin: 0 0 16px;
color: #1f2d3d;
font-size: 22px;
}
.content-body,
.reply-box {
color: #495463;
font-size: 15px;
line-height: 2;
}
.content-body p,
.reply-box {
margin: 0;
white-space: pre-wrap;
}
.reply-box {
padding: 18px 20px;
background: #f8fafc;
}
.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;
}
@media (max-width: 768px) {
.suggestion-detail-main {
padding: 24px 0 40px;
}
.suggestion-detail-card {
padding: 24px 18px;
}
.detail-head {
flex-direction: column;
}
.detail-head h1 {
font-size: 28px;
}
.detail-side {
align-items: start;
}
}
</style>
+271 -117
View File
@@ -1,164 +1,318 @@
<template>
<div class="suggestions-page">
<div class="page-header">
<h1 class="page-title">建言献策</h1>
<p class="page-desc">您的每一条建议都是我们进步的动力期待您的声音</p>
</div>
<PortalHeader active-path="/suggestions" />
<div class="suggestions-content">
<div class="intro-section">
<h2>参与方式</h2>
<p>欢迎您对政策制定经济发展社会治理等方面提出宝贵意见和建议请您先登录或注册账号然后填写建言内容</p>
</div>
<a-form
v-if="isAuthed"
:model="formData"
:rules="rules"
layout="vertical"
class="suggestion-form"
>
<a-form-item label="建言标题" name="title">
<a-input v-model:value="formData.title" placeholder="请输入建言标题" :maxlength="100" show-count />
</a-form-item>
<a-form-item label="建言内容" name="content">
<a-textarea
v-model:value="formData.content"
placeholder="请详细描述您的建议和意见..."
:rows="8"
:maxlength="2000"
show-count
<main class="container suggestions-main">
<div class="page-head">
<div>
<h1>建言献策</h1>
<p>展示已审核并公开的建言内容</p>
</div>
<div class="head-actions">
<a-input-search
v-model:value="keyword"
placeholder="搜索建言标题"
enter-button="搜索"
allow-clear
@search="handleSearch"
/>
</a-form-item>
<a-form-item label="联系方式(选填)" name="contact">
<a-input v-model:value="formData.contact" placeholder="请输入您的联系方式,方便我们与您联系" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" size="large" @click="handleSubmit" :loading="submitting">
提交建言
</a-button>
<a-button size="large" @click="handleReset">
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
<div v-else class="login-prompt">
<a-result
title="请先登录"
sub-title="登录后可提交建言献策"
>
<template #extra>
<a-button type="primary" size="large" @click="navigateTo('/login')">
去登录
</a-button>
</template>
</a-result>
<a-button type="primary" @click="navigateTo('/suggestions/submit')">我要建言</a-button>
</div>
</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="!suggestions.length" class="state-wrap">
<a-empty description="暂无公开建言" />
</div>
<div v-else class="suggestion-list">
<article
v-for="item in suggestions"
:key="item.suggestionId || item.id"
class="suggestion-card"
@click="goDetail(item)"
>
<div class="suggestion-card-top">
<h3>{{ item.title }}</h3>
<span>{{ item.createTime?.slice(0, 10) || '' }}</span>
</div>
<p>{{ item.content || '暂无内容' }}</p>
<div class="suggestion-card-bottom">
<a-tag :color="Number(item.status) === 2 ? 'green' : 'blue'">
{{ Number(item.status) === 2 ? '已采纳' : '已处理' }}
</a-tag>
<span v-if="item.processedTime">处理时间{{ item.processedTime.slice(0, 10) }}</span>
</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>
</main>
<footer class="site-footer">
<PortalFooter />
</footer>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { getToken } from '@/utils/token-util'
import { pageSiteSuggestions, type SiteSuggestion } from '@/api/site'
import { usePageSeo } from '@/composables/usePageSeo'
useHead({ title: '建言献策 - 决策咨询网' })
const isAuthed = computed(() => !!getToken())
const submitting = ref(false)
const formData = reactive({
title: '',
content: '',
contact: '',
definePageMeta({
layout: 'blank'
})
const rules = {
title: [{ required: true, message: '请输入建言标题' }],
content: [{ required: true, message: '请输入建言内容' }],
}
usePageSeo({
title: '建言献策',
description: '广西决策咨询网公开建言列表'
})
async function handleSubmit() {
const route = useRoute()
const router = useRouter()
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 suggestions = ref<SiteSuggestion[]>([])
async function loadSuggestions() {
loading.value = true
try {
// TODO: 接入实际API提交建言
// await submitSuggestion(formData)
message.success('建言已提交,感谢您的参与!')
handleReset()
} catch (e: any) {
message.error(e?.message || '提交失败')
const data = await pageSiteSuggestions({
page: currentPage.value,
limit: pageSize,
keywords: keyword.value || undefined,
})
total.value = data.count || 0
suggestions.value = data.list || []
} catch (error) {
total.value = 0
suggestions.value = []
message.error(error instanceof Error ? error.message : '加载建言失败')
} finally {
loading.value = false
}
}
function handleReset() {
formData.title = ''
formData.content = ''
formData.contact = ''
function updateRoute() {
router.replace({
query: {
...(keyword.value ? { keyword: keyword.value } : {}),
...(currentPage.value > 1 ? { page: currentPage.value } : {}),
}
})
}
function handleSearch() {
currentPage.value = 1
updateRoute()
loadSuggestions()
}
function handlePageChange(page: number) {
currentPage.value = page
updateRoute()
loadSuggestions()
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function goDetail(item: SiteSuggestion) {
router.push(`/suggestions/${item.suggestionId || 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))
loadSuggestions()
})
onMounted(() => {
loadSuggestions()
})
</script>
<style scoped>
.suggestions-page {
max-width: 800px;
min-height: 100vh;
background: #f5f7fa;
}
.container {
width: min(1200px, calc(100% - 32px));
margin: 0 auto;
padding: 40px 20px;
}
.page-header {
text-align: center;
margin-bottom: 40px;
.suggestions-main {
padding: 32px 0 48px;
}
.page-title {
font-size: 32px;
font-weight: 700;
color: #1f2937;
margin: 0 0 12px;
.page-head {
display: flex;
align-items: end;
justify-content: space-between;
gap: 24px;
margin-bottom: 28px;
}
.page-desc {
font-size: 16px;
color: #6b7280;
.page-head h1 {
margin: 0 0 10px;
color: #1f2d3d;
font-size: 34px;
}
.page-head p {
margin: 0;
color: #697586;
font-size: 15px;
}
.suggestions-content {
.head-actions {
display: flex;
align-items: center;
gap: 12px;
width: min(460px, 100%);
}
.head-actions :deep(.ant-input-search) {
flex: 1;
}
.suggestion-list {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 20px;
}
.suggestion-card {
min-height: 220px;
padding: 24px;
border: 1px solid #e6ecf2;
background: #fff;
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
cursor: pointer;
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.intro-section {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
.suggestion-card:hover {
border-color: #9fb7d5;
box-shadow: 0 12px 30px rgba(25, 62, 111, 0.08);
transform: translateY(-2px);
}
.intro-section h2 {
font-size: 18px;
font-weight: 600;
color: #1f2937;
margin: 0 0 8px;
.suggestion-card-top {
display: flex;
align-items: start;
justify-content: space-between;
gap: 16px;
margin-bottom: 14px;
}
.intro-section p {
font-size: 14px;
color: #6b7280;
.suggestion-card-top h3 {
margin: 0;
line-height: 1.6;
color: #1f2d3d;
font-size: 20px;
line-height: 1.4;
}
.suggestion-form {
max-width: 600px;
.suggestion-card-top span {
flex-shrink: 0;
color: #7d8a98;
font-size: 13px;
}
.login-prompt {
padding: 40px 0;
.suggestion-card p {
margin: 0 0 18px;
color: #556371;
font-size: 14px;
line-height: 1.8;
display: -webkit-box;
overflow: hidden;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
}
.suggestion-card-bottom {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
color: #7d8a98;
font-size: 13px;
}
.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) {
.suggestions-main {
padding: 24px 0 40px;
}
.page-head {
flex-direction: column;
align-items: stretch;
}
.head-actions {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.suggestion-list {
grid-template-columns: 1fr;
}
.suggestion-card-bottom {
flex-direction: column;
align-items: start;
}
}
</style>
+214
View File
@@ -0,0 +1,214 @@
<template>
<div class="suggestions-submit-page">
<PortalHeader active-path="/suggestions" />
<main class="container submit-main">
<div class="page-header">
<h1 class="page-title">提交建言</h1>
<p class="page-desc">欢迎您对政策制定经济发展社会治理等方面提出宝贵意见和建议</p>
</div>
<div class="suggestions-content">
<div class="intro-section">
<h2>参与方式</h2>
<p>建言提交后会进入审核流程只有审核通过且设置为显示的内容才会在前台展示</p>
</div>
<a-form
:model="formData"
:rules="rules"
layout="vertical"
class="suggestion-form"
>
<a-form-item label="建言标题" name="title">
<a-input v-model:value="formData.title" placeholder="请输入建言标题" :maxlength="100" show-count />
</a-form-item>
<a-form-item label="建言内容" name="content">
<a-textarea
v-model:value="formData.content"
placeholder="请详细描述您的建议和意见..."
:rows="8"
:maxlength="2000"
show-count
/>
</a-form-item>
<a-form-item label="联系方式(选填)" name="contact">
<a-input v-model:value="formData.contact" placeholder="请输入您的联系方式,方便我们与您联系" />
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" size="large" @click="handleSubmit" :loading="submitting">
提交建言
</a-button>
<a-button size="large" @click="handleReset">
重置
</a-button>
</a-space>
</a-form-item>
</a-form>
</div>
</main>
<footer class="site-footer">
<PortalFooter />
</footer>
</div>
</template>
<script setup lang="ts">
import { message } from 'ant-design-vue'
import { submitSiteSuggestion } from '@/api/site'
import { usePageSeo } from '@/composables/usePageSeo'
definePageMeta({
layout: 'blank'
})
usePageSeo({
title: '提交建言',
description: '广西决策咨询网建言提交页'
})
const submitting = ref(false)
const formData = reactive({
title: '',
content: '',
contact: '',
})
const rules = {
title: [{ required: true, message: '请输入建言标题' }],
content: [{ required: true, message: '请输入建言内容' }],
}
async function handleSubmit() {
if (!formData.title.trim()) {
message.warning('请输入建言标题')
return
}
if (!formData.content.trim()) {
message.warning('请输入建言内容')
return
}
submitting.value = true
try {
await submitSiteSuggestion({
title: formData.title.trim(),
content: formData.content.trim(),
contact: formData.contact.trim(),
})
message.success('建言已提交,感谢您的参与!')
navigateTo('/suggestions')
} catch (error: unknown) {
message.error(error instanceof Error ? error.message : '提交失败')
} finally {
submitting.value = false
}
}
function handleReset() {
formData.title = ''
formData.content = ''
formData.contact = ''
}
</script>
<style scoped>
.suggestions-submit-page {
min-height: 100vh;
background: #f5f7fa;
}
.container {
width: min(960px, calc(100% - 32px));
margin: 0 auto;
}
.submit-main {
padding: 40px 0 48px;
}
.page-header {
text-align: center;
margin-bottom: 32px;
}
.page-title {
margin: 0 0 12px;
color: #1f2937;
font-size: 32px;
font-weight: 700;
}
.page-desc {
margin: 0;
color: #6b7280;
font-size: 16px;
}
.suggestions-content {
background: #fff;
padding: 40px;
box-shadow: 0 4px 16px rgba(0,0,0,0.08);
}
.intro-section {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
}
.intro-section h2 {
margin: 0 0 8px;
color: #1f2937;
font-size: 18px;
font-weight: 600;
}
.intro-section p {
margin: 0;
color: #6b7280;
font-size: 14px;
line-height: 1.6;
}
.suggestion-form {
max-width: 720px;
}
.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) {
.submit-main {
padding: 28px 0 40px;
}
.suggestions-content {
padding: 24px 18px;
}
}
</style>
+17
View File
@@ -0,0 +1,17 @@
<template>
<ModuleEntryDetailPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryDetailPage from '@/components/ModuleEntryDetailPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
title: '支持单位',
baseRoute: '/support-units',
showImage: false,
}
</script>
+19
View File
@@ -0,0 +1,19 @@
<template>
<ModuleEntryListPage :config="pageConfig" />
</template>
<script setup lang="ts">
import ModuleEntryListPage from '@/components/ModuleEntryListPage.vue'
definePageMeta({
layout: 'blank'
})
const pageConfig = {
type: 'support_unit',
title: '支持单位',
description: '展示广西决策咨询网支持单位相关信息',
baseRoute: '/support-units',
showImage: false,
}
</script>