ProjectAchievements.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <template>
  2. <view class="module-container">
  3. <uv-empty v-if="projectStore.fetchAchievementLoading" mode="list" text="加载中..."></uv-empty>
  4. <view v-else-if="isEmpty" class="empty-wrap">
  5. <uv-empty mode="data" text="暂无科研成果"></uv-empty>
  6. </view>
  7. <view v-else class="list-wrapper">
  8. <!-- 标签页切换 -->
  9. <view class="tabs-wrapper">
  10. <uv-tabs :list="achievementTabs" :current="currentTab" @click="handleTabClick" lineColor="#1c9bfd" activeColor="#1c9bfd" inactiveColor="#666"></uv-tabs>
  11. </view>
  12. <!-- 学术论文 -->
  13. <view class="section" v-show="currentTab === 0">
  14. <uv-empty v-if="!projectStore.paperData || projectStore.paperData.length === 0" mode="data" text="暂无学术论文"></uv-empty>
  15. <template v-else>
  16. <CommonListCard
  17. v-for="(item, index) in projectStore.paperData"
  18. :key="index"
  19. :title="(index + 1) + '. ' + (item.paperName || '未知论文')"
  20. @click="handleItemClick(item, 0)"
  21. >
  22. <CommonInfoRow label="论文编号" :value="item.paperCode" />
  23. <CommonInfoRow label="发表时间" :value="item.publicationDate ? formatDate(item.publicationDate) : '--'" noBorder />
  24. </CommonListCard>
  25. </template>
  26. </view>
  27. <!-- 学术著作 -->
  28. <view class="section" v-show="currentTab === 1">
  29. <uv-empty v-if="!projectStore.workData || projectStore.workData.length === 0" mode="data" text="暂无学术著作"></uv-empty>
  30. <template v-else>
  31. <CommonListCard
  32. v-for="(item, index) in projectStore.workData"
  33. :key="index"
  34. :title="(index + 1) + '. ' + (item.workName || '未知著作')"
  35. @click="handleItemClick(item, 1)"
  36. >
  37. <CommonInfoRow label="著作编号" :value="item.workCode" />
  38. <CommonInfoRow label="出版时间" :value="item.workPublicationDate ? formatDate(item.workPublicationDate) : '--'" noBorder />
  39. </CommonListCard>
  40. </template>
  41. </view>
  42. <!-- 学术专利 -->
  43. <view class="section" v-show="currentTab === 2">
  44. <uv-empty v-if="!projectStore.patentData || projectStore.patentData.length === 0" mode="data" text="暂无学术专利"></uv-empty>
  45. <template v-else>
  46. <CommonListCard
  47. v-for="(item, index) in projectStore.patentData"
  48. :key="index"
  49. :title="(index + 1) + '. ' + (item.patentName || '未知专利')"
  50. @click="handleItemClick(item, 2)"
  51. >
  52. <CommonInfoRow label="专利编号" :value="item.patentCode" />
  53. <CommonInfoRow label="专利类型" :value="getLabel(patentClassOptions, item.patentClass)" noBorder />
  54. </CommonListCard>
  55. </template>
  56. </view>
  57. <!-- 奖项荣誉 -->
  58. <view class="section" v-show="currentTab === 3">
  59. <uv-empty v-if="!projectStore.awardData || projectStore.awardData.length === 0" mode="data" text="暂无奖项荣誉"></uv-empty>
  60. <template v-else>
  61. <CommonListCard
  62. v-for="(item, index) in projectStore.awardData"
  63. :key="index"
  64. :title="(index + 1) + '. ' + (item.awardName || '未知奖项')"
  65. @click="handleItemClick(item, 3)"
  66. >
  67. <CommonInfoRow label="获奖编号" :value="item.awardCode" />
  68. <CommonInfoRow label="获奖日期" :value="item.awardDate ? formatDate(item.awardDate) : '--'" noBorder />
  69. </CommonListCard>
  70. </template>
  71. </view>
  72. </view>
  73. <!-- 详情弹窗 -->
  74. <uv-popup ref="detailPopup" mode="bottom" round="20" bgColor="#f8f9fc">
  75. <view class="detail-popup-content">
  76. <view class="popup-header">
  77. <text class="title">业绩详情</text>
  78. <uv-icon name="close" @click="closePopup" color="#999" size="20"></uv-icon>
  79. </view>
  80. <scroll-view scroll-y class="detail-scroll">
  81. <view class="detail-list">
  82. <view class="detail-item" v-for="(field, idx) in currentFields" :key="idx">
  83. <text class="label">{{ field.label }}</text>
  84. <view class="value-wrap">
  85. <text class="value">{{ formatFieldValue(field) }}</text>
  86. </view>
  87. </view>
  88. </view>
  89. </scroll-view>
  90. </view>
  91. </uv-popup>
  92. </view>
  93. </template>
  94. <script setup lang="ts">
  95. import { onMounted, computed, watch, ref } from 'vue';
  96. import { useProjectStore } from '@/store/modules/project';
  97. import { formatDate } from '@/utils/date';
  98. import {
  99. paperTypeOptions,
  100. workClassOptions,
  101. patentClassOptions,
  102. awardLevelOptions,
  103. awardGradeOptions,
  104. awardClassOptions,
  105. awardTypeOptions,
  106. cooperationTypeOptions
  107. } from '@/constants';
  108. import CommonListCard from '@/components/ui/CommonListCard.vue';
  109. import CommonInfoRow from '@/components/ui/CommonInfoRow.vue';
  110. /**
  111. * 接收来自父组件的属性
  112. * projectId: 项目内码
  113. * projectType: 项目分类标识
  114. * projectData: 包含 projectCode 的项目基本信息
  115. */
  116. const props = defineProps<{
  117. projectId: number;
  118. projectType: string;
  119. projectData: any;
  120. }>();
  121. const projectStore = useProjectStore();
  122. // 当前激活的子 Tab 索引 (学术论文/著作/专利/奖项)
  123. const currentTab = ref(0);
  124. const detailPopup = ref();
  125. const activeItem = ref<any>(null);
  126. const activeTabType = ref(0);
  127. // 详情字段配置
  128. const fieldConfigs = {
  129. paper: [
  130. { label: '论文名称', key: 'paperName' },
  131. { label: '论文编号', key: 'paperCode' },
  132. { label: '发表/出版时间', key: 'publicationDate', isDate: true },
  133. { label: '发表/刊物论文集', key: 'publicationName' },
  134. { label: '论文类型', key: 'paperType', options: paperTypeOptions },
  135. ],
  136. work: [
  137. { label: '著作名称', key: 'workName' },
  138. { label: '著作编号', key: 'workCode' },
  139. { label: '著作类别', key: 'workClass', options: workClassOptions },
  140. { label: '出版单位', key: 'workPublisher' },
  141. { label: '出版时间', key: 'workPublicationDate', isDate: true },
  142. { label: '所属单位', key: 'deptName' },
  143. ],
  144. patent: [
  145. { label: '专利名称', key: 'patentName' },
  146. { label: '专利编号', key: 'patentCode' },
  147. { label: '所属单位', key: 'deptName' },
  148. { label: '专利类型', key: 'patentClass', options: patentClassOptions },
  149. { label: '专利简介', key: 'patentDesc' },
  150. { label: '申请人', key: 'applicantName' },
  151. ],
  152. award: [
  153. { label: '奖励名称', key: 'awardName' },
  154. { label: '获奖编号', key: 'awardCode' },
  155. { label: '成果名称', key: 'resultName' },
  156. { label: '奖励类型', key: 'awardType', options: awardTypeOptions },
  157. { label: '发证机关', key: 'awardIssueAuthority' },
  158. { label: '获奖级别', key: 'awardLevel', options: awardLevelOptions },
  159. { label: '获奖等级', key: 'awardGrade', options: awardGradeOptions },
  160. { label: '奖励类别', key: 'awardClass', options: awardClassOptions },
  161. { label: '获奖日期', key: 'awardDate', isDate: true },
  162. { label: '成果形式', key: 'resultForm' },
  163. { label: '所属单位', key: 'deptName' },
  164. { label: '合作类型', key: 'cooperationType', options: cooperationTypeOptions },
  165. { label: '项目来源', key: 'projectSource' },
  166. ]
  167. };
  168. const currentFields = computed(() => {
  169. if (activeTabType.value === 0) return fieldConfigs.paper;
  170. if (activeTabType.value === 1) return fieldConfigs.work;
  171. if (activeTabType.value === 2) return fieldConfigs.patent;
  172. return fieldConfigs.award;
  173. });
  174. const formatFieldValue = (field: any) => {
  175. if (!activeItem.value) return '--';
  176. const val = activeItem.value[field.key];
  177. if (field.isDate) return val ? formatDate(val) : '--';
  178. if (field.options) return getLabel(field.options, val);
  179. return val || '--';
  180. };
  181. /**
  182. * 详情点击显示
  183. */
  184. const handleItemClick = (item: any, type: number) => {
  185. activeItem.value = item;
  186. activeTabType.value = type;
  187. detailPopup.value.open();
  188. };
  189. const closePopup = () => {
  190. detailPopup.value.close();
  191. };
  192. const achievementTabs = ref([
  193. { name: '学术论文' },
  194. { name: '学术著作' },
  195. { name: '学术专利' },
  196. { name: '奖项荣誉' }
  197. ]);
  198. /**
  199. * 切换子 Tab 处理
  200. */
  201. const handleTabClick = (item: any) => {
  202. currentTab.value = item.index;
  203. };
  204. /**
  205. * 计算属性:检查所有成果数据是否都为空
  206. * 用于触发全局暂无数据的占位图展示
  207. */
  208. const isEmpty = computed(() => {
  209. return (
  210. (!projectStore.paperData || projectStore.paperData.length === 0) &&
  211. (!projectStore.workData || projectStore.workData.length === 0) &&
  212. (!projectStore.patentData || projectStore.patentData.length === 0) &&
  213. (!projectStore.awardData || projectStore.awardData.length === 0)
  214. );
  215. });
  216. /**
  217. * 监听项目代码变化,联动发起成果数据查询
  218. */
  219. watch(() => props.projectData?.projectCode, (code) => {
  220. if (code) {
  221. projectStore.fetchAchievements(code, props.projectType);
  222. }
  223. }, { immediate: true });
  224. /**
  225. * 通用方法:根据字典值获取对应的展示文本
  226. * @param options 常量字典数组 (包含 dictLabel 和 dictValue)
  227. * @param value 待匹配的代码值
  228. * @returns 匹配到的中文名称,未匹配到返回 '--'
  229. */
  230. const getLabel = (options: any[], value: string | number) => {
  231. if (!value) return '--';
  232. const match = options.find((item) => item.dictValue == value);
  233. return match ? match.dictLabel : '--';
  234. };
  235. </script>
  236. <style lang="scss" scoped>
  237. .module-container {
  238. min-height: 400rpx;
  239. position: relative;
  240. }
  241. .empty-wrap {
  242. min-height: 300rpx;
  243. display: flex;
  244. justify-content: center;
  245. align-items: center;
  246. }
  247. .tabs-wrapper {
  248. background-color: #fff;
  249. border-radius: 16rpx;
  250. overflow: hidden;
  251. box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
  252. margin-bottom: 24rpx;
  253. }
  254. .section {
  255. display: flex;
  256. flex-direction: column;
  257. gap: 20rpx;
  258. }
  259. .detail-popup-content {
  260. padding: 40rpx 30rpx;
  261. max-height: 85vh;
  262. display: flex;
  263. flex-direction: column;
  264. .popup-header {
  265. display: flex;
  266. justify-content: space-between;
  267. align-items: center;
  268. margin-bottom: 40rpx;
  269. padding: 0 10rpx;
  270. .title {
  271. font-size: 34rpx;
  272. font-weight: bold;
  273. color: #333;
  274. }
  275. }
  276. .detail-scroll {
  277. flex: 1;
  278. overflow: hidden;
  279. }
  280. .detail-list {
  281. display: flex;
  282. flex-direction: column;
  283. gap: 30rpx;
  284. .detail-item {
  285. display: flex;
  286. justify-content: space-between;
  287. align-items: flex-start;
  288. padding-bottom: 24rpx;
  289. border-bottom: 1rpx solid #efefef;
  290. .label {
  291. font-size: 28rpx;
  292. color: #909399;
  293. flex-shrink: 0;
  294. margin-right: 30rpx;
  295. }
  296. .value-wrap {
  297. display: flex;
  298. align-items: baseline;
  299. justify-content: flex-end;
  300. flex: 1;
  301. .value {
  302. font-size: 28rpx;
  303. color: #333;
  304. text-align: right;
  305. word-break: break-all;
  306. }
  307. }
  308. }
  309. }
  310. }
  311. </style>