InspDetailPopup.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <template>
  2. <uv-popup ref="popupRef" mode="bottom" round="20" :safeAreaInsetBottom="true">
  3. <view class="popup-container">
  4. <view class="popup-header">
  5. <text class="title">中检详情</text>
  6. <uv-icon name="close" size="36rpx" color="#999" @click="close"></uv-icon>
  7. </view>
  8. <scroll-view class="popup-content" scroll-y v-if="!projectStore.fetchInspListLoading">
  9. <uv-tabs :list="tabList" :current="currentTab" @click="handleTabClick" lineColor="#1c9bfd" activeColor="#1c9bfd" inactiveColor="#666"></uv-tabs>
  10. <view class="tab-pane" v-show="currentTab === 0">
  11. <!-- 基本信息 -->
  12. <CommonSection title="基本信息">
  13. <CommonInfoRow label="中检批次名称" :value="projectStore.inspDetailData.batchName" />
  14. <CommonInfoRow label="项目名称" :value="projectStore.inspDetailData.projectTitle || projectStore.inspDetailData.projectName" />
  15. <CommonInfoRow label="负责人" :value="projectStore.inspDetailData.projectHeadName" />
  16. <CommonInfoRow
  17. label="中检日期"
  18. :value="(projectStore.inspDetailData.batchStartDate ? formatDate(projectStore.inspDetailData.batchStartDate) : '--') + ' ~ ' + (projectStore.inspDetailData.batchEndDate ? formatDate(projectStore.inspDetailData.batchEndDate) : '--')"
  19. />
  20. <view v-if="batchInformList && batchInformList.length > 0" style="margin-top: 20rpx;">
  21. <CommonFileList :files="batchInformList.map((f: any) => ({ fileName: f.name, fileUrl: f.url }))" />
  22. </view>
  23. </CommonSection>
  24. </view>
  25. <view class="tab-pane" v-show="currentTab === 1">
  26. <!-- 中检信息 (普通项目展示方式) -->
  27. <template v-if="localProjectType !== 'horizontal'">
  28. <CommonSection title="中检信息">
  29. <CommonInfoRow label="项目实施周期" :value="projectStore.inspDetailData.implementCycle + ' 月'" />
  30. <CommonInfoRow label="项目进行时间" :value="projectStore.inspDetailData.implementExecute + ' 月'" />
  31. <CommonInfoRow label="项目完成占比" :value="projectStore.inspDetailData.implementCycleProp + ' %'" />
  32. <CommonInfoRow label="项目经费总额" :value="formatWithComma(projectStore.inspDetailData.fundsTotal) + ' 元'" isAmount noBorder />
  33. </CommonSection>
  34. </template>
  35. <!-- 中检信息 (横向项目展示方式) -->
  36. <template v-else>
  37. <CommonSection title="中检信息">
  38. <CommonInfoRow label="项目名称" :value="projectStore.inspDetailData.projectTitle" />
  39. <CommonInfoRow
  40. label="项目日期"
  41. :value="formatDate(projectStore.inspDetailData.projectStartDate) + ' ~ ' + formatDate(projectStore.inspDetailData.projectEndDate)"
  42. />
  43. <CommonInfoRow label="实施周期" :value="projectStore.inspDetailData.implCycle" />
  44. <CommonInfoRow label="项目完成占比" :value="projectStore.inspDetailData.implementCycleProp + '%'" />
  45. <CommonInfoRow
  46. :label="projectStore.inspDetailData.isClinicalTrial == '10' ? '合同经费' : '科研经费'"
  47. :value="formatWithComma(projectStore.inspDetailData.fundsTotal) + ' 元'"
  48. isAmount
  49. />
  50. <CommonInfoRow label="经费使用总额" :value="formatWithComma(projectStore.inspDetailData.fundsUsed) + ' 元'" />
  51. <CommonInfoRow label="费用使用占比" :value="(projectStore.inspDetailData.fundsProp || '0') + '%'" />
  52. <CommonInfoRow label="项目进展" :value="projectStore.inspDetailData.projectProgress" isColumn noBorder />
  53. </CommonSection>
  54. </template>
  55. <!-- 附件信息 -->
  56. <CommonSection title="附件信息" v-if="projectStore.inspDetailData.fileList && projectStore.inspDetailData.fileList.length > 0">
  57. <CommonFileList :files="projectStore.inspDetailData.fileList" />
  58. </CommonSection>
  59. </view>
  60. <view class="tab-pane" v-show="currentTab === 2">
  61. <!-- 审批信息 -->
  62. <CommonSection title="审批信息" v-if="projectStore.inspDetailData.approvalStatus > 10">
  63. <FlowTable
  64. :id="projectStore.inspDetailData.id"
  65. :businessCode="String(projectStore.inspDetailData.id)"
  66. defCode="sci_project_inspection"
  67. />
  68. </CommonSection>
  69. <uv-empty v-else mode="data" text="暂无审批信息"></uv-empty>
  70. </view>
  71. </scroll-view>
  72. <view class="loading-wrap" v-else>
  73. <uv-loading-icon text="数据加载中..." :vertical="true"></uv-loading-icon>
  74. </view>
  75. </view>
  76. </uv-popup>
  77. </template>
  78. <script setup lang="ts">
  79. import { ref, computed } from 'vue';
  80. import { useProjectStore } from '@/store/modules/project';
  81. import FlowTable from './FlowTable.vue';
  82. import { formatDate } from '@/utils/date';
  83. import { formatWithComma } from '@/utils/format';
  84. import { previewFile } from '@/utils/file';
  85. import CommonSection from '@/components/ui/CommonSection.vue';
  86. import CommonInfoRow from '@/components/ui/CommonInfoRow.vue';
  87. import CommonFileList from '@/components/ui/CommonFileList.vue';
  88. /**
  89. * 中检详情弹窗组件
  90. * 根据项目类型(纵向/横向/自发)展示不同的业务字段
  91. */
  92. const projectStore = useProjectStore();
  93. const popupRef = ref<any>(null);
  94. const localProjectType = ref('');
  95. /**
  96. * 计算属性:中检批次通知附件解析
  97. * 数据来源为 JSON 字符串,需解析为数组对象以便渲染
  98. */
  99. const batchInformList = computed(() => {
  100. const inform = projectStore.inspDetailData.batchInform;
  101. if (!inform) return [];
  102. try {
  103. return JSON.parse(inform);
  104. } catch (e) {
  105. return [];
  106. }
  107. });
  108. /**
  109. * 计算属性:Tab 页标题配置
  110. * 只有在已提交审批(审批状态 > 10)时才显示“审批信息”页签
  111. */
  112. const tabList = computed(() => {
  113. const tabs = [
  114. { name: '基本信息' },
  115. { name: '中检信息' }
  116. ];
  117. if (projectStore.inspDetailData.approvalStatus > 10) {
  118. tabs.push({ name: '审批信息' });
  119. }
  120. return tabs;
  121. });
  122. // 当前激活的 tab index
  123. const currentTab = ref(0);
  124. /**
  125. * 标签页点击切换处理
  126. */
  127. const handleTabClick = (item: any) => {
  128. currentTab.value = item.index;
  129. };
  130. /**
  131. * 外部调用开启详情弹窗
  132. * @param row 原始列表单条数据对象
  133. * @param type 项目类型标识 (vertical/horizontal/spontaneity)
  134. */
  135. const open = async (row: any, type: string = '') => {
  136. localProjectType.value = type;
  137. popupRef.value?.open();
  138. // 重置为第一个标签页
  139. currentTab.value = 0;
  140. // 调用 store 发起异步请求获取完整中检详情数据
  141. await projectStore.fetchInspDetail(row.id, '', type);
  142. /**
  143. * 业务逻辑补充:处理嵌套的文件字段反序列化
  144. * 针对支出明细等关联数据中的 fileUrl 字段进行二次解析
  145. */
  146. if (projectStore.inspDetailData.expenseList && projectStore.inspDetailData.expenseList.length > 0) {
  147. projectStore.inspDetailData.expenseList.forEach((item: any) => {
  148. if (item.fileUrl) {
  149. try {
  150. item.file = JSON.parse(item.fileUrl);
  151. } catch {
  152. item.file = null;
  153. }
  154. }
  155. });
  156. }
  157. };
  158. /**
  159. * 关闭弹窗逻辑
  160. */
  161. const close = () => {
  162. popupRef.value?.close();
  163. };
  164. /**
  165. * 统一附件预览处理
  166. * 提取文件链接并调用全局封装的预览工具
  167. */
  168. const handleDownload = (file: any) => {
  169. const url = file.url || file.fileUrl;
  170. if (!url) {
  171. uni.showToast({ title: '无法获取附件地址', icon: 'none' });
  172. return;
  173. }
  174. previewFile(url, file.name || file.fileName);
  175. };
  176. defineExpose({ open, close });
  177. </script>
  178. <style lang="scss" scoped>
  179. .popup-container {
  180. height: 85vh;
  181. display: flex;
  182. flex-direction: column;
  183. background-color: #f7f8fa;
  184. }
  185. .popup-header {
  186. flex-shrink: 0;
  187. display: flex;
  188. justify-content: space-between;
  189. align-items: center;
  190. padding: 30rpx 40rpx;
  191. background-color: #fff;
  192. border-bottom: 2rpx solid #eee;
  193. .title {
  194. font-size: 34rpx;
  195. font-weight: bold;
  196. color: #343A3F;
  197. }
  198. }
  199. .loading-wrap {
  200. flex: 1;
  201. display: flex;
  202. align-items: center;
  203. justify-content: center;
  204. }
  205. .popup-content {
  206. flex: 1;
  207. overflow: hidden;
  208. background-color: #f7f8fa;
  209. }
  210. .tab-pane {
  211. padding: 24rpx;
  212. display: flex;
  213. flex-direction: column;
  214. gap: 24rpx;
  215. }
  216. .section {
  217. background-color: #fff;
  218. border-radius: 16rpx;
  219. padding: 30rpx;
  220. margin-bottom: 20rpx;
  221. .section-title {
  222. font-size: 30rpx;
  223. font-weight: bold;
  224. color: #343A3F;
  225. display: flex;
  226. align-items: center;
  227. margin-bottom: 24rpx;
  228. .icon {
  229. width: 8rpx;
  230. height: 30rpx;
  231. background-color: #1c9bfd;
  232. border-radius: 4rpx;
  233. margin-right: 16rpx;
  234. }
  235. }
  236. }
  237. .info-list {
  238. display: flex;
  239. flex-direction: column;
  240. .info-item {
  241. display: flex;
  242. padding: 16rpx 0;
  243. border-bottom: 2rpx dashed #f5f5f5;
  244. font-size: 28rpx;
  245. line-height: 1.5;
  246. &:last-child {
  247. border-bottom: none;
  248. }
  249. .label {
  250. color: #343A3F;
  251. width: 180rpx;
  252. flex-shrink: 0;
  253. }
  254. .value {
  255. flex: 1;
  256. color: #585858;
  257. word-break: break-all;
  258. text-align: right;
  259. &.text-left {
  260. text-align: left;
  261. margin-top: 10rpx;
  262. }
  263. }
  264. &.block-item {
  265. flex-direction: column;
  266. align-items: flex-start;
  267. .label {
  268. width: 100%;
  269. margin-bottom: 10rpx;
  270. }
  271. .value {
  272. text-align: left;
  273. width: 100%;
  274. }
  275. }
  276. .amount {
  277. color: #ff4d4f;
  278. font-weight: bold;
  279. }
  280. }
  281. }
  282. .links-col {
  283. display: flex;
  284. flex-direction: column;
  285. gap: 12rpx;
  286. }
  287. .file-list {
  288. display: flex;
  289. flex-direction: column;
  290. gap: 20rpx;
  291. .file-item {
  292. display: flex;
  293. align-items: center;
  294. justify-content: space-between;
  295. padding: 20rpx;
  296. background-color: #f7f9fa;
  297. border-radius: 8rpx;
  298. .file-type {
  299. font-size: 26rpx;
  300. color: #666;
  301. }
  302. .file-name {
  303. font-size: 26rpx;
  304. text-decoration: underline;
  305. max-width: 60%;
  306. overflow: hidden;
  307. text-overflow: ellipsis;
  308. white-space: nowrap;
  309. text-align: right;
  310. }
  311. }
  312. }
  313. .text-blue {
  314. color: #1c9bfd;
  315. }
  316. .text-red {
  317. color: #ff4d4f;
  318. }
  319. </style>