feat: 实现动态配置和表单自动重置功能

- 添加动态配置支持,包括下载链接、网站标题等
- 表单提交成功后自动重置表单内容
- 优化详情页布局和显示逻辑
- 更新联系方式卡片样式和功能
- 修改vite配置允许所有网络接口访问
master
Swanky 2 months ago
parent 09a3494e77
commit 6a01af9bb1
  1. 4
      index.html
  2. 49
      src/components/AchievementList.vue
  3. 51
      src/components/DemandList.vue
  4. 6
      src/components/SubmitAchievementForm.vue
  5. 7
      src/components/SubmitDemandForm.vue
  6. 100
      src/views/HomeView.vue
  7. 1
      vite.config.js

@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>project-report</title> <title></title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

@ -3,6 +3,11 @@ import { ref, reactive, onMounted } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import { getAchievementList } from '../services/api' import { getAchievementList } from '../services/api'
//
const props = defineProps({
config: Object
})
// //
const searchKey = ref('') const searchKey = ref('')
@ -84,8 +89,13 @@ const handleDetail = (item) => {
} }
// //
const handleDownload = (year) => { const handleDownload = (url) => {
showToast(`${year}清单下载功能开发中`) if (url) {
//
window.open(url, '_blank')
} else {
showToast('下载链接无效')
}
} }
// //
@ -98,7 +108,7 @@ onMounted(() => {
<div class="achievement-list"> <div class="achievement-list">
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="search-container"> <div class="search-container">
<van-search v-model="searchKey" placeholder="请输入关键词搜索" show-action :show-filter="true" :clearable="false" <van-search v-model="searchKey" placeholder="请输入关键词搜索" show-action action-text="清除" :show-filter="true" :clearable="false"
wrap-with-form @search="handleSearch" @cancel="handleCancel" shape="round" @click-right-icon="handleSearch"> wrap-with-form @search="handleSearch" @cancel="handleCancel" shape="round" @click-right-icon="handleSearch">
<template #right-icon> <template #right-icon>
<a style="text-decoration:none">搜索</a> <a style="text-decoration:none">搜索</a>
@ -108,11 +118,14 @@ onMounted(() => {
<!-- 下载按钮 --> <!-- 下载按钮 -->
<div class="download-container"> <div class="download-container">
<van-button type="default" size="small" @click="handleDownload('2025')"> <van-button
2025年清单下载 v-for="item in props.config?.achievementDownList || []"
</van-button> :key="item.name"
<van-button type="default" size="small" @click="handleDownload('2024')"> type="default"
2024年清单下载 size="small"
@click="handleDownload(item.url)"
>
{{ item.name }}
</van-button> </van-button>
</div> </div>
@ -143,10 +156,10 @@ onMounted(() => {
<div class="item-tags"> <div class="item-tags">
<van-tag v-if="item?.tech_maturity" color="#ff9800" size="small" round>{{ item.tech_maturity <van-tag v-if="item?.tech_maturity" color="#ff9800" size="small" round>{{ item.tech_maturity
}}</van-tag> }}</van-tag>
<van-tag v-if="item?.intellectual_property" color="#9c27b0" size="small" round>{{ <van-tag v-if="item?.cooperation_mode" color="#9c27b0" size="small" round>{{
item.intellectual_property }}</van-tag> item.cooperation_mode }}</van-tag>
</div> </div>
<div class="item-date">{{ item?.create_time || '' }}</div> <!-- <div class="item-date">{{ item?.create_time || '' }}</div> -->
</div> </div>
<div class="item-intro">{{ item?.achievement_intro || '' }}</div> <div class="item-intro">{{ item?.achievement_intro || '' }}</div>
</div> </div>
@ -180,7 +193,7 @@ onMounted(() => {
<van-popup v-model:show="showDetail" position="bottom" :style="{ height: '80%', borderRadius: '16px 16px 0 0' }"> <van-popup v-model:show="showDetail" position="bottom" :style="{ height: '80%', borderRadius: '16px 16px 0 0' }">
<div class="detail-content"> <div class="detail-content">
<div class="detail-header"> <div class="detail-header">
<h3>{{ currentAchievement?.project_name }}</h3> <h3>成果详情</h3>
<van-icon name="cross" size="20" color="#999" @click="showDetail = false" /> <van-icon name="cross" size="20" color="#999" @click="showDetail = false" />
</div> </div>
@ -190,6 +203,10 @@ onMounted(() => {
<van-icon name="info-o" color="#2196f3" size="18" /> <van-icon name="info-o" color="#2196f3" size="18" />
基本信息 基本信息
</h4> </h4>
<div class="detail-item">
<span class="item-label">项目名称</span>
<span class="item-value">{{ currentAchievement?.project_name }}</span>
</div>
<div class="detail-item"> <div class="detail-item">
<span class="item-label">所属领域</span> <span class="item-label">所属领域</span>
<span class="item-value">{{ currentAchievement?.field }}</span> <span class="item-value">{{ currentAchievement?.field }}</span>
@ -198,10 +215,10 @@ onMounted(() => {
<span class="item-label">技术成熟度</span> <span class="item-label">技术成熟度</span>
<span class="item-value">{{ currentAchievement?.tech_maturity }}</span> <span class="item-value">{{ currentAchievement?.tech_maturity }}</span>
</div> </div>
<div class="detail-item"> <!-- <div class="detail-item">
<span class="item-label">知识产权情况</span> <span class="item-label">知识产权情况</span>
<span class="item-value">{{ currentAchievement?.intellectual_property }}</span> <span class="item-value">{{ currentAchievement?.intellectual_property }}</span>
</div> </div> -->
<div class="detail-item"> <div class="detail-item">
<span class="item-label">合作方式</span> <span class="item-label">合作方式</span>
<span class="item-value">{{ currentAchievement?.cooperation_mode }}</span> <span class="item-value">{{ currentAchievement?.cooperation_mode }}</span>
@ -225,9 +242,9 @@ onMounted(() => {
</div> </div>
<!-- 发布时间 --> <!-- 发布时间 -->
<div style="font-size: 12px; color: #999; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;"> <!-- <div style="font-size: 12px; color: #999; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
发布时间{{ currentAchievement?.create_time || '未设置' }} 发布时间{{ currentAchievement?.create_time || '未设置' }}
</div> </div> -->
</div> </div>
</div> </div>
</van-popup> </van-popup>

@ -3,6 +3,11 @@ import { ref, reactive, onMounted } from 'vue'
import { showToast } from 'vant' import { showToast } from 'vant'
import { getDemandList } from '../services/api' import { getDemandList } from '../services/api'
//
const props = defineProps({
config: Object
})
// //
const searchKey = ref('') const searchKey = ref('')
@ -84,8 +89,13 @@ const handleDetail = (item) => {
} }
// //
const handleDownload = (year) => { const handleDownload = (url) => {
showToast(`${year}清单下载功能开发中`) if (url) {
//
window.open(url, '_blank')
} else {
showToast('下载链接无效')
}
} }
// //
@ -98,8 +108,9 @@ onMounted(() => {
<div class="demand-list"> <div class="demand-list">
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="search-container"> <div class="search-container">
<van-search v-model="searchKey" placeholder="请输入关键词搜索" show-action :show-filter="true" :clearable="false" <van-search v-model="searchKey" placeholder="请输入关键词搜索" action-text="清除" show-action :show-filter="true"
wrap-with-form @search="handleSearch" @cancel="handleCancel" shape="round" @click-right-icon="handleSearch"> :clearable="false" wrap-with-form @search="handleSearch" @cancel="handleCancel" shape="round"
@click-right-icon="handleSearch">
<template #right-icon> <template #right-icon>
<a style="text-decoration:none">搜索</a> <a style="text-decoration:none">搜索</a>
</template> </template>
@ -108,11 +119,9 @@ onMounted(() => {
<!-- 下载按钮 --> <!-- 下载按钮 -->
<div class="download-container"> <div class="download-container">
<van-button type="default" size="small" @click="handleDownload('2025')"> <van-button v-for="item in props.config?.demandDownList || []" :key="item.name" type="default" size="small"
2025年清单下载 @click="handleDownload(item.url)">
</van-button> {{ item.name }}
<van-button type="default" size="small" @click="handleDownload('2024')">
2024年清单下载
</van-button> </van-button>
</div> </div>
@ -135,19 +144,18 @@ onMounted(() => {
<van-loading type="spinner" color="#1989fa" /> <van-loading type="spinner" color="#1989fa" />
</div> </div>
<div v-for="item in demandList" :key="item?.id || item" class="list-item" <div v-for="item in demandList" :key="item?.id || item" class="list-item" @click="item && handleDetail(item)">
@click="item && handleDetail(item)">
<div class="item-content"> <div class="item-content">
<div class="item-title">{{ item?.project_name || '' }}</div> <div class="item-title">{{ item?.project_name || '' }}</div>
<div class="item-info-row"> <!-- <div class="item-info-row">
<div class="item-tags"> <div class="item-tags">
<van-tag v-if="item?.tech_maturity" color="#ff9800" size="small" round>{{ item.tech_maturity <van-tag v-if="item?.tech_maturity" color="#ff9800" size="small" round>{{ item.tech_maturity
}}</van-tag> }}</van-tag>
<van-tag v-if="item?.intellectual_property" color="#9c27b0" size="small" round>{{ <van-tag v-if="item?.intellectual_property" color="#9c27b0" size="small" round>{{
item.intellectual_property }}</van-tag> item.intellectual_property }}</van-tag>
</div> </div>
<div class="item-date">{{ item?.create_time || '' }}</div> <div class="item-date">{{ item?.create_time || '' }}</div>
</div> </div> -->
<div class="item-intro">{{ item?.demand_overview || '' }}</div> <div class="item-intro">{{ item?.demand_overview || '' }}</div>
</div> </div>
<div class="item-footer"> <div class="item-footer">
@ -180,7 +188,7 @@ onMounted(() => {
<van-popup v-model:show="showDetail" position="bottom" :style="{ height: '80%', borderRadius: '16px 16px 0 0' }"> <van-popup v-model:show="showDetail" position="bottom" :style="{ height: '80%', borderRadius: '16px 16px 0 0' }">
<div class="detail-content"> <div class="detail-content">
<div class="detail-header"> <div class="detail-header">
<h3>{{ currentDemand?.project_name }}</h3> <h3>需求详情</h3>
<van-icon name="cross" size="20" color="#999" @click="showDetail = false" /> <van-icon name="cross" size="20" color="#999" @click="showDetail = false" />
</div> </div>
@ -190,18 +198,22 @@ onMounted(() => {
<van-icon name="info-o" color="#2196f3" size="18" /> <van-icon name="info-o" color="#2196f3" size="18" />
基本信息 基本信息
</h4> </h4>
<div class="detail-item">
<span class="item-label">项目名称</span>
<span class="item-value">{{ currentDemand?.project_name }}</span>
</div>
<div class="detail-item"> <div class="detail-item">
<span class="item-label">所属领域</span> <span class="item-label">所属领域</span>
<span class="item-value">{{ currentDemand?.field }}</span> <span class="item-value">{{ currentDemand?.field }}</span>
</div> </div>
<div class="detail-item"> <!-- <div class="detail-item">
<span class="item-label">技术成熟度</span> <span class="item-label">技术成熟度</span>
<span class="item-value">{{ currentDemand?.tech_maturity }}</span> <span class="item-value">{{ currentDemand?.tech_maturity }}</span>
</div> </div>
<div class="detail-item"> <div class="detail-item">
<span class="item-label">知识产权要求</span> <span class="item-label">知识产权要求</span>
<span class="item-value">{{ currentDemand?.intellectual_property }}</span> <span class="item-value">{{ currentDemand?.intellectual_property }}</span>
</div> </div> -->
<div class="detail-item"> <div class="detail-item">
<span class="item-label">预期技术指标</span> <span class="item-label">预期技术指标</span>
<span class="item-value">{{ currentDemand?.expected_tech_target }}</span> <span class="item-value">{{ currentDemand?.expected_tech_target }}</span>
@ -238,7 +250,7 @@ onMounted(() => {
</h4> </h4>
<div class="detail-content-text">{{ currentDemand?.demand_overview }}</div> <div class="detail-content-text">{{ currentDemand?.demand_overview }}</div>
</div> </div>
<!-- 发布时间 --> <!-- 发布时间 -->
<div style="font-size: 12px; color: #999; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;"> <div style="font-size: 12px; color: #999; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
发布时间{{ currentDemand?.create_time || '未设置' }} 发布时间{{ currentDemand?.create_time || '未设置' }}
@ -513,8 +525,7 @@ onMounted(() => {
padding-left: 0; padding-left: 0;
} }
.download-container { .download-container {}
}
.list-container { .list-container {
padding: 12px; padding: 12px;

@ -136,6 +136,12 @@ const submitForm = async () => {
// //
showSuccessToast('表单提交成功') showSuccessToast('表单提交成功')
console.log('表单提交成功:', response) console.log('表单提交成功:', response)
//
for (const key in form) {
if (Object.hasOwnProperty.call(form, key)) {
form[key] = ''
}
}
} else { } else {
// //
showToast(response.message || '提交失败,请稍后重试') showToast(response.message || '提交失败,请稍后重试')

@ -124,7 +124,12 @@ const submitForm = async () => {
// //
if (response.code === 1) { if (response.code === 1) {
showSuccessToast('需求提交成功') showSuccessToast('需求提交成功')
// //
for (const key in form) {
if (Object.hasOwnProperty.call(form, key)) {
form[key] = ''
}
}
} else { } else {
showToast(response.message || '提交失败,请稍后重试') showToast(response.message || '提交失败,请稍后重试')
} }

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue' import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { getHomeConfig } from '../services/api' import { getHomeConfig } from '../services/api'
import { showToast } from 'vant' import { showToast } from 'vant'
import SubmitAchievementForm from '../components/SubmitAchievementForm.vue' import SubmitAchievementForm from '../components/SubmitAchievementForm.vue'
@ -13,10 +13,16 @@ const config = ref({
introduction: '', introduction: '',
serviceHotline: [], serviceHotline: [],
emails: [], emails: [],
address: [] officialSite: []
}) })
const loading = ref(true) const loading = ref(true)
//
const introductionParagraphs = computed(() => {
if (!config.value.introduction) return []
return config.value.introduction.split(/\r\n|\r|\n/).filter(paragraph => paragraph.trim())
})
// <van-icon name="balance-list" /><van-icon name="add-square" /> // <van-icon name="balance-list" /><van-icon name="add-square" />
const tabs = [ const tabs = [
{ key: 'achievement', name: '成果清单', icon: 'balance-list', color: '#2196f3', activeColor: '#1976d2' }, { key: 'achievement', name: '成果清单', icon: 'balance-list', color: '#2196f3', activeColor: '#1976d2' },
@ -83,6 +89,10 @@ onMounted(async () => {
const response = await getHomeConfig() const response = await getHomeConfig()
if (response.code === 1) { if (response.code === 1) {
config.value = response.data config.value = response.data
//
if (response.data?.siteTitle) {
document.title = response.data.siteTitle
}
} else { } else {
showToast('获取数据失败') showToast('获取数据失败')
} }
@ -115,6 +125,7 @@ onUnmounted(() => {
<!-- 顶部标题区域 --> <!-- 顶部标题区域 -->
<div class="header"> <div class="header">
<h1 class="title">{{ config.title }}</h1> <h1 class="title">{{ config.title }}</h1>
<p class="subtitle">{{ config.subTitle }}</p>
</div> </div>
<!-- 内容区 --> <!-- 内容区 -->
@ -125,7 +136,11 @@ onUnmounted(() => {
<div v-if="loading" class="loading-container"> <div v-if="loading" class="loading-container">
<van-skeleton title :row="3" animated /> <van-skeleton title :row="3" animated />
</div> </div>
<div v-else class="intro-content">{{ config.introduction }}</div> <div v-else class="intro-content">
<p v-for="(paragraph, index) in introductionParagraphs" :key="index" class="intro-paragraph">
{{ paragraph }}
</p>
</div>
</div> </div>
<!-- 切换按钮区域 --> <!-- 切换按钮区域 -->
@ -152,12 +167,12 @@ onUnmounted(() => {
<div class="tab-content"> <div class="tab-content">
<!-- 成果清单 --> <!-- 成果清单 -->
<div v-if="activeTab === 'achievement'" class="tab-panel"> <div v-if="activeTab === 'achievement'" class="tab-panel">
<AchievementList /> <AchievementList :config="config" />
</div> </div>
<!-- 需求清单 --> <!-- 需求清单 -->
<div v-else-if="activeTab === 'demand'" class="tab-panel"> <div v-else-if="activeTab === 'demand'" class="tab-panel">
<DemandList /> <DemandList :config="config" />
</div> </div>
<!-- 成果填报 --> <!-- 成果填报 -->
@ -183,13 +198,16 @@ onUnmounted(() => {
<!-- 服务热线 --> <!-- 服务热线 -->
<div class="contact-item-card phone-card"> <div class="contact-item-card phone-card">
<div class="contact-item-header"> <div class="contact-item-header">
<van-icon name="phone-o" size="18" color="#2196f3" /> <van-icon name="service-o" size="18" color="#2196f3" />
<span class="contact-item-label">服务热线</span> <span class="contact-item-label">服务热线</span>
</div> </div>
<div class="contact-item-content"> <div class="contact-item-content">
<div v-for="(item, index) in config.serviceHotline" :key="index" class="contact-person"> <div v-for="(item, index) in config.serviceHotline" :key="index" class="contact-person">
<span class="person-name">{{ item.name }}</span> <span class="person-name">{{ item.name }}</span>
<a :href="`tel:${item.phone}`" class="person-phone">{{ item.phone }}</a> <a :href="`tel:${item.phone}`" class="person-phone">
<van-icon name="phone" size="14" color="#2196f3" />
<span>{{ item.phone }}</span>
</a>
</div> </div>
</div> </div>
</div> </div>
@ -197,7 +215,7 @@ onUnmounted(() => {
<!-- 邮箱咨询 --> <!-- 邮箱咨询 -->
<div class="contact-item-card email-card"> <div class="contact-item-card email-card">
<div class="contact-item-header"> <div class="contact-item-header">
<van-icon name="email-o" size="18" color="#9c27b0" /> <van-icon name="envelop-o" size="18" color="#9c27b0" />
<span class="contact-item-label">邮箱咨询</span> <span class="contact-item-label">邮箱咨询</span>
</div> </div>
<div class="contact-item-content"> <div class="contact-item-content">
@ -207,16 +225,16 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 办公地址 --> <!-- 协会官网 -->
<div class="contact-item-card address-card"> <div class="contact-item-card official-site-card">
<div class="contact-item-header"> <div class="contact-item-header">
<van-icon name="location-o" size="18" color="#4caf50" /> <van-icon name="link-o" size="18" color="#4caf50" />
<span class="contact-item-label">办公地址</span> <span class="contact-item-label">协会官网</span>
</div> </div>
<div class="contact-item-content"> <div class="contact-item-content">
<div v-for="(addr, index) in config.address" :key="index" class="contact-address"> <a v-for="(site, index) in config.officialSite" :key="index" :href="site" class="contact-official-site" target="_blank">
{{ addr }} {{ site }}
</div> </a>
</div> </div>
</div> </div>
</div> </div>
@ -249,6 +267,16 @@ onUnmounted(() => {
word-break: break-word; word-break: break-word;
} }
/* 副标题样式 */
.subtitle {
font-size: 16px;
font-weight: 500;
margin: 8px 0 0 0;
line-height: 1.3;
word-break: break-word;
opacity: 0.9;
}
/* 内容区 */ /* 内容区 */
.content { .content {
padding: 15px; padding: 15px;
@ -284,12 +312,26 @@ onUnmounted(() => {
padding: 10px 0; padding: 10px 0;
} }
/* 简介内容 */ /* 简介内容容器 */
.intro-content { .intro-content {
display: block;
width: 100%;
box-sizing: border-box;
}
/* 简介段落 */
.intro-paragraph {
font-size: 14px; font-size: 14px;
line-height: 1.7; line-height: 1.7;
color: #555; color: #555;
text-align: justify; text-align: justify;
text-indent: 2em;
margin: 0 0 12px 0;
word-wrap: break-word;
}
.intro-paragraph:last-child {
margin-bottom: 0;
} }
/* 正文占位符 */ /* 正文占位符 */
@ -377,17 +419,17 @@ onUnmounted(() => {
color: #9c27b0; color: #9c27b0;
} }
/* 办公地址卡片样式 */ /* 协会官网卡片样式 */
.address-card { .official-site-card {
background-color: rgba(76, 175, 80, 0.05); background-color: rgba(76, 175, 80, 0.05);
border-left: 4px solid #4caf50; border-left: 4px solid #4caf50;
} }
.address-card .contact-item-label { .official-site-card .contact-item-label {
color: #4caf50; color: #4caf50;
} }
.address-card .contact-address { .official-site-card .contact-official-site {
color: #4caf50; color: #4caf50;
} }
@ -414,9 +456,9 @@ onUnmounted(() => {
/* 联系人信息 */ /* 联系人信息 */
.contact-person { .contact-person {
display: flex; display: flex;
justify-content: space-between; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 8px; margin-bottom: 12px;
font-size: 14px; font-size: 14px;
} }
@ -428,10 +470,14 @@ onUnmounted(() => {
.person-name { .person-name {
color: #666; color: #666;
font-weight: 500; font-weight: 500;
margin-right: 12px;
} }
/* 联系人电话 */ /* 联系人电话 */
.person-phone { .person-phone {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500; font-weight: 500;
text-decoration: none; text-decoration: none;
} }
@ -444,12 +490,18 @@ onUnmounted(() => {
font-weight: 500; font-weight: 500;
} }
/* 联系地址 */ /* 协会官网链接 */
.contact-address { .contact-official-site {
display: block; display: block;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
line-height: 1.6; line-height: 1.6;
text-decoration: none;
word-break: break-all;
}
.contact-official-site:hover {
text-decoration: underline;
} }
/* 切换按钮区域 */ /* 切换按钮区域 */

@ -16,6 +16,7 @@ export default defineConfig({
// }), // }),
], ],
server: { server: {
host: '0.0.0.0', // 允许所有网络接口访问
proxy: { proxy: {
'/pro-report': { '/pro-report': {
target: 'http://47.109.32.96:9999', target: 'http://47.109.32.96:9999',

Loading…
Cancel
Save