更新首页

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
+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>