commit
09a3494e77
@ -0,0 +1,24 @@ |
||||
# Logs |
||||
logs |
||||
*.log |
||||
npm-debug.log* |
||||
yarn-debug.log* |
||||
yarn-error.log* |
||||
pnpm-debug.log* |
||||
lerna-debug.log* |
||||
|
||||
node_modules |
||||
dist |
||||
dist-ssr |
||||
*.local |
||||
|
||||
# Editor directories and files |
||||
.vscode/* |
||||
!.vscode/extensions.json |
||||
.idea |
||||
.DS_Store |
||||
*.suo |
||||
*.ntvs* |
||||
*.njsproj |
||||
*.sln |
||||
*.sw? |
||||
@ -0,0 +1,3 @@ |
||||
{ |
||||
"recommendations": ["Vue.volar"] |
||||
} |
||||
@ -0,0 +1,5 @@ |
||||
# Vue 3 + Vite |
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. |
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support). |
||||
@ -0,0 +1,13 @@ |
||||
<!doctype html> |
||||
<html lang="en"> |
||||
<head> |
||||
<meta charset="UTF-8" /> |
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
||||
<title>project-report</title> |
||||
</head> |
||||
<body> |
||||
<div id="app"></div> |
||||
<script type="module" src="/src/main.js"></script> |
||||
</body> |
||||
</html> |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@ |
||||
{ |
||||
"name": "project-report", |
||||
"private": true, |
||||
"version": "0.0.0", |
||||
"type": "module", |
||||
"scripts": { |
||||
"dev": "vite", |
||||
"build": "vite build", |
||||
"preview": "vite preview" |
||||
}, |
||||
"dependencies": { |
||||
"axios": "^1.13.2", |
||||
"vant": "^4.8.0", |
||||
"vue": "^3.3.4", |
||||
"vue-router": "^4.6.4" |
||||
}, |
||||
"devDependencies": { |
||||
"@vitejs/plugin-vue": "^4.4.0", |
||||
"amfe-flexible": "^2.2.1", |
||||
"postcss-pxtorem": "^6.0.0", |
||||
"unplugin-auto-import": "^0.16.7", |
||||
"unplugin-vue-components": "^0.25.2", |
||||
"vite": "^4.5.0" |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
export default { |
||||
plugins: { |
||||
'postcss-pxtorem': { |
||||
rootValue: 37.5, |
||||
propList: ['*'], |
||||
exclude: /node_modules/i, |
||||
}, |
||||
}, |
||||
} |
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,30 @@ |
||||
<script setup> |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="app"> |
||||
<!-- 路由出口 --> |
||||
<router-view /> |
||||
</div> |
||||
</template> |
||||
|
||||
<style> |
||||
/* 全局样式重置 */ |
||||
* { |
||||
box-sizing: border-box; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
body { |
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
||||
font-size: 14px; |
||||
line-height: 1.5; |
||||
color: #333; |
||||
background-color: #f5f5f5; |
||||
} |
||||
|
||||
.app { |
||||
min-height: 100vh; |
||||
} |
||||
</style> |
||||
|
After Width: | Height: | Size: 496 B |
@ -0,0 +1,538 @@ |
||||
<script setup> |
||||
import { ref, reactive, onMounted } from 'vue' |
||||
import { showToast } from 'vant' |
||||
import { getAchievementList } from '../services/api' |
||||
|
||||
// 搜索关键词 |
||||
const searchKey = ref('') |
||||
|
||||
// 分页参数 |
||||
const pagination = reactive({ |
||||
page: 1, |
||||
size: 3, |
||||
total: 0 |
||||
}) |
||||
|
||||
// 成果列表数据 |
||||
const achievementList = ref([]) |
||||
|
||||
// 加载状态 |
||||
const loading = ref(true) |
||||
// 是否为第一次加载 |
||||
const isFirstLoad = ref(true) |
||||
|
||||
// 详情弹窗 |
||||
const showDetail = ref(false) |
||||
const currentAchievement = ref(null) |
||||
|
||||
// 获取成果列表 |
||||
const fetchAchievementList = async () => { |
||||
try { |
||||
loading.value = true |
||||
const response = await getAchievementList({ |
||||
page: pagination.page, |
||||
size: pagination.size, |
||||
key: searchKey.value |
||||
}) |
||||
if (response.code === 1) { |
||||
achievementList.value = response.data?.list || [] |
||||
pagination.total = response.data?.total || 0 |
||||
// 第一次加载完成后设置为false |
||||
if (isFirstLoad.value) { |
||||
isFirstLoad.value = false |
||||
} |
||||
} else { |
||||
showToast('获取成果列表失败') |
||||
// 失败时清空列表 |
||||
achievementList.value = [] |
||||
pagination.total = 0 |
||||
} |
||||
} catch (error) { |
||||
console.error('请求成果列表失败:', error) |
||||
showToast('网络错误,请稍后重试') |
||||
// 异常时清空列表 |
||||
achievementList.value = [] |
||||
pagination.total = 0 |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
} |
||||
|
||||
// 搜索 |
||||
const handleSearch = () => { |
||||
pagination.page = 1 |
||||
fetchAchievementList() |
||||
} |
||||
|
||||
// 取消搜索 |
||||
const handleCancel = () => { |
||||
searchKey.value = '' |
||||
pagination.page = 1 |
||||
fetchAchievementList() |
||||
} |
||||
|
||||
// 分页处理 |
||||
const handlePageChange = (page) => { |
||||
pagination.page = page |
||||
fetchAchievementList() |
||||
} |
||||
|
||||
// 查看详情 |
||||
const handleDetail = (item) => { |
||||
currentAchievement.value = item |
||||
showDetail.value = true |
||||
} |
||||
|
||||
// 下载清单 |
||||
const handleDownload = (year) => { |
||||
showToast(`${year}清单下载功能开发中`) |
||||
} |
||||
|
||||
// 页面挂载时获取数据 |
||||
onMounted(() => { |
||||
fetchAchievementList() |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="achievement-list"> |
||||
<!-- 搜索框 --> |
||||
<div class="search-container"> |
||||
<van-search v-model="searchKey" placeholder="请输入关键词搜索" show-action :show-filter="true" :clearable="false" |
||||
wrap-with-form @search="handleSearch" @cancel="handleCancel" shape="round" @click-right-icon="handleSearch"> |
||||
<template #right-icon> |
||||
<a style="text-decoration:none">搜索</a> |
||||
</template> |
||||
</van-search> |
||||
</div> |
||||
|
||||
<!-- 下载按钮 --> |
||||
<div class="download-container"> |
||||
<van-button type="default" size="small" @click="handleDownload('2025')"> |
||||
2025年清单下载 |
||||
</van-button> |
||||
<van-button type="default" size="small" @click="handleDownload('2024')"> |
||||
2024年清单下载 |
||||
</van-button> |
||||
</div> |
||||
|
||||
<!-- 列表区域 --> |
||||
<div class="list-container"> |
||||
<!-- 首次加载骨架屏 --> |
||||
<div v-if="loading && isFirstLoad" class="loading-container"> |
||||
<van-skeleton title :row="5" animated /> |
||||
</div> |
||||
|
||||
<!-- 空数据状态 --> |
||||
<div v-else-if="achievementList.length === 0" class="empty-container"> |
||||
<van-empty description="暂无数据" /> |
||||
</div> |
||||
|
||||
<!-- 列表内容 --> |
||||
<div v-else-if="Array.isArray(achievementList)" class="list-wrapper"> |
||||
<!-- 加载遮罩层 --> |
||||
<div v-if="loading" class="loading-mask"> |
||||
<van-loading type="spinner" color="#1989fa" /> |
||||
</div> |
||||
|
||||
<div v-for="item in achievementList" :key="item?.id || item" class="list-item" |
||||
@click="item && handleDetail(item)"> |
||||
<div class="item-content"> |
||||
<div class="item-title">{{ item?.project_name || '' }}</div> |
||||
<div class="item-info-row"> |
||||
<div class="item-tags"> |
||||
<van-tag v-if="item?.tech_maturity" color="#ff9800" size="small" round>{{ item.tech_maturity |
||||
}}</van-tag> |
||||
<van-tag v-if="item?.intellectual_property" color="#9c27b0" size="small" round>{{ |
||||
item.intellectual_property }}</van-tag> |
||||
</div> |
||||
<div class="item-date">{{ item?.create_time || '' }}</div> |
||||
</div> |
||||
<div class="item-intro">{{ item?.achievement_intro || '' }}</div> |
||||
</div> |
||||
<div class="item-footer"> |
||||
<div class="footer-left"> |
||||
<van-tag v-if="item?.field" color="#4caf50" size="small" round>{{ item.field }}</van-tag> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 分页 --> |
||||
<div v-if="achievementList.length > 0" class="pagination-container"> |
||||
<div class="pagination-info"> |
||||
共 {{ pagination.total }} 条 |
||||
</div> |
||||
<van-pagination v-model="pagination.page" :total-items="pagination.total" :show-page-size="5" |
||||
:items-per-page="pagination.size" :disabled="loading" @change="handlePageChange"> |
||||
<template #prev-text> |
||||
<van-icon name="arrow-left" /> |
||||
</template> |
||||
<template #next-text> |
||||
<van-icon name="arrow-right" /> |
||||
</template> |
||||
<template #page="{ text }">{{ text }}</template> |
||||
</van-pagination> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 详情弹窗 --> |
||||
<van-popup v-model:show="showDetail" position="bottom" :style="{ height: '80%', borderRadius: '16px 16px 0 0' }"> |
||||
<div class="detail-content"> |
||||
<div class="detail-header"> |
||||
<h3>{{ currentAchievement?.project_name }}</h3> |
||||
<van-icon name="cross" size="20" color="#999" @click="showDetail = false" /> |
||||
</div> |
||||
|
||||
<div class="detail-body"> |
||||
<div class="detail-section"> |
||||
<h4 class="section-title"> |
||||
<van-icon name="info-o" color="#2196f3" size="18" /> |
||||
基本信息 |
||||
</h4> |
||||
<div class="detail-item"> |
||||
<span class="item-label">所属领域:</span> |
||||
<span class="item-value">{{ currentAchievement?.field }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">技术成熟度:</span> |
||||
<span class="item-value">{{ currentAchievement?.tech_maturity }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">知识产权情况:</span> |
||||
<span class="item-value">{{ currentAchievement?.intellectual_property }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">合作方式:</span> |
||||
<span class="item-value">{{ currentAchievement?.cooperation_mode }}</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="detail-section"> |
||||
<h4 class="section-title"> |
||||
<van-icon name="comment-o" color="#ff9800" size="18" /> |
||||
备注 |
||||
</h4> |
||||
<div class="detail-content-text">{{ currentAchievement?.remarks || '无备注信息' }}</div> |
||||
</div> |
||||
|
||||
<div class="detail-section"> |
||||
<h4 class="section-title"> |
||||
<van-icon name="comment-o" color="#ff9800" size="18" /> |
||||
成果简介 |
||||
</h4> |
||||
<div class="detail-content-text">{{ currentAchievement?.achievement_intro }}</div> |
||||
</div> |
||||
|
||||
<!-- 发布时间 --> |
||||
<div style="font-size: 12px; color: #999; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;"> |
||||
发布时间:{{ currentAchievement?.create_time || '未设置' }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</van-popup> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* 搜索容器 */ |
||||
.search-container { |
||||
/* padding: 15px; */ |
||||
background-color: white; |
||||
|
||||
} |
||||
|
||||
.search-container :deep(.van-search) { |
||||
padding-left: 0; |
||||
} |
||||
|
||||
/* 下载按钮容器 */ |
||||
.download-container { |
||||
display: flex; |
||||
gap: 10px; |
||||
padding: 0 0 15px 0; |
||||
background-color: white; |
||||
} |
||||
|
||||
/* 列表容器 */ |
||||
.list-container { |
||||
padding: 15px; |
||||
background-color: #f5f5f5; |
||||
} |
||||
|
||||
/* 加载状态 */ |
||||
.loading-container { |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 15px; |
||||
margin-bottom: 15px; |
||||
} |
||||
|
||||
/* 空数据状态 */ |
||||
.empty-container { |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 30px 0; |
||||
} |
||||
|
||||
/* 列表包装器 */ |
||||
.list-wrapper { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 15px; |
||||
position: relative; |
||||
} |
||||
|
||||
/* 加载遮罩层 */ |
||||
.loading-mask { |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background-color: rgba(255, 255, 255, 0.8); |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
border-radius: 8px; |
||||
z-index: 10; |
||||
} |
||||
|
||||
/* 列表项 */ |
||||
.list-item { |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 15px; |
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); |
||||
cursor: pointer; |
||||
transition: transform 0.2s ease, box-shadow 0.2s ease; |
||||
} |
||||
|
||||
.list-item:active { |
||||
transform: translateY(1px); |
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.05); |
||||
} |
||||
|
||||
/* 列表项内容 */ |
||||
.item-content { |
||||
padding-bottom: 15px; |
||||
border-bottom: 1px solid #f0f0f0; |
||||
margin-bottom: 15px; |
||||
} |
||||
|
||||
/* 标题 */ |
||||
.item-title { |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
color: #333; |
||||
margin-bottom: 10px; |
||||
line-height: 1.4; |
||||
} |
||||
|
||||
/* 信息行 */ |
||||
.item-info-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
/* 标签容器 */ |
||||
.item-tags { |
||||
display: flex; |
||||
gap: 5px; |
||||
} |
||||
|
||||
/* 简介 */ |
||||
.item-intro { |
||||
font-size: 14px; |
||||
color: #666; |
||||
line-height: 1.5; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
display: -webkit-box; |
||||
-webkit-line-clamp: 3; |
||||
-webkit-box-orient: vertical; |
||||
} |
||||
|
||||
/* 列表项底部 */ |
||||
.item-footer { |
||||
display: flex; |
||||
justify-content: flex-start; |
||||
align-items: center; |
||||
} |
||||
|
||||
/* 底部左侧 */ |
||||
.footer-left { |
||||
display: flex; |
||||
gap: 5px; |
||||
} |
||||
|
||||
/* 日期 */ |
||||
.item-date { |
||||
font-size: 12px; |
||||
color: #999; |
||||
text-align: right; |
||||
} |
||||
|
||||
/* 分页容器 */ |
||||
.pagination-container { |
||||
margin-top: 15px; |
||||
text-align: center; |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 15px; |
||||
} |
||||
|
||||
/* 分页信息 */ |
||||
.pagination-info { |
||||
font-size: 14px; |
||||
color: #666; |
||||
margin-bottom: 10px; |
||||
text-align: center; |
||||
} |
||||
|
||||
/* 详情弹窗 */ |
||||
.detail-content { |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
padding: 15px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 详情头部 */ |
||||
.detail-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 15px; |
||||
padding-bottom: 10px; |
||||
border-bottom: 1px solid #f0f0f0; |
||||
} |
||||
|
||||
.detail-header h3 { |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
margin: 0; |
||||
color: #333; |
||||
line-height: 1.4; |
||||
} |
||||
|
||||
/* 详情主体 */ |
||||
.detail-body { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 15px; |
||||
} |
||||
|
||||
/* 详情区块 */ |
||||
.detail-section { |
||||
background-color: #fafafa; |
||||
border-radius: 8px; |
||||
padding: 12px; |
||||
} |
||||
|
||||
/* 区块标题 */ |
||||
.section-title { |
||||
font-size: 15px; |
||||
font-weight: 600; |
||||
margin: 0 0 12px 0; |
||||
color: #333; |
||||
padding-bottom: 6px; |
||||
border-bottom: 1px solid #e0e0e0; |
||||
} |
||||
|
||||
/* 详情项 */ |
||||
.detail-item { |
||||
display: flex; |
||||
align-items: flex-start; |
||||
margin-bottom: 10px; |
||||
line-height: 1.5; |
||||
} |
||||
|
||||
.detail-item:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
/* 详情标签 */ |
||||
.item-label { |
||||
font-weight: 500; |
||||
color: #666; |
||||
min-width: 80px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 详情值 */ |
||||
.item-value { |
||||
flex: 1; |
||||
color: #333; |
||||
font-size: 14px; |
||||
line-height: 1.5; |
||||
word-break: break-word; |
||||
} |
||||
|
||||
/* 详情内容文本 */ |
||||
.detail-content-text { |
||||
color: #333; |
||||
line-height: 1.6; |
||||
text-align: justify; |
||||
white-space: pre-line; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 区块标题 */ |
||||
.section-title { |
||||
font-size: 15px; |
||||
font-weight: 600; |
||||
margin: 0 0 12px 0; |
||||
color: #333; |
||||
padding-bottom: 6px; |
||||
border-bottom: 1px solid #e0e0e0; |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 6px; |
||||
} |
||||
|
||||
/* 适配小屏幕 */ |
||||
@media (max-width: 375px) { |
||||
.search-container :deep(.van-search) { |
||||
padding-left: 0; |
||||
} |
||||
|
||||
.download-container { |
||||
/* padding: 0 0 12px 0; */ |
||||
} |
||||
|
||||
.list-container { |
||||
padding: 12px; |
||||
} |
||||
|
||||
.list-item { |
||||
padding: 12px; |
||||
} |
||||
|
||||
.item-title { |
||||
font-size: 15px; |
||||
} |
||||
|
||||
.item-intro { |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.detail-content { |
||||
padding: 15px; |
||||
} |
||||
|
||||
.detail-header h3 { |
||||
font-size: 17px; |
||||
} |
||||
|
||||
.section-title { |
||||
font-size: 15px; |
||||
} |
||||
|
||||
.detail-section { |
||||
padding: 12px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,551 @@ |
||||
<script setup> |
||||
import { ref, reactive, onMounted } from 'vue' |
||||
import { showToast } from 'vant' |
||||
import { getDemandList } from '../services/api' |
||||
|
||||
// 搜索关键词 |
||||
const searchKey = ref('') |
||||
|
||||
// 分页参数 |
||||
const pagination = reactive({ |
||||
page: 1, |
||||
size: 3, |
||||
total: 0 |
||||
}) |
||||
|
||||
// 需求列表数据 |
||||
const demandList = ref([]) |
||||
|
||||
// 加载状态 |
||||
const loading = ref(true) |
||||
// 是否为第一次加载 |
||||
const isFirstLoad = ref(true) |
||||
|
||||
// 详情弹窗 |
||||
const showDetail = ref(false) |
||||
const currentDemand = ref(null) |
||||
|
||||
// 获取需求列表 |
||||
const fetchDemandList = async () => { |
||||
try { |
||||
loading.value = true |
||||
const response = await getDemandList({ |
||||
page: pagination.page, |
||||
size: pagination.size, |
||||
key: searchKey.value |
||||
}) |
||||
if (response.code === 1) { |
||||
demandList.value = response.data?.list || [] |
||||
pagination.total = response.data?.total || 0 |
||||
// 第一次加载完成后设置为false |
||||
if (isFirstLoad.value) { |
||||
isFirstLoad.value = false |
||||
} |
||||
} else { |
||||
showToast('获取需求列表失败') |
||||
// 失败时清空列表 |
||||
demandList.value = [] |
||||
pagination.total = 0 |
||||
} |
||||
} catch (error) { |
||||
console.error('请求需求列表失败:', error) |
||||
showToast('网络错误,请稍后重试') |
||||
// 异常时清空列表 |
||||
demandList.value = [] |
||||
pagination.total = 0 |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
} |
||||
|
||||
// 搜索 |
||||
const handleSearch = () => { |
||||
pagination.page = 1 |
||||
fetchDemandList() |
||||
} |
||||
|
||||
// 取消搜索 |
||||
const handleCancel = () => { |
||||
searchKey.value = '' |
||||
pagination.page = 1 |
||||
fetchDemandList() |
||||
} |
||||
|
||||
// 分页处理 |
||||
const handlePageChange = (page) => { |
||||
pagination.page = page |
||||
fetchDemandList() |
||||
} |
||||
|
||||
// 查看详情 |
||||
const handleDetail = (item) => { |
||||
currentDemand.value = item |
||||
showDetail.value = true |
||||
} |
||||
|
||||
// 下载清单 |
||||
const handleDownload = (year) => { |
||||
showToast(`${year}清单下载功能开发中`) |
||||
} |
||||
|
||||
// 页面挂载时获取数据 |
||||
onMounted(() => { |
||||
fetchDemandList() |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="demand-list"> |
||||
<!-- 搜索框 --> |
||||
<div class="search-container"> |
||||
<van-search v-model="searchKey" placeholder="请输入关键词搜索" show-action :show-filter="true" :clearable="false" |
||||
wrap-with-form @search="handleSearch" @cancel="handleCancel" shape="round" @click-right-icon="handleSearch"> |
||||
<template #right-icon> |
||||
<a style="text-decoration:none">搜索</a> |
||||
</template> |
||||
</van-search> |
||||
</div> |
||||
|
||||
<!-- 下载按钮 --> |
||||
<div class="download-container"> |
||||
<van-button type="default" size="small" @click="handleDownload('2025')"> |
||||
2025年清单下载 |
||||
</van-button> |
||||
<van-button type="default" size="small" @click="handleDownload('2024')"> |
||||
2024年清单下载 |
||||
</van-button> |
||||
</div> |
||||
|
||||
<!-- 列表区域 --> |
||||
<div class="list-container"> |
||||
<!-- 首次加载骨架屏 --> |
||||
<div v-if="loading && isFirstLoad" class="loading-container"> |
||||
<van-skeleton title :row="5" animated /> |
||||
</div> |
||||
|
||||
<!-- 空数据状态 --> |
||||
<div v-else-if="demandList.length === 0" class="empty-container"> |
||||
<van-empty description="暂无数据" /> |
||||
</div> |
||||
|
||||
<!-- 列表内容 --> |
||||
<div v-else-if="Array.isArray(demandList)" class="list-wrapper"> |
||||
<!-- 加载遮罩层 --> |
||||
<div v-if="loading" class="loading-mask"> |
||||
<van-loading type="spinner" color="#1989fa" /> |
||||
</div> |
||||
|
||||
<div v-for="item in demandList" :key="item?.id || item" class="list-item" |
||||
@click="item && handleDetail(item)"> |
||||
<div class="item-content"> |
||||
<div class="item-title">{{ item?.project_name || '' }}</div> |
||||
<div class="item-info-row"> |
||||
<div class="item-tags"> |
||||
<van-tag v-if="item?.tech_maturity" color="#ff9800" size="small" round>{{ item.tech_maturity |
||||
}}</van-tag> |
||||
<van-tag v-if="item?.intellectual_property" color="#9c27b0" size="small" round>{{ |
||||
item.intellectual_property }}</van-tag> |
||||
</div> |
||||
<div class="item-date">{{ item?.create_time || '' }}</div> |
||||
</div> |
||||
<div class="item-intro">{{ item?.demand_overview || '' }}</div> |
||||
</div> |
||||
<div class="item-footer"> |
||||
<div class="footer-left"> |
||||
<van-tag v-if="item?.field" color="#4caf50" size="small" round>{{ item.field }}</van-tag> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 分页 --> |
||||
<div v-if="demandList.length > 0" class="pagination-container"> |
||||
<div class="pagination-info"> |
||||
共 {{ pagination.total }} 条 |
||||
</div> |
||||
<van-pagination v-model="pagination.page" :total-items="pagination.total" :show-page-size="5" |
||||
:items-per-page="pagination.size" :disabled="loading" @change="handlePageChange"> |
||||
<template #prev-text> |
||||
<van-icon name="arrow-left" /> |
||||
</template> |
||||
<template #next-text> |
||||
<van-icon name="arrow-right" /> |
||||
</template> |
||||
<template #page="{ text }">{{ text }}</template> |
||||
</van-pagination> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 详情弹窗 --> |
||||
<van-popup v-model:show="showDetail" position="bottom" :style="{ height: '80%', borderRadius: '16px 16px 0 0' }"> |
||||
<div class="detail-content"> |
||||
<div class="detail-header"> |
||||
<h3>{{ currentDemand?.project_name }}</h3> |
||||
<van-icon name="cross" size="20" color="#999" @click="showDetail = false" /> |
||||
</div> |
||||
|
||||
<div class="detail-body"> |
||||
<div class="detail-section"> |
||||
<h4 class="section-title"> |
||||
<van-icon name="info-o" color="#2196f3" size="18" /> |
||||
基本信息 |
||||
</h4> |
||||
<div class="detail-item"> |
||||
<span class="item-label">所属领域:</span> |
||||
<span class="item-value">{{ currentDemand?.field }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">技术成熟度:</span> |
||||
<span class="item-value">{{ currentDemand?.tech_maturity }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">知识产权要求:</span> |
||||
<span class="item-value">{{ currentDemand?.intellectual_property }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">预期技术指标:</span> |
||||
<span class="item-value">{{ currentDemand?.expected_tech_target }}</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="detail-section"> |
||||
<h4 class="section-title"> |
||||
<van-icon name="user-o" color="#4caf50" size="18" /> |
||||
填报单位信息 |
||||
</h4> |
||||
<div class="detail-item"> |
||||
<span class="item-label">填报企业:</span> |
||||
<span class="item-value">{{ currentDemand?.filling_enterprise }}</span> |
||||
</div> |
||||
<!-- <div class="detail-item"> |
||||
<span class="item-label">联系人:</span> |
||||
<span class="item-value">{{ currentDemand?.contact_person }}</span> |
||||
</div> |
||||
<div class="detail-item"> |
||||
<span class="item-label">联系电话:</span> |
||||
<span class="item-value">{{ currentDemand?.contact_phone }}</span> |
||||
</div> --> |
||||
<div class="detail-item"> |
||||
<span class="item-label">企业意向资金:</span> |
||||
<span class="item-value">{{ currentDemand?.enterprise_intended_fund }}万元</span> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="detail-section"> |
||||
<h4 class="section-title"> |
||||
<van-icon name="comment-o" color="#ff9800" size="18" /> |
||||
需求概述 |
||||
</h4> |
||||
<div class="detail-content-text">{{ currentDemand?.demand_overview }}</div> |
||||
</div> |
||||
|
||||
<!-- 发布时间 --> |
||||
<div style="font-size: 12px; color: #999; margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;"> |
||||
发布时间:{{ currentDemand?.create_time || '未设置' }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</van-popup> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* 搜索容器 */ |
||||
.search-container { |
||||
background-color: white; |
||||
|
||||
} |
||||
|
||||
.search-container :deep(.van-search) { |
||||
padding-left: 0; |
||||
} |
||||
|
||||
/* 下载按钮容器 */ |
||||
.download-container { |
||||
display: flex; |
||||
gap: 10px; |
||||
padding: 0 0 15px 0; |
||||
background-color: white; |
||||
} |
||||
|
||||
/* 列表容器 */ |
||||
.list-container { |
||||
padding: 15px; |
||||
background-color: #f5f5f5; |
||||
} |
||||
|
||||
/* 加载状态 */ |
||||
.loading-container { |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 15px; |
||||
margin-bottom: 15px; |
||||
} |
||||
|
||||
/* 空数据状态 */ |
||||
.empty-container { |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 30px 0; |
||||
} |
||||
|
||||
/* 列表包装器 */ |
||||
.list-wrapper { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 15px; |
||||
position: relative; |
||||
} |
||||
|
||||
/* 加载遮罩层 */ |
||||
.loading-mask { |
||||
position: absolute; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
bottom: 0; |
||||
background-color: rgba(255, 255, 255, 0.8); |
||||
display: flex; |
||||
justify-content: center; |
||||
align-items: center; |
||||
border-radius: 8px; |
||||
z-index: 10; |
||||
} |
||||
|
||||
/* 列表项 */ |
||||
.list-item { |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 15px; |
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); |
||||
cursor: pointer; |
||||
transition: transform 0.2s ease, box-shadow 0.2s ease; |
||||
} |
||||
|
||||
.list-item:active { |
||||
transform: translateY(1px); |
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.05); |
||||
} |
||||
|
||||
/* 列表项内容 */ |
||||
.item-content { |
||||
padding-bottom: 15px; |
||||
border-bottom: 1px solid #f0f0f0; |
||||
margin-bottom: 15px; |
||||
} |
||||
|
||||
/* 标题 */ |
||||
.item-title { |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
color: #333; |
||||
margin-bottom: 10px; |
||||
line-height: 1.4; |
||||
} |
||||
|
||||
/* 信息行 */ |
||||
.item-info-row { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 10px; |
||||
} |
||||
|
||||
/* 标签容器 */ |
||||
.item-tags { |
||||
display: flex; |
||||
gap: 5px; |
||||
} |
||||
|
||||
/* 简介 */ |
||||
.item-intro { |
||||
font-size: 14px; |
||||
color: #666; |
||||
line-height: 1.5; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
display: -webkit-box; |
||||
-webkit-line-clamp: 3; |
||||
-webkit-box-orient: vertical; |
||||
} |
||||
|
||||
/* 列表项底部 */ |
||||
.item-footer { |
||||
display: flex; |
||||
justify-content: flex-start; |
||||
align-items: center; |
||||
} |
||||
|
||||
/* 底部左侧 */ |
||||
.footer-left { |
||||
display: flex; |
||||
gap: 5px; |
||||
} |
||||
|
||||
/* 日期 */ |
||||
.item-date { |
||||
font-size: 12px; |
||||
color: #999; |
||||
text-align: right; |
||||
} |
||||
|
||||
/* 分页容器 */ |
||||
.pagination-container { |
||||
margin-top: 15px; |
||||
text-align: center; |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
padding: 15px; |
||||
} |
||||
|
||||
/* 分页信息 */ |
||||
.pagination-info { |
||||
font-size: 14px; |
||||
color: #666; |
||||
margin-bottom: 10px; |
||||
text-align: center; |
||||
} |
||||
|
||||
/* 详情弹窗 */ |
||||
.detail-content { |
||||
height: 100%; |
||||
overflow-y: auto; |
||||
padding: 15px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 详情头部 */ |
||||
.detail-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 15px; |
||||
padding-bottom: 10px; |
||||
border-bottom: 1px solid #f0f0f0; |
||||
} |
||||
|
||||
.detail-header h3 { |
||||
font-size: 16px; |
||||
font-weight: 600; |
||||
margin: 0; |
||||
color: #333; |
||||
line-height: 1.4; |
||||
} |
||||
|
||||
/* 详情主体 */ |
||||
.detail-body { |
||||
display: flex; |
||||
flex-direction: column; |
||||
gap: 15px; |
||||
} |
||||
|
||||
/* 详情区块 */ |
||||
.detail-section { |
||||
background-color: #fafafa; |
||||
border-radius: 8px; |
||||
padding: 12px; |
||||
} |
||||
|
||||
/* 区块标题 */ |
||||
.section-title { |
||||
font-size: 15px; |
||||
font-weight: 600; |
||||
margin: 0 0 12px 0; |
||||
color: #333; |
||||
padding-bottom: 6px; |
||||
border-bottom: 1px solid #e0e0e0; |
||||
} |
||||
|
||||
/* 详情项 */ |
||||
.detail-item { |
||||
display: flex; |
||||
align-items: flex-start; |
||||
margin-bottom: 10px; |
||||
line-height: 1.5; |
||||
} |
||||
|
||||
.detail-item:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
/* 详情标签 */ |
||||
.item-label { |
||||
font-weight: 500; |
||||
color: #666; |
||||
min-width: 100px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 详情值 */ |
||||
.item-value { |
||||
flex: 1; |
||||
color: #333; |
||||
font-size: 14px; |
||||
line-height: 1.5; |
||||
word-break: break-word; |
||||
} |
||||
|
||||
/* 详情内容文本 */ |
||||
.detail-content-text { |
||||
color: #333; |
||||
line-height: 1.6; |
||||
text-align: justify; |
||||
white-space: pre-line; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 区块标题 */ |
||||
.section-title { |
||||
font-size: 15px; |
||||
font-weight: 600; |
||||
margin: 0 0 12px 0; |
||||
color: #333; |
||||
padding-bottom: 6px; |
||||
border-bottom: 1px solid #e0e0e0; |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 6px; |
||||
} |
||||
|
||||
/* 适配小屏幕 */ |
||||
@media (max-width: 375px) { |
||||
.search-container :deep(.van-search) { |
||||
padding-left: 0; |
||||
} |
||||
|
||||
.download-container { |
||||
} |
||||
|
||||
.list-container { |
||||
padding: 12px; |
||||
} |
||||
|
||||
.list-item { |
||||
padding: 12px; |
||||
} |
||||
|
||||
.item-title { |
||||
font-size: 15px; |
||||
} |
||||
|
||||
.item-intro { |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.detail-content { |
||||
padding: 15px; |
||||
} |
||||
|
||||
.detail-header h3 { |
||||
font-size: 17px; |
||||
} |
||||
|
||||
.section-title { |
||||
font-size: 15px; |
||||
} |
||||
|
||||
.detail-section { |
||||
padding: 12px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,43 @@ |
||||
<script setup> |
||||
import { ref } from 'vue' |
||||
|
||||
defineProps({ |
||||
msg: String, |
||||
}) |
||||
|
||||
const count = ref(0) |
||||
</script> |
||||
|
||||
<template> |
||||
<h1>{{ msg }}</h1> |
||||
|
||||
<div class="card"> |
||||
<button type="button" @click="count++">count is {{ count }}</button> |
||||
<p> |
||||
Edit |
||||
<code>components/HelloWorld.vue</code> to test HMR |
||||
</p> |
||||
</div> |
||||
|
||||
<p> |
||||
Check out |
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank" |
||||
>create-vue</a |
||||
>, the official Vue + Vite starter |
||||
</p> |
||||
<p> |
||||
Learn more about IDE Support for Vue in the |
||||
<a |
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support" |
||||
target="_blank" |
||||
>Vue Docs Scaling up Guide</a |
||||
>. |
||||
</p> |
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.read-the-docs { |
||||
color: #888; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,461 @@ |
||||
<script setup> |
||||
import { ref, reactive, onMounted } from 'vue' |
||||
import { showToast, showSuccessToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant' |
||||
import { getSystemEnum, createAchievement } from '../services/api' |
||||
|
||||
// 表单数据 |
||||
const form = reactive({ |
||||
// 项目基本信息 |
||||
project_name: '', |
||||
field: '', |
||||
|
||||
// 项目技术信息 |
||||
tech_maturity: '', |
||||
intellectual_property: '', |
||||
|
||||
// 合作相关信息 |
||||
cooperation_mode: '', |
||||
remarks: '', |
||||
|
||||
// 联系人信息 |
||||
completer_name: '', |
||||
completer_company: '', |
||||
contact_phone: '', |
||||
|
||||
// 项目详细描述 |
||||
achievement_intro: '' |
||||
}) |
||||
|
||||
// 选择器显示状态 |
||||
const showFieldPicker = ref(false) |
||||
const showMaturityPicker = ref(false) |
||||
const showIpPicker = ref(false) |
||||
const showCooperationPicker = ref(false) |
||||
|
||||
// 必填字段验证规则 |
||||
const requiredFields = { |
||||
project_name: '项目名称', |
||||
field: '所属领域', |
||||
tech_maturity: '技术成熟度', |
||||
intellectual_property: '知识产权情况', |
||||
cooperation_mode: '合作方式', |
||||
completer_name: '成果完成人姓名', |
||||
completer_company: '工作单位', |
||||
contact_phone: '联系电话', |
||||
achievement_intro: '成果简介' |
||||
} |
||||
|
||||
// 枚举数据 |
||||
const enumData = reactive({ |
||||
domain: [], // 所属领域 |
||||
trl: [], // 技术成熟度 |
||||
propertyRights: [], // 知识产权情况 |
||||
coopMode: [] // 合作方式 |
||||
}) |
||||
|
||||
// 加载状态 |
||||
const loading = ref(true) |
||||
|
||||
// 页面挂载时获取系统枚举数据 |
||||
onMounted(async () => { |
||||
try { |
||||
loading.value = true |
||||
const response = await getSystemEnum() |
||||
if (response.code === 1) { |
||||
// 转换枚举数据格式为 Vant Picker 需要的格式 |
||||
enumData.domain = response.data.domain.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
enumData.trl = response.data.trl.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
enumData.propertyRights = response.data.propertyRights.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
enumData.coopMode = response.data.coopMode.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
} else { |
||||
showToast('获取枚举数据失败') |
||||
} |
||||
} catch (error) { |
||||
console.error('请求枚举数据失败:', error) |
||||
showToast('网络错误,请稍后重试') |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
}) |
||||
|
||||
// 表单验证 |
||||
const validateForm = () => { |
||||
for (const [field, label] of Object.entries(requiredFields)) { |
||||
if (!form[field]) { |
||||
showToast(`${label}为必填项`) |
||||
return false |
||||
} |
||||
} |
||||
|
||||
// 验证成果简介字数 |
||||
if (form.achievement_intro.length > 800) { |
||||
showToast('成果简介不能超过800字') |
||||
return false |
||||
} |
||||
|
||||
// 验证联系电话格式 |
||||
// const phoneRegex = /^1[3-9]\d{9}$/ |
||||
// if (!phoneRegex.test(form.contact_phone)) { |
||||
// showToast('请输入正确的联系电话') |
||||
// return false |
||||
// } |
||||
|
||||
return true |
||||
} |
||||
|
||||
// 提交表单 |
||||
const submitForm = async () => { |
||||
if (validateForm()) { |
||||
try { |
||||
// 显示加载状态 |
||||
const loading = showLoadingToast({ |
||||
message: '提交中...', |
||||
forbidClick: true, |
||||
duration: 0 |
||||
}) |
||||
|
||||
// 调用API提交表单数据 |
||||
const response = await createAchievement(form) |
||||
|
||||
// 隐藏加载状态 |
||||
closeToast() |
||||
|
||||
if (response.code === 1) { |
||||
// 提交成功 |
||||
showSuccessToast('表单提交成功') |
||||
console.log('表单提交成功:', response) |
||||
} else { |
||||
// 提交失败,显示错误信息 |
||||
showToast(response.message || '提交失败,请稍后重试') |
||||
} |
||||
} catch (error) { |
||||
// 隐藏加载状态 |
||||
closeToast() |
||||
// 网络错误处理 |
||||
showToast('网络错误,请检查网络连接后重试') |
||||
console.error('表单提交失败:', error) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 重置表单 |
||||
const resetForm = async () => { |
||||
try { |
||||
// 显示确认对话框 |
||||
await showConfirmDialog({ |
||||
title: '确认重置', |
||||
message: '确定要重置表单吗?所有填写内容都将被清空。', |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消' |
||||
}) |
||||
|
||||
// 用户点击确认后执行重置操作 |
||||
for (const key in form) { |
||||
if (Object.hasOwnProperty.call(form, key)) { |
||||
form[key] = '' |
||||
} |
||||
} |
||||
|
||||
// 显示重置成功提示 |
||||
showToast('表单已重置') |
||||
} catch (error) { |
||||
// 用户点击取消或发生错误,不执行任何操作 |
||||
console.log('重置已取消') |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="submit-achievement-form"> |
||||
<!-- 表单内容 --> |
||||
<div class="form-container"> |
||||
<!-- 加载状态 --> |
||||
<div v-if="loading" class="loading-container"> |
||||
<van-skeleton title :row="10" animated /> |
||||
</div> |
||||
|
||||
<div v-else> |
||||
<!-- 项目基本信息 --> |
||||
<div class="form-group"> |
||||
<div class="group-title">项目基本信息</div> |
||||
<div class="field-wrapper"> |
||||
<van-field |
||||
v-model="form.project_name" |
||||
label="项目名称" |
||||
placeholder="请输入项目名称" |
||||
required |
||||
/> |
||||
<van-field |
||||
v-model="form.field" |
||||
label="所属领域" |
||||
placeholder="请选择所属领域" |
||||
required |
||||
@click="showFieldPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 项目技术信息 --> |
||||
<div class="form-group"> |
||||
<div class="group-title">项目技术信息</div> |
||||
<div class="field-wrapper"> |
||||
<van-field |
||||
v-model="form.tech_maturity" |
||||
label="技术成熟度" |
||||
placeholder="请选择技术成熟度" |
||||
required |
||||
@click="showMaturityPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
<van-field |
||||
v-model="form.intellectual_property" |
||||
label="知识产权情况" |
||||
placeholder="请选择知识产权情况" |
||||
required |
||||
@click="showIpPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 合作相关信息 --> |
||||
<div class="form-group"> |
||||
<div class="group-title">合作相关信息</div> |
||||
<div class="field-wrapper"> |
||||
<van-field |
||||
v-model="form.cooperation_mode" |
||||
label="合作方式" |
||||
placeholder="请选择合作方式" |
||||
required |
||||
@click="showCooperationPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
<van-field |
||||
v-model="form.remarks" |
||||
label="备注" |
||||
placeholder="请输入备注信息" |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 联系人信息 --> |
||||
<div class="form-group"> |
||||
<div class="group-title">联系人信息</div> |
||||
<div class="field-wrapper"> |
||||
<van-field |
||||
v-model="form.completer_name" |
||||
label="成果完成人姓名" |
||||
placeholder="请输入成果完成人姓名" |
||||
required |
||||
/> |
||||
<van-field |
||||
v-model="form.completer_company" |
||||
label="工作单位" |
||||
placeholder="请输入工作单位" |
||||
required |
||||
/> |
||||
<van-field |
||||
v-model="form.contact_phone" |
||||
label="联系电话" |
||||
placeholder="请输入联系电话" |
||||
required |
||||
type="tel" |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 项目详细描述 --> |
||||
<div class="form-group"> |
||||
<div class="group-title">项目详细描述</div> |
||||
<div class="field-wrapper"> |
||||
<van-field |
||||
v-model="form.achievement_intro" |
||||
label="成果简介" |
||||
placeholder="请输入成果简介(800字以内)" |
||||
type="textarea" |
||||
rows="5" |
||||
maxlength="800" |
||||
show-word-limit |
||||
required |
||||
/> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 提交按钮 --> |
||||
<div class="submit-btn-container"> |
||||
<van-button type="default" round size="large" @click="resetForm"> |
||||
重置 |
||||
</van-button> |
||||
<van-button type="primary" round size="large" @click="submitForm"> |
||||
提交 |
||||
</van-button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 选择器 --> |
||||
<!-- 所属领域选择器 --> |
||||
<van-popup v-model:show="showFieldPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.domain" |
||||
@confirm="(value) => { |
||||
form.field = value.selectedValues[0]; |
||||
showFieldPicker = false |
||||
}" |
||||
@cancel="showFieldPicker = false" |
||||
title="选择所属领域" |
||||
/> |
||||
</van-popup> |
||||
|
||||
<!-- 技术成熟度选择器 --> |
||||
<van-popup v-model:show="showMaturityPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.trl" |
||||
@confirm="(value) => { |
||||
form.tech_maturity = value.selectedValues[0]; |
||||
showMaturityPicker = false |
||||
}" |
||||
@cancel="showMaturityPicker = false" |
||||
title="选择技术成熟度" |
||||
/> |
||||
</van-popup> |
||||
|
||||
<!-- 知识产权情况选择器 --> |
||||
<van-popup v-model:show="showIpPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.propertyRights" |
||||
@confirm="(value) => { |
||||
form.intellectual_property = value.selectedValues[0]; |
||||
showIpPicker = false |
||||
}" |
||||
@cancel="showIpPicker = false" |
||||
title="选择知识产权情况" |
||||
/> |
||||
</van-popup> |
||||
|
||||
<!-- 合作方式选择器 --> |
||||
<van-popup v-model:show="showCooperationPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.coopMode" |
||||
@confirm="(value) => { |
||||
form.cooperation_mode = value.selectedValues[0]; |
||||
showCooperationPicker = false |
||||
}" |
||||
@cancel="showCooperationPicker = false" |
||||
title="选择合作方式" |
||||
/> |
||||
</van-popup> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* 表单容器 */ |
||||
.form-container { |
||||
padding: 10px 0; |
||||
} |
||||
|
||||
/* 表单分组 */ |
||||
.form-group { |
||||
margin-bottom: 15px; |
||||
background-color: white; |
||||
border-radius: 8px; |
||||
overflow: hidden; |
||||
border: 1px solid #f0f0f0; |
||||
} |
||||
|
||||
/* 分组标题 */ |
||||
.group-title { |
||||
font-size: 14px; |
||||
font-weight: 600; |
||||
color: #333; |
||||
padding: 10px 15px; |
||||
background-color: #fafafa; |
||||
border-bottom: 1px solid #f0f0f0; |
||||
} |
||||
|
||||
/* 字段包装器 */ |
||||
.field-wrapper { |
||||
padding: 0; |
||||
} |
||||
|
||||
/* 自定义字段样式 */ |
||||
.field-wrapper :deep(.van-field) { |
||||
padding: 10px 15px; |
||||
font-size: 14px; |
||||
border-bottom: 1px solid #f5f5f5; |
||||
} |
||||
|
||||
.field-wrapper :deep(.van-field__label) { |
||||
font-weight: 500; |
||||
color: #666; |
||||
} |
||||
|
||||
.field-wrapper :deep(.van-field__control) { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 最后一个字段移除下边框 */ |
||||
.field-wrapper :deep(.van-field:last-child) { |
||||
border-bottom: none; |
||||
} |
||||
|
||||
/* 必填项星号颜色 */ |
||||
.field-wrapper :deep(.van-field__required) { |
||||
color: #f44336; |
||||
} |
||||
|
||||
/* 提交按钮容器 */ |
||||
.submit-btn-container { |
||||
margin-top: 20px; |
||||
padding: 0 15px; |
||||
display: flex; |
||||
gap: 10px; |
||||
} |
||||
|
||||
/* 适配小屏幕 */ |
||||
@media (max-width: 375px) { |
||||
.form-container { |
||||
padding: 8px 0; |
||||
} |
||||
|
||||
.group-title { |
||||
padding: 8px 12px; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.field-wrapper :deep(.van-field) { |
||||
padding: 8px 12px; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.submit-btn-container { |
||||
padding: 0 12px; |
||||
margin-top: 15px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,385 @@ |
||||
<script setup> |
||||
import { ref, reactive, onMounted } from 'vue' |
||||
import { showToast, showSuccessToast, showLoadingToast, closeToast, showConfirmDialog } from 'vant' |
||||
import { getSystemEnum, createDemand } from '../services/api' |
||||
|
||||
// 表单数据 |
||||
const form = reactive({ |
||||
// 项目基本信息 |
||||
project_name: '', |
||||
field: '', |
||||
tech_maturity: '', |
||||
expected_tech_target: '', |
||||
intellectual_property: '', |
||||
enterprise_intended_fund: '', |
||||
filling_enterprise: '', |
||||
contact_person: '', |
||||
contact_phone: '', |
||||
demand_overview: '' |
||||
}) |
||||
|
||||
// 选择器显示状态 |
||||
const showFieldPicker = ref(false) |
||||
const showMaturityPicker = ref(false) |
||||
const showIpPicker = ref(false) |
||||
|
||||
// 必填字段验证规则 |
||||
const requiredFields = { |
||||
project_name: '项目名称', |
||||
field: '所属领域', |
||||
tech_maturity: '技术成熟度', |
||||
expected_tech_target: '预期技术目标(指标)', |
||||
intellectual_property: '知识产权情况', |
||||
enterprise_intended_fund: '企业意向资金额(万元)', |
||||
filling_enterprise: '填报企业', |
||||
contact_person: '联系人', |
||||
contact_phone: '联系电话', |
||||
demand_overview: '需求概述' |
||||
} |
||||
|
||||
// 枚举数据 |
||||
const enumData = reactive({ |
||||
domain: [], // 所属领域 |
||||
trl: [], // 技术成熟度 |
||||
propertyRights: [] // 知识产权情况 |
||||
}) |
||||
|
||||
// 加载状态 |
||||
const loading = ref(true) |
||||
|
||||
// 页面挂载时获取系统枚举数据 |
||||
onMounted(async () => { |
||||
try { |
||||
loading.value = true |
||||
const response = await getSystemEnum() |
||||
if (response.code === 1) { |
||||
// 转换枚举数据格式为 Vant Picker 需要的格式 |
||||
enumData.domain = response.data.domain.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
enumData.trl = response.data.trl.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
enumData.propertyRights = response.data.propertyRights.map(item => ({ |
||||
text: item.name, |
||||
value: item.value |
||||
})) |
||||
} else { |
||||
showToast('获取枚举数据失败') |
||||
} |
||||
} catch (error) { |
||||
console.error('请求枚举数据失败:', error) |
||||
showToast('网络错误,请稍后重试') |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
}) |
||||
|
||||
// 表单验证 |
||||
const validateForm = () => { |
||||
for (const [field, label] of Object.entries(requiredFields)) { |
||||
if (!form[field]) { |
||||
showToast(`${label}为必填项`) |
||||
return false |
||||
} |
||||
} |
||||
|
||||
// 验证需求概述字数 |
||||
if (form.demand_overview.length > 800) { |
||||
showToast('需求概述不能超过800字') |
||||
return false |
||||
} |
||||
|
||||
// 验证联系电话格式 |
||||
// const phoneRegex = /^1[3-9]\d{9}$/ |
||||
// if (!phoneRegex.test(form.contact_phone)) { |
||||
// showToast('请输入正确的联系电话') |
||||
// return false |
||||
// } |
||||
|
||||
// 验证企业意向资金额为数字 |
||||
// if (isNaN(Number(form.enterprise_intended_fund)) || Number(form.enterprise_intended_fund) < 0) { |
||||
// showToast('请输入正确的企业意向资金额') |
||||
// return false |
||||
// } |
||||
|
||||
return true |
||||
} |
||||
|
||||
// 提交表单 |
||||
const submitForm = async () => { |
||||
if (validateForm()) { |
||||
try { |
||||
// 显示加载提示 |
||||
showLoadingToast({ message: '提交中...', forbidClick: true, duration: 0 }) |
||||
|
||||
// 调用API提交数据 |
||||
const response = await createDemand(form) |
||||
|
||||
// 关闭加载提示 |
||||
closeToast() |
||||
|
||||
// 处理响应 |
||||
if (response.code === 1) { |
||||
showSuccessToast('需求提交成功') |
||||
// 这里可以添加表单重置或页面跳转逻辑 |
||||
} else { |
||||
showToast(response.message || '提交失败,请稍后重试') |
||||
} |
||||
} catch (error) { |
||||
// 关闭加载提示 |
||||
closeToast() |
||||
|
||||
// 处理错误 |
||||
showToast('网络错误,请检查网络连接后重试') |
||||
console.error('表单提交失败:', error) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 重置表单 |
||||
const resetForm = async () => { |
||||
try { |
||||
// 显示确认对话框 |
||||
await showConfirmDialog({ |
||||
title: '确认重置', |
||||
message: '确定要重置表单吗?所有填写内容都将被清空。', |
||||
confirmButtonText: '确定', |
||||
cancelButtonText: '取消' |
||||
}) |
||||
|
||||
// 用户点击确认后执行重置操作 |
||||
for (const key in form) { |
||||
if (Object.hasOwnProperty.call(form, key)) { |
||||
form[key] = '' |
||||
} |
||||
} |
||||
|
||||
// 显示重置成功提示 |
||||
showToast('表单已重置') |
||||
} catch (error) { |
||||
// 用户点击取消或发生错误,不执行任何操作 |
||||
console.log('重置已取消') |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="submit-demand-form"> |
||||
<!-- 表单内容 --> |
||||
<div class="form-container"> |
||||
<!-- 加载状态 --> |
||||
<div v-if="loading" class="loading-container"> |
||||
<van-skeleton title :row="12" animated /> |
||||
</div> |
||||
|
||||
<div v-else> |
||||
<!-- 项目名称 --> |
||||
<van-field |
||||
v-model="form.project_name" |
||||
label="项目名称" |
||||
placeholder="请输入项目名称" |
||||
required |
||||
/> |
||||
|
||||
<!-- 所属领域 --> |
||||
<van-field |
||||
v-model="form.field" |
||||
label="所属领域" |
||||
placeholder="请选择所属领域" |
||||
required |
||||
@click="showFieldPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
|
||||
<!-- 技术成熟度 --> |
||||
<van-field |
||||
v-model="form.tech_maturity" |
||||
label="技术成熟度" |
||||
placeholder="请选择技术成熟度" |
||||
required |
||||
@click="showMaturityPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
|
||||
<!-- 预期技术目标(指标) --> |
||||
<van-field |
||||
v-model="form.expected_tech_target" |
||||
label="预期技术目标(指标)" |
||||
placeholder="请输入预期技术目标(指标)" |
||||
required |
||||
/> |
||||
|
||||
<!-- 知识产权情况 --> |
||||
<van-field |
||||
v-model="form.intellectual_property" |
||||
label="知识产权情况" |
||||
placeholder="请选择知识产权情况" |
||||
required |
||||
@click="showIpPicker = true" |
||||
> |
||||
<template #right-icon> |
||||
<van-icon name="arrow-down" size="16" color="#999" /> |
||||
</template> |
||||
</van-field> |
||||
|
||||
<!-- 企业意向资金额(万元) --> |
||||
<van-field |
||||
v-model="form.enterprise_intended_fund" |
||||
label="企业意向资金额(万元)" |
||||
placeholder="请输入企业意向资金额" |
||||
required |
||||
type="number" |
||||
/> |
||||
|
||||
<!-- 填报企业 --> |
||||
<van-field |
||||
v-model="form.filling_enterprise" |
||||
label="填报企业" |
||||
placeholder="请输入填报企业" |
||||
required |
||||
/> |
||||
|
||||
<!-- 联系人 --> |
||||
<van-field |
||||
v-model="form.contact_person" |
||||
label="联系人" |
||||
placeholder="请输入联系人" |
||||
required |
||||
/> |
||||
|
||||
<!-- 联系电话 --> |
||||
<van-field |
||||
v-model="form.contact_phone" |
||||
label="联系电话" |
||||
placeholder="请输入联系电话" |
||||
required |
||||
type="tel" |
||||
/> |
||||
|
||||
<!-- 需求概述 --> |
||||
<van-field |
||||
v-model="form.demand_overview" |
||||
label="需求概述" |
||||
placeholder="请输入需求概述(800字以内)" |
||||
type="textarea" |
||||
rows="5" |
||||
maxlength="800" |
||||
show-word-limit |
||||
required |
||||
/> |
||||
|
||||
<!-- 提交按钮 --> |
||||
<div class="submit-btn-container"> |
||||
<van-button type="default" round size="large" @click="resetForm"> |
||||
重置 |
||||
</van-button> |
||||
<van-button type="primary" round size="large" @click="submitForm"> |
||||
提交 |
||||
</van-button> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 选择器 --> |
||||
<!-- 所属领域选择器 --> |
||||
<van-popup v-model:show="showFieldPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.domain" |
||||
@confirm="(value) => { |
||||
form.field = value.selectedValues[0]; |
||||
showFieldPicker = false |
||||
}" |
||||
@cancel="showFieldPicker = false" |
||||
title="选择所属领域" |
||||
/> |
||||
</van-popup> |
||||
|
||||
<!-- 技术成熟度选择器 --> |
||||
<van-popup v-model:show="showMaturityPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.trl" |
||||
@confirm="(value) => { |
||||
form.tech_maturity = value.selectedValues[0]; |
||||
showMaturityPicker = false |
||||
}" |
||||
@cancel="showMaturityPicker = false" |
||||
title="选择技术成熟度" |
||||
/> |
||||
</van-popup> |
||||
|
||||
<!-- 知识产权情况选择器 --> |
||||
<van-popup v-model:show="showIpPicker" position="bottom"> |
||||
<van-picker |
||||
:columns="enumData.propertyRights" |
||||
@confirm="(value) => { |
||||
form.intellectual_property = value.selectedValues[0]; |
||||
showIpPicker = false |
||||
}" |
||||
@cancel="showIpPicker = false" |
||||
title="选择知识产权情况" |
||||
/> |
||||
</van-popup> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* 表单容器 */ |
||||
.form-container { |
||||
padding: 10px 0; |
||||
} |
||||
|
||||
/* 自定义字段样式 */ |
||||
.van-field { |
||||
padding: 10px 15px; |
||||
font-size: 14px; |
||||
border-bottom: 1px solid #f5f5f5; |
||||
} |
||||
|
||||
.van-field__label { |
||||
font-weight: 500; |
||||
color: #666; |
||||
} |
||||
|
||||
.van-field__control { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
/* 必填项星号颜色 */ |
||||
.van-field__required { |
||||
color: #f44336; |
||||
} |
||||
|
||||
/* 提交按钮容器 */ |
||||
.submit-btn-container { |
||||
margin-top: 20px; |
||||
padding: 0 15px; |
||||
display: flex; |
||||
gap: 10px; |
||||
} |
||||
|
||||
/* 适配小屏幕 */ |
||||
@media (max-width: 375px) { |
||||
.form-container { |
||||
padding: 8px 0; |
||||
} |
||||
|
||||
.van-field { |
||||
padding: 8px 12px; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.submit-btn-container { |
||||
padding: 0 12px; |
||||
margin-top: 15px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,12 @@ |
||||
import { createApp } from 'vue' |
||||
import 'amfe-flexible' |
||||
import App from './App.vue' |
||||
import router from './router' |
||||
import Vant from 'vant' |
||||
import 'vant/lib/index.css' |
||||
import './style.css' |
||||
|
||||
const app = createApp(App) |
||||
app.use(Vant) |
||||
app.use(router) |
||||
app.mount('#app') |
||||
@ -0,0 +1,16 @@ |
||||
import { createRouter, createWebHistory } from 'vue-router' |
||||
|
||||
const routes = [ |
||||
{ |
||||
path: '/', |
||||
name: 'Home', |
||||
component: () => import('../views/HomeView.vue') |
||||
} |
||||
] |
||||
|
||||
const router = createRouter({ |
||||
history: createWebHistory(), |
||||
routes |
||||
}) |
||||
|
||||
export default router |
||||
@ -0,0 +1,57 @@ |
||||
import axios from 'axios' |
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({ |
||||
baseURL: '/pro-report', |
||||
timeout: 10000, |
||||
}) |
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use( |
||||
(config) => { |
||||
return config |
||||
}, |
||||
(error) => { |
||||
return Promise.reject(error) |
||||
} |
||||
) |
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use( |
||||
(response) => { |
||||
return response.data |
||||
}, |
||||
(error) => { |
||||
return Promise.reject(error) |
||||
} |
||||
) |
||||
|
||||
// 获取首页配置
|
||||
export const getHomeConfig = () => { |
||||
return api.get('/system/config') |
||||
} |
||||
|
||||
// 获取系统枚举
|
||||
export const getSystemEnum = () => { |
||||
return api.get('/system/enum') |
||||
} |
||||
|
||||
// 获取成果列表
|
||||
export const getAchievementList = (params) => { |
||||
return api.get('/achievement/list', { params }) |
||||
} |
||||
|
||||
// 获取需求列表
|
||||
export const getDemandList = (params) => { |
||||
return api.get('/demand/list', { params }) |
||||
} |
||||
|
||||
// 创建成果
|
||||
export const createAchievement = (data) => { |
||||
return api.post('/achievement/create', data) |
||||
} |
||||
|
||||
// 创建需求
|
||||
export const createDemand = (data) => { |
||||
return api.post('/demand/create', data) |
||||
} |
||||
@ -0,0 +1,74 @@ |
||||
/* 移动端H5样式重置 */ |
||||
:root { |
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; |
||||
line-height: 1.5; |
||||
font-weight: 400; |
||||
color: #333; |
||||
background-color: #f5f5f5; |
||||
font-synthesis: none; |
||||
text-rendering: optimizeLegibility; |
||||
-webkit-font-smoothing: antialiased; |
||||
-moz-osx-font-smoothing: grayscale; |
||||
} |
||||
|
||||
/* 全局样式重置 */ |
||||
* { |
||||
box-sizing: border-box; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
body { |
||||
margin: 0; |
||||
min-width: 320px; |
||||
min-height: 100vh; |
||||
background-color: #f5f5f5; |
||||
} |
||||
|
||||
/* 移除默认的#app样式 */ |
||||
#app { |
||||
max-width: none; |
||||
margin: 0; |
||||
padding: 0; |
||||
text-align: left; |
||||
} |
||||
|
||||
/* 链接样式 */ |
||||
a { |
||||
color: #1e88e5; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
a:hover { |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
/* 标题样式重置 */ |
||||
h1, h2, h3, h4, h5, h6 { |
||||
font-weight: 600; |
||||
line-height: 1.2; |
||||
margin: 0; |
||||
} |
||||
|
||||
h1 { |
||||
font-size: 1.8em; |
||||
} |
||||
|
||||
h2 { |
||||
font-size: 1.5em; |
||||
} |
||||
|
||||
/* 按钮样式重置 */ |
||||
button { |
||||
border: none; |
||||
background: none; |
||||
font-family: inherit; |
||||
font-size: inherit; |
||||
color: inherit; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
/* 卡片样式重置 */ |
||||
.card { |
||||
padding: 0; |
||||
} |
||||
@ -0,0 +1,622 @@ |
||||
<script setup> |
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue' |
||||
import { getHomeConfig } from '../services/api' |
||||
import { showToast } from 'vant' |
||||
import SubmitAchievementForm from '../components/SubmitAchievementForm.vue' |
||||
import SubmitDemandForm from '../components/SubmitDemandForm.vue' |
||||
import AchievementList from '../components/AchievementList.vue' |
||||
import DemandList from '../components/DemandList.vue' |
||||
|
||||
// 定义数据 |
||||
const config = ref({ |
||||
title: '', |
||||
introduction: '', |
||||
serviceHotline: [], |
||||
emails: [], |
||||
address: [] |
||||
}) |
||||
|
||||
const loading = ref(true) |
||||
// 切换标签数据<van-icon name="balance-list" /><van-icon name="add-square" /> |
||||
const tabs = [ |
||||
{ key: 'achievement', name: '成果清单', icon: 'balance-list', color: '#2196f3', activeColor: '#1976d2' }, |
||||
{ key: 'demand', name: '需求清单', icon: 'award', color: '#9c27b0', activeColor: '#7b1fa2' }, |
||||
{ key: 'submit-achievement', name: '成果填报', icon: 'add', color: '#4caf50', activeColor: '#388e3c' }, |
||||
{ key: 'submit-demand', name: '需求填报', icon: 'add-square', color: '#f44336', activeColor: '#d32f2f' } |
||||
] |
||||
|
||||
// 当前激活的标签 |
||||
const activeTab = ref('achievement') |
||||
|
||||
// 处理标签点击 |
||||
const handleTabClick = (tab) => { |
||||
// 更新激活状态 |
||||
activeTab.value = tab.key |
||||
} |
||||
|
||||
// 是否固定标签栏 |
||||
const isTabsFixed = ref(false) |
||||
|
||||
// 标签栏DOM引用 |
||||
const tabsContainer = ref(null) |
||||
|
||||
// 标签栏初始top值 |
||||
const tabsInitialTop = ref(0) |
||||
|
||||
// 滚动监听处理函数 |
||||
const handleScroll = () => { |
||||
if (tabsInitialTop.value === 0) return |
||||
|
||||
const scrollTop = window.scrollY |
||||
|
||||
isTabsFixed.value = scrollTop >= tabsInitialTop.value |
||||
} |
||||
|
||||
// 十六进制颜色转RGB值函数 |
||||
const hexToRgb = (hex) => { |
||||
// 移除#号 |
||||
hex = hex.replace(/^#/, '') |
||||
|
||||
// 解析十六进制值 |
||||
let r = 0, g = 0, b = 0 |
||||
|
||||
if (hex.length === 3) { |
||||
// 简写形式,如 #RGB |
||||
r = parseInt(hex[0] + hex[0], 16) |
||||
g = parseInt(hex[1] + hex[1], 16) |
||||
b = parseInt(hex[2] + hex[2], 16) |
||||
} else if (hex.length === 6) { |
||||
// 完整形式,如 #RRGGBB |
||||
r = parseInt(hex.substring(0, 2), 16) |
||||
g = parseInt(hex.substring(2, 4), 16) |
||||
b = parseInt(hex.substring(4, 6), 16) |
||||
} |
||||
|
||||
return `${r}, ${g}, ${b}` |
||||
} |
||||
|
||||
// 页面挂载时获取数据和添加滚动监听 |
||||
onMounted(async () => { |
||||
// 获取数据 |
||||
try { |
||||
loading.value = true |
||||
const response = await getHomeConfig() |
||||
if (response.code === 1) { |
||||
config.value = response.data |
||||
} else { |
||||
showToast('获取数据失败') |
||||
} |
||||
} catch (error) { |
||||
console.error('请求失败:', error) |
||||
showToast('网络错误,请稍后重试') |
||||
} finally { |
||||
loading.value = false |
||||
} |
||||
|
||||
// 等待DOM更新后获取初始位置 |
||||
nextTick(() => { |
||||
if (tabsContainer.value) { |
||||
tabsInitialTop.value = tabsContainer.value.offsetTop |
||||
} |
||||
}) |
||||
|
||||
// 添加滚动监听 |
||||
window.addEventListener('scroll', handleScroll) |
||||
}) |
||||
|
||||
// 页面卸载时移除滚动监听 |
||||
onUnmounted(() => { |
||||
window.removeEventListener('scroll', handleScroll) |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="home"> |
||||
<!-- 顶部标题区域 --> |
||||
<div class="header"> |
||||
<h1 class="title">{{ config.title }}</h1> |
||||
</div> |
||||
|
||||
<!-- 内容区 --> |
||||
<div class="content"> |
||||
<!-- 简介区域 --> |
||||
<div class="card intro-card"> |
||||
<!-- <h2 class="section-title">平台简介</h2> --> |
||||
<div v-if="loading" class="loading-container"> |
||||
<van-skeleton title :row="3" animated /> |
||||
</div> |
||||
<div v-else class="intro-content">{{ config.introduction }}</div> |
||||
</div> |
||||
|
||||
<!-- 切换按钮区域 --> |
||||
<div class="tabs-container" ref="tabsContainer" :class="{ 'tabs-fixed': isTabsFixed }"> |
||||
<div class="tabs-wrapper"> |
||||
<div |
||||
v-for="tab in tabs" |
||||
:key="tab.key" |
||||
class="tab-item" |
||||
:style="{ |
||||
'--tab-color': activeTab === tab.key ? tab.activeColor : tab.color, |
||||
'--tab-color-rgb': hexToRgb(activeTab === tab.key ? tab.activeColor : tab.color) |
||||
}" |
||||
:class="{ active: activeTab === tab.key }" |
||||
@click="handleTabClick(tab)" |
||||
> |
||||
<van-icon :name="tab.icon" size="18" /> |
||||
<span class="tab-text">{{ tab.name }}</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 切换内容区域 --> |
||||
<div class="tab-content"> |
||||
<!-- 成果清单 --> |
||||
<div v-if="activeTab === 'achievement'" class="tab-panel"> |
||||
<AchievementList /> |
||||
</div> |
||||
|
||||
<!-- 需求清单 --> |
||||
<div v-else-if="activeTab === 'demand'" class="tab-panel"> |
||||
<DemandList /> |
||||
</div> |
||||
|
||||
<!-- 成果填报 --> |
||||
<div v-else-if="activeTab === 'submit-achievement'" class="tab-panel"> |
||||
<SubmitAchievementForm /> |
||||
</div> |
||||
|
||||
<!-- 需求填报 --> |
||||
<div v-else-if="activeTab === 'submit-demand'" class="tab-panel"> |
||||
<SubmitDemandForm /> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 底部咨询对接区域 --> |
||||
<div class="contact-section"> |
||||
<div class="contact-card"> |
||||
<div class="contact-title-container"> |
||||
<van-icon name="chat-o" size="20" color="#4caf50" /> |
||||
<h2 class="contact-title">咨询对接</h2> |
||||
</div> |
||||
|
||||
<!-- 服务热线 --> |
||||
<div class="contact-item-card phone-card"> |
||||
<div class="contact-item-header"> |
||||
<van-icon name="phone-o" size="18" color="#2196f3" /> |
||||
<span class="contact-item-label">服务热线</span> |
||||
</div> |
||||
<div class="contact-item-content"> |
||||
<div v-for="(item, index) in config.serviceHotline" :key="index" class="contact-person"> |
||||
<span class="person-name">{{ item.name }}</span> |
||||
<a :href="`tel:${item.phone}`" class="person-phone">{{ item.phone }}</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 邮箱咨询 --> |
||||
<div class="contact-item-card email-card"> |
||||
<div class="contact-item-header"> |
||||
<van-icon name="email-o" size="18" color="#9c27b0" /> |
||||
<span class="contact-item-label">邮箱咨询</span> |
||||
</div> |
||||
<div class="contact-item-content"> |
||||
<a v-for="(email, index) in config.emails" :key="index" :href="`mailto:${email}`" class="contact-email"> |
||||
{{ email }} |
||||
</a> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- 办公地址 --> |
||||
<div class="contact-item-card address-card"> |
||||
<div class="contact-item-header"> |
||||
<van-icon name="location-o" size="18" color="#4caf50" /> |
||||
<span class="contact-item-label">办公地址</span> |
||||
</div> |
||||
<div class="contact-item-content"> |
||||
<div v-for="(addr, index) in config.address" :key="index" class="contact-address"> |
||||
{{ addr }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
/* 全局样式重置和基础设置 */ |
||||
.home { |
||||
min-height: 100vh; |
||||
background-color: #f5f5f5; |
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
||||
} |
||||
|
||||
/* 顶部标题区域 */ |
||||
.header { |
||||
background: linear-gradient(135deg, #2563eb 0%, #7e22ce 100%); |
||||
color: white; |
||||
padding: 30px 20px; |
||||
text-align: center; |
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
.title { |
||||
font-size: 22px; |
||||
font-weight: 700; |
||||
margin: 0; |
||||
line-height: 1.4; |
||||
word-break: break-word; |
||||
} |
||||
|
||||
/* 内容区 */ |
||||
.content { |
||||
padding: 15px; |
||||
} |
||||
|
||||
/* 卡片样式 */ |
||||
.card { |
||||
background-color: white; |
||||
border-radius: 12px; |
||||
padding: 15px; |
||||
margin-bottom: 15px; |
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); |
||||
transition: transform 0.2s ease, box-shadow 0.2s ease; |
||||
} |
||||
|
||||
.card:active { |
||||
transform: translateY(1px); |
||||
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.05); |
||||
} |
||||
|
||||
/* 标题样式 */ |
||||
.section-title { |
||||
font-size: 18px; |
||||
font-weight: 600; |
||||
color: #333; |
||||
margin: 0 0 15px 0; |
||||
padding-bottom: 8px; |
||||
border-bottom: 2px solid #1e88e5; |
||||
} |
||||
|
||||
/* 加载状态 */ |
||||
.loading-container { |
||||
padding: 10px 0; |
||||
} |
||||
|
||||
/* 简介内容 */ |
||||
.intro-content { |
||||
font-size: 14px; |
||||
line-height: 1.7; |
||||
color: #555; |
||||
text-align: justify; |
||||
} |
||||
|
||||
/* 正文占位符 */ |
||||
.content-placeholder { |
||||
text-align: center; |
||||
padding: 30px 15px; |
||||
color: #999; |
||||
font-size: 15px; |
||||
background-color: #fafafa; |
||||
border-radius: 8px; |
||||
margin-top: 5px; |
||||
} |
||||
|
||||
/* 底部咨询对接区域 */ |
||||
.contact-section { |
||||
background-color: #f5f5f5; |
||||
padding: 20px 15px; |
||||
margin-top: auto; |
||||
} |
||||
|
||||
/* 整体咨询卡片 */ |
||||
.contact-card { |
||||
background-color: white; |
||||
border-radius: 12px; |
||||
padding: 20px; |
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); |
||||
overflow: hidden; |
||||
} |
||||
|
||||
/* 咨询标题容器 */ |
||||
.contact-title-container { |
||||
display: flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
gap: 10px; |
||||
margin-bottom: 20px; |
||||
} |
||||
|
||||
/* 咨询标题 */ |
||||
.contact-title { |
||||
font-size: 18px; |
||||
font-weight: 600; |
||||
color: #333; |
||||
margin: 0; |
||||
} |
||||
|
||||
/* 单个咨询项卡片 */ |
||||
.contact-item-card { |
||||
border-radius: 10px; |
||||
padding: 15px; |
||||
margin-bottom: 15px; |
||||
background-color: #fafafa; |
||||
transition: all 0.2s ease; |
||||
} |
||||
|
||||
.contact-item-card:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
/* 服务热线卡片样式 */ |
||||
.phone-card { |
||||
background-color: rgba(33, 150, 243, 0.05); |
||||
border-left: 4px solid #2196f3; |
||||
} |
||||
|
||||
.phone-card .contact-item-label { |
||||
color: #2196f3; |
||||
} |
||||
|
||||
.phone-card .person-phone { |
||||
color: #2196f3; |
||||
} |
||||
|
||||
/* 邮箱咨询卡片样式 */ |
||||
.email-card { |
||||
background-color: rgba(156, 39, 176, 0.05); |
||||
border-left: 4px solid #9c27b0; |
||||
} |
||||
|
||||
.email-card .contact-item-label { |
||||
color: #9c27b0; |
||||
} |
||||
|
||||
.email-card .contact-email { |
||||
color: #9c27b0; |
||||
} |
||||
|
||||
/* 办公地址卡片样式 */ |
||||
.address-card { |
||||
background-color: rgba(76, 175, 80, 0.05); |
||||
border-left: 4px solid #4caf50; |
||||
} |
||||
|
||||
.address-card .contact-item-label { |
||||
color: #4caf50; |
||||
} |
||||
|
||||
.address-card .contact-address { |
||||
color: #4caf50; |
||||
} |
||||
|
||||
/* 咨询项头部 */ |
||||
.contact-item-header { |
||||
display: flex; |
||||
align-items: center; |
||||
gap: 8px; |
||||
margin-bottom: 12px; |
||||
font-size: 16px; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
/* 咨询项标签 */ |
||||
.contact-item-label { |
||||
font-weight: 500; |
||||
} |
||||
|
||||
/* 咨询项内容 */ |
||||
.contact-item-content { |
||||
padding-left: 26px; |
||||
} |
||||
|
||||
/* 联系人信息 */ |
||||
.contact-person { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
margin-bottom: 8px; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.contact-person:last-child { |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
/* 联系人姓名 */ |
||||
.person-name { |
||||
color: #666; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
/* 联系人电话 */ |
||||
.person-phone { |
||||
font-weight: 500; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
/* 联系邮箱 */ |
||||
.contact-email { |
||||
display: block; |
||||
font-size: 14px; |
||||
text-decoration: none; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
/* 联系地址 */ |
||||
.contact-address { |
||||
display: block; |
||||
font-size: 14px; |
||||
font-weight: 500; |
||||
line-height: 1.6; |
||||
} |
||||
|
||||
/* 切换按钮区域 */ |
||||
.tabs-container { |
||||
margin: 0; |
||||
background-color: white; |
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); |
||||
z-index: 10; |
||||
transition: all 0.3s ease; |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
/* 固定状态 */ |
||||
.tabs-fixed { |
||||
position: fixed; |
||||
top: 0; |
||||
left: 0; |
||||
right: 0; |
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1); |
||||
} |
||||
|
||||
/* 切换按钮容器 */ |
||||
.tabs-wrapper { |
||||
display: flex; |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
margin: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
/* 确保所有状态下按钮容器的宽度正确 */ |
||||
.tabs-container { |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
/* 固定状态下的按钮容器 */ |
||||
.tabs-fixed .tabs-wrapper { |
||||
width: 100%; |
||||
display: flex; |
||||
flex-direction: row; |
||||
flex-wrap: nowrap; |
||||
} |
||||
|
||||
/* 确保固定状态下按钮容器不超出视口 */ |
||||
.tabs-fixed { |
||||
width: 100%; |
||||
box-sizing: border-box; |
||||
} |
||||
|
||||
/* 切换按钮项 */ |
||||
.tab-item { |
||||
flex: 1 1 25%; |
||||
width: 25%; |
||||
padding: 12px 0; |
||||
text-align: center; |
||||
font-size: 13px; |
||||
font-weight: 500; |
||||
color: var(--tab-color); |
||||
cursor: pointer; |
||||
position: relative; |
||||
transition: all 0.2s ease; |
||||
display: flex; |
||||
flex-direction: column; |
||||
align-items: center; |
||||
gap: 6px; |
||||
border-left: 1px solid #e5e5e5; |
||||
background-color: white; |
||||
box-sizing: border-box; |
||||
min-width: 0; |
||||
} |
||||
|
||||
.tab-item:first-child { |
||||
border-left: none; |
||||
} |
||||
|
||||
.tab-item:active { |
||||
opacity: 0.9; |
||||
} |
||||
|
||||
/* 激活状态 */ |
||||
.tab-item.active { |
||||
font-weight: 600; |
||||
/* 使用对应颜色的深色背景作为选中效果 */ |
||||
background-color: var(--tab-color); |
||||
} |
||||
|
||||
/* 激活状态的图标 */ |
||||
.tab-item.active .van-icon { |
||||
color: white; |
||||
} |
||||
|
||||
/* 激活状态的文字 */ |
||||
.tab-item.active .tab-text { |
||||
color: white; |
||||
} |
||||
|
||||
/* 非激活状态的图标 */ |
||||
.tab-item:not(.active) .van-icon { |
||||
color: var(--tab-color); |
||||
font-size: 18px; |
||||
} |
||||
|
||||
/* 非激活状态的文字 */ |
||||
.tab-item:not(.active) .tab-text { |
||||
color: var(--tab-color); |
||||
font-size: 13px; |
||||
} |
||||
|
||||
/* 切换内容区域 */ |
||||
.tab-content { |
||||
margin-top: 15px; |
||||
} |
||||
|
||||
/* 切换面板 */ |
||||
.tab-panel { |
||||
background-color: white; |
||||
border-radius: 12px; |
||||
padding: 15px; |
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); |
||||
min-height: 400px; |
||||
} |
||||
|
||||
/* 成果填报表单容器 */ |
||||
.tab-panel :deep(.submit-achievement-form) { |
||||
padding: 0; |
||||
} |
||||
|
||||
/* 适配小屏幕 */ |
||||
@media (max-width: 375px) { |
||||
.tab-panel { |
||||
padding: 12px; |
||||
} |
||||
} |
||||
|
||||
/* 适配小屏幕 */ |
||||
@media (max-width: 375px) { |
||||
.header { |
||||
padding: 25px 15px; |
||||
} |
||||
|
||||
.title { |
||||
font-size: 20px; |
||||
} |
||||
|
||||
.content { |
||||
padding: 12px; |
||||
} |
||||
|
||||
.section-title { |
||||
font-size: 17px; |
||||
} |
||||
|
||||
.contact-section { |
||||
padding: 15px 12px; |
||||
} |
||||
|
||||
/* 小屏幕切换按钮 */ |
||||
.tab-item { |
||||
font-size: 14px; |
||||
padding: 12px 0; |
||||
} |
||||
|
||||
.tab-panel { |
||||
padding: 15px; |
||||
min-height: 300px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -0,0 +1,27 @@ |
||||
import { defineConfig } from 'vite' |
||||
import vue from '@vitejs/plugin-vue' |
||||
import AutoImport from 'unplugin-auto-import/vite' |
||||
import Components from 'unplugin-vue-components/vite' |
||||
import { VantResolver } from 'unplugin-vue-components/resolvers' |
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({ |
||||
plugins: [ |
||||
vue(), |
||||
// AutoImport({
|
||||
// resolvers: [VantResolver()],
|
||||
// }),
|
||||
// Components({
|
||||
// resolvers: [VantResolver()],
|
||||
// }),
|
||||
], |
||||
server: { |
||||
proxy: { |
||||
'/pro-report': { |
||||
target: 'http://47.109.32.96:9999', |
||||
changeOrigin: true, |
||||
secure: false, |
||||
}, |
||||
}, |
||||
}, |
||||
}) |
||||
Loading…
Reference in new issue