detail.vue 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. <template>
  2. <view class="detail-container">
  3. <view class="header-card">
  4. <view class="title">{{ projectStore.projectDetailData.projectName || '未知名称' }}</view>
  5. <view class="meta-row">
  6. <view class="leader" v-if="projectStore.projectDetailData.projectLeaderName || projectStore.projectDetailData.manager">
  7. 项目负责人:{{ projectStore.projectDetailData.projectLeaderName || projectStore.projectDetailData.manager }}
  8. </view>
  9. <view class="tags">
  10. <text class="tag bg-blue">{{ getProjectTypeName(projectType) }}</text>
  11. </view>
  12. </view>
  13. </view>
  14. <view class="tabs-container">
  15. <uv-tabs :list="tabList" :current="currentTab" @click="handleTabClick" lineColor="#1c9bfd" activeColor="#1c9bfd"
  16. inactiveColor="#666" :scrollable="true"></uv-tabs>
  17. </view>
  18. <scroll-view class="content-area" scroll-y>
  19. <view class="component-wrapper">
  20. <component
  21. :is="currentComponentName"
  22. :projectId="projectId"
  23. :projectType="projectType"
  24. :projectData="projectStore.projectDetailData"
  25. ></component>
  26. </view>
  27. </scroll-view>
  28. </view>
  29. </template>
  30. <script setup lang="ts">
  31. import { onLoad } from '@dcloudio/uni-app';
  32. import { ref, computed } from 'vue';
  33. import { useProjectStore } from '@/store/modules/project';
  34. import { getProjectTypeName } from '@/constants';
  35. // 引入全部模块组件
  36. import ProjectSetup from './components/detail/ProjectSetup.vue';
  37. import ProjectMembers from './components/detail/ProjectMembers.vue';
  38. import ProjectCooperates from './components/detail/ProjectCooperates.vue';
  39. import ProjectBudget from './components/detail/ProjectBudget.vue';
  40. import ProjectDocs from './components/detail/ProjectDocs.vue';
  41. import ProjectProcess from './components/detail/ProjectProcess.vue';
  42. import ProjectChanges from './components/detail/ProjectChanges.vue';
  43. import ProjectMidterm from './components/detail/ProjectMidterm.vue';
  44. import ProjectClosing from './components/detail/ProjectClosing.vue';
  45. import ProjectFunding from './components/detail/ProjectFunding.vue';
  46. import ProjectAchievements from './components/detail/ProjectAchievements.vue';
  47. import ProjectInspection from './components/detail/ProjectInspection.vue';
  48. import ProjectApproval from './components/detail/ProjectApproval.vue';
  49. import ProjectEthical from './components/detail/ProjectEthical.vue';
  50. // 注册动态组件
  51. const componentsMap: Record<string, any> = {
  52. ProjectSetup,
  53. ProjectMembers,
  54. ProjectCooperates,
  55. ProjectBudget,
  56. ProjectDocs,
  57. ProjectProcess,
  58. ProjectChanges,
  59. ProjectMidterm,
  60. ProjectClosing,
  61. ProjectFunding,
  62. ProjectAchievements,
  63. ProjectInspection,
  64. ProjectApproval,
  65. ProjectEthical
  66. };
  67. const projectType = ref('');
  68. const projectId = ref(0);
  69. const projectStore = useProjectStore();
  70. // 标签页当前激活项索引
  71. const currentTab = ref(0);
  72. /**
  73. * 标签页配置
  74. * 根据项目类型动态调整需要显示的模块 Tab
  75. */
  76. const tabList = computed(() => {
  77. const tabs = [
  78. { name: '立项信息', component: 'ProjectSetup' }
  79. ];
  80. if (projectType.value !== 'horizontal') {
  81. tabs.push({ name: '项目预算', component: 'ProjectBudget' });
  82. }
  83. tabs.push({ name: '项目成员', component: 'ProjectMembers' });
  84. if (projectType.value === 'horizontal') {
  85. tabs.push({ name: '合作单位', component: 'ProjectCooperates' });
  86. }
  87. tabs.push(
  88. { name: '项目文档', component: 'ProjectDocs' },
  89. { name: '科研成果', component: 'ProjectAchievements' },
  90. { name: '经费信息', component: 'ProjectFunding' },
  91. { name: '中检信息', component: 'ProjectInspection' },
  92. { name: '结题信息', component: 'ProjectClosing' },
  93. { name: '变更信息', component: 'ProjectChanges' },
  94. { name: '审批信息', component: 'ProjectApproval' },
  95. { name: '伦理信息', component: 'ProjectEthical' }
  96. );
  97. return tabs;
  98. });
  99. /**
  100. * 动态组件计算
  101. * 根据当前 Tab 索引返回对应的 Vue 组件定义
  102. */
  103. const currentComponentName = computed(() => {
  104. const comp = tabList.value[currentTab.value];
  105. if (!comp) return '';
  106. return componentsMap[comp.component];
  107. });
  108. /**
  109. * 页面加载生命周期
  110. * 1. 解析路由参数(项目类型、ID)
  111. * 2. 调用 store 获取项目详细信息数据
  112. */
  113. onLoad((options: any) => {
  114. if (options.type) projectType.value = options.type;
  115. if (options.id) projectId.value = Number(options.id);
  116. if (projectId.value && projectType.value) {
  117. uni.showLoading({ title: '加载中...' });
  118. // 业务逻辑交由 Store 处理,页面只负责触发
  119. projectStore.fetchProjectDetailData(projectId.value, projectType.value)
  120. .finally(() => {
  121. uni.hideLoading();
  122. });
  123. }
  124. });
  125. /**
  126. * 标签切换点击处理
  127. */
  128. const handleTabClick = (item: any) => {
  129. currentTab.value = item.index;
  130. };
  131. /**
  132. * 通用动作处理 (占位)
  133. */
  134. const handleAction = () => {
  135. uni.showToast({ title: '功能开发中', icon: 'none' });
  136. };
  137. </script>
  138. <style lang="scss" scoped>
  139. .detail-container {
  140. height: 100vh;
  141. display: flex;
  142. flex-direction: column;
  143. background-color: #f5f7fa;
  144. box-sizing: border-box;
  145. }
  146. .header-card {
  147. flex-shrink: 0;
  148. background: linear-gradient(135deg, #1c9bfd 0%, #15a982 100%);
  149. padding: 40rpx 30rpx 80rpx;
  150. color: #fff;
  151. border-bottom-left-radius: 40rpx;
  152. border-bottom-right-radius: 40rpx;
  153. box-shadow: 0 10rpx 20rpx rgba(28, 155, 253, 0.2);
  154. .title {
  155. font-size: 40rpx;
  156. font-weight: bold;
  157. margin-bottom: 20rpx;
  158. line-height: 1.4;
  159. }
  160. .meta-row {
  161. display: flex;
  162. align-items: center;
  163. justify-content: space-between;
  164. gap: 20rpx;
  165. }
  166. .leader {
  167. font-size: 28rpx;
  168. opacity: 0.9;
  169. }
  170. .tags {
  171. display: flex;
  172. flex-wrap: wrap;
  173. gap: 16rpx;
  174. .tag {
  175. font-size: 24rpx;
  176. padding: 6rpx 20rpx;
  177. border-radius: 30rpx;
  178. border: 2rpx solid rgba(255, 255, 255, 0.4);
  179. background: rgba(255, 255, 255, 0.1);
  180. backdrop-filter: blur(4px);
  181. }
  182. }
  183. }
  184. .tabs-container {
  185. flex-shrink: 0;
  186. margin: -40rpx 30rpx 20rpx;
  187. background-color: #fff;
  188. border-radius: 16rpx;
  189. padding: 10rpx 0;
  190. box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
  191. position: relative;
  192. z-index: 10;
  193. }
  194. .content-area {
  195. flex: 1;
  196. height: 0;
  197. /* 必须设置高度以便 scroll-view 撑开 */
  198. }
  199. .component-wrapper {
  200. margin: 0 30rpx;
  201. }
  202. .bottom-bar {
  203. position: fixed;
  204. bottom: 0;
  205. left: 0;
  206. width: 100%;
  207. background-color: #fff;
  208. padding: 20rpx 30rpx;
  209. box-sizing: border-box;
  210. box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
  211. z-index: 99;
  212. }
  213. </style>