detail.vue 6.5 KB

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