InspDetailPopup.vue 10.0 KB

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