初始化项目,包含需求清单、成果清单、需求填报和成果填报功能

master
Swanky 2 months ago
commit 09a3494e77
  1. 24
      .gitignore
  2. 3
      .vscode/extensions.json
  3. 5
      README.md
  4. 13
      index.html
  5. 3013
      package-lock.json
  6. 25
      package.json
  7. 9
      postcss.config.js
  8. 1
      public/vite.svg
  9. 30
      src/App.vue
  10. 1
      src/assets/vue.svg
  11. 538
      src/components/AchievementList.vue
  12. 551
      src/components/DemandList.vue
  13. 43
      src/components/HelloWorld.vue
  14. 461
      src/components/SubmitAchievementForm.vue
  15. 385
      src/components/SubmitDemandForm.vue
  16. 12
      src/main.js
  17. 16
      src/router/index.js
  18. 57
      src/services/api.js
  19. 74
      src/style.css
  20. 622
      src/views/HomeView.vue
  21. 27
      vite.config.js

24
.gitignore vendored

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

3013
package-lock.json generated

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,
},
},
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

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>

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

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…
Cancel
Save