ProjectFunding.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. <template>
  2. <view class="module-container">
  3. <uv-empty v-if="projectStore.fetchFundingLoading" mode="list" text="加载中..."></uv-empty>
  4. <view v-else class="content-wrapper">
  5. <!-- 经费汇总概览 -->
  6. <view class="summary-card">
  7. <view class="sum-row">
  8. <view class="sum-item">
  9. <text class="sum-label">到款合计({{ projectStore.fundsData?.fundAllotCount || 0 }}笔)</text>
  10. <text class="sum-value text-blue">{{ amountUnitFormatter(projectStore.fundsData?.fundAllotSum) }}<text class="unit">元</text></text>
  11. </view>
  12. <view class="sum-item">
  13. <text class="sum-label">结余</text>
  14. <text class="sum-value text-green">{{ amountUnitFormatter(projectStore.fundsData?.surplus) }}<text class="unit">元</text></text>
  15. </view>
  16. </view>
  17. <view class="sum-row">
  18. <view class="sum-item">
  19. <text class="sum-label">支出合计({{ projectStore.fundsData?.expenseCount || 0 }}笔)</text>
  20. <text class="sum-value text-red">{{ amountUnitFormatter(projectStore.fundsData?.expenseSum) }}<text class="unit">元</text></text>
  21. </view>
  22. <view class="sum-item">
  23. <text class="sum-label">外拨合计({{ projectStore.fundsData?.externalAllotCount || 0 }}笔)</text>
  24. <text class="sum-value text-orange">{{ amountUnitFormatter(projectStore.fundsData?.externalAllotSum) }}<text class="unit">元</text></text>
  25. </view>
  26. </view>
  27. </view>
  28. <!-- 标签页切换 -->
  29. <view class="tabs-wrapper">
  30. <uv-tabs :list="fundingTabs" :current="currentTab" @click="handleTabClick" lineColor="#1c9bfd" activeColor="#1c9bfd" inactiveColor="#666"></uv-tabs>
  31. </view>
  32. <!-- 入账列表 -->
  33. <view class="section" v-show="currentTab === 0">
  34. <uv-empty v-if="!projectStore.claimdData || projectStore.claimdData.length === 0" mode="data" text="暂无入账记录"></uv-empty>
  35. <template v-else>
  36. <CommonListCard
  37. v-for="(item, index) in projectStore.claimdData"
  38. :key="'in'+index"
  39. :title="item.allotNo || '入账记录'"
  40. statusType="primary"
  41. @click="handleItemClick(item, 0)"
  42. >
  43. <CommonInfoRow label="认领金额" :value="amountUnitFormatter(item.amount)" isAmount />
  44. <CommonInfoRow label="认领时间" :value="item.applyTime ? formatDate(item.applyTime) : '--'" />
  45. <CommonInfoRow label="负责人" :value="item.projectIncharge" noBorder />
  46. </CommonListCard>
  47. </template>
  48. </view>
  49. <!-- 外拨列表 -->
  50. <view class="section" v-show="currentTab === 1">
  51. <uv-empty v-if="!projectStore.outBoundData || projectStore.outBoundData.length === 0" mode="data" text="暂无外拨记录"></uv-empty>
  52. <template v-else>
  53. <CommonListCard
  54. v-for="(item, index) in projectStore.outBoundData"
  55. :key="'out'+index"
  56. :title="item.externalAllotNo || '外拨记录'"
  57. statusType="warning"
  58. @click="handleItemClick(item, 1)"
  59. >
  60. <CommonInfoRow label="外拨总额" :value="amountUnitFormatter(item.amount)" isAmount />
  61. <CommonInfoRow label="申请日期" :value="item.applyTime ? formatDate(item.applyTime) : '--'" />
  62. <CommonInfoRow label="负责人" :value="item.projectIncharge" noBorder />
  63. </CommonListCard>
  64. </template>
  65. </view>
  66. <!-- 支出列表 -->
  67. <view class="section" v-show="currentTab === 2">
  68. <uv-empty v-if="!projectStore.rebateData || projectStore.rebateData.length === 0" mode="data" text="暂无支出记录"></uv-empty>
  69. <template v-else>
  70. <CommonListCard
  71. v-for="(item, index) in projectStore.rebateData"
  72. :key="'exp'+index"
  73. :title="item.expenseNo || '支出记录'"
  74. :statusLabel="getExpenseStatusLabel(item.status)"
  75. :statusType="getExpenseStatusType(item.status)"
  76. @click="handleItemClick(item, 2)"
  77. >
  78. <CommonInfoRow label="支出金额" :value="amountUnitFormatter(item.amount)" isAmount />
  79. <CommonInfoRow label="支出科目" :value="item.subject" />
  80. <CommonInfoRow label="支出日期" :value="item.expendTime ? formatDate(item.expendTime) : '--'" noBorder />
  81. </CommonListCard>
  82. </template>
  83. </view>
  84. </view>
  85. <!-- 详情弹窗 -->
  86. <uv-popup ref="detailPopup" mode="bottom" round="20" bgColor="#f8f9fc">
  87. <view class="detail-popup-content">
  88. <view class="popup-header">
  89. <text class="title">详情记录</text>
  90. <uv-icon name="close" @click="closePopup" color="#999" size="20"></uv-icon>
  91. </view>
  92. <scroll-view scroll-y class="detail-scroll">
  93. <view class="detail-list">
  94. <view class="detail-item" v-for="(field, idx) in currentFields" :key="idx">
  95. <text class="label">{{ field.label }}</text>
  96. <view class="value-wrap">
  97. <text class="value" :class="{ 'amount': field.isAmount }">
  98. {{ formatFieldValue(field) }}
  99. </text>
  100. <text class="unit" v-if="field.isAmount">元</text>
  101. </view>
  102. </view>
  103. <view v-if="activeItem?.fileName" class="attachment-section">
  104. <text class="label">附件信息</text>
  105. <CommonFileList :files="[{ fileName: activeItem.fileName, fileUrl: activeItem.fileUrl }]" />
  106. </view>
  107. </view>
  108. </scroll-view>
  109. </view>
  110. </uv-popup>
  111. </view>
  112. </template>
  113. <script setup lang="ts">
  114. import { ref, watch, computed } from 'vue';
  115. import { useProjectStore } from '@/store/modules/project';
  116. import { formatDate } from '@/utils/date';
  117. import { formatWithComma } from '@/utils/format';
  118. import { previewFile } from '@/utils/file';
  119. import CommonListCard from '@/components/ui/CommonListCard.vue';
  120. import CommonInfoRow from '@/components/ui/CommonInfoRow.vue';
  121. import CommonFileList from '@/components/ui/CommonFileList.vue';
  122. /**
  123. * 接收来自父组件的属性
  124. * projectId: 项目内码
  125. * projectType: 项目分类标识
  126. */
  127. const props = defineProps<{
  128. projectId: number;
  129. projectType: string;
  130. }>();
  131. const projectStore = useProjectStore();
  132. // 当前激活的子 Tab 索引
  133. const currentTab = ref(0);
  134. const detailPopup = ref();
  135. const activeItem = ref<any>(null);
  136. const activeTabType = ref(0);
  137. // 详情字段配置
  138. const fieldConfigs = {
  139. inbound: [
  140. { label: '认领编号', key: 'allotNo' },
  141. { label: '项目负责人', key: 'projectIncharge' },
  142. { label: '项目类型', key: 'projectType', isType: true },
  143. { label: '认领金额', key: 'amount', isAmount: true },
  144. { label: '外拨金额', key: 'externalAmount', isAmount: true },
  145. { label: '留校金额', key: 'internalAmount', isAmount: true },
  146. { label: '认领时间', key: 'applyTime', isDate: true },
  147. { label: '摘要', key: 'remark' },
  148. { label: '创建人', key: 'createdName' },
  149. { label: '创建时间', key: 'createdTime', isDate: true },
  150. ],
  151. outbound: [
  152. { label: '外拨编号', key: 'externalAllotNo' },
  153. { label: '项目编号', key: 'projectNo' },
  154. { label: '项目负责人', key: 'projectIncharge' },
  155. { label: '外拨总额', key: 'amount', isAmount: true },
  156. { label: '已拨金额', key: 'allottedAmount', isAmount: true },
  157. { label: '未拨金额', key: 'notAllottedAmount', isAmount: true },
  158. { label: '申请日期', key: 'applyTime', isDate: true },
  159. { label: '摘要', key: 'remark' },
  160. { label: '创建人', key: 'createdName' },
  161. { label: '创建时间', key: 'createdTime', isDate: true },
  162. ],
  163. expense: [
  164. { label: '支出编号', key: 'expenseNo' },
  165. { label: '项目类型', key: 'projectType', isType: true },
  166. { label: '项目名称', key: 'projectName' },
  167. { label: '支出金额', key: 'amount', isAmount: true },
  168. { label: '支出科目', key: 'subject' },
  169. { label: '支出状态', key: 'status', isStatus: true },
  170. { label: '合同金额', key: 'contractAmount', isAmount: true },
  171. { label: '间接/配套经费支出', key: 'otherAmount', isAmount: true },
  172. { label: '财政拨款支出', key: 'projectAmount', isAmount: true },
  173. { label: '自筹经费支出', key: 'raiseAmount', isAmount: true },
  174. { label: '收款人', key: 'receiver' },
  175. { label: '支出日期', key: 'expendTime', isDate: true },
  176. { label: '摘要', key: 'remark' },
  177. { label: '创建人', key: 'createdName' },
  178. { label: '创建时间', key: 'createdTime', isDate: true },
  179. ]
  180. };
  181. const currentFields = computed(() => {
  182. if (activeTabType.value === 0) return fieldConfigs.inbound;
  183. if (activeTabType.value === 1) return fieldConfigs.outbound;
  184. return fieldConfigs.expense;
  185. });
  186. const formatFieldValue = (field: any) => {
  187. if (!activeItem.value) return '--';
  188. const val = activeItem.value[field.key];
  189. if (field.isAmount) return amountUnitFormatter(val);
  190. if (field.isDate) return val ? formatDate(val) : '--';
  191. if (field.isType) return getProjectType(val);
  192. if (field.isStatus) return getExpenseStatusLabel(val);
  193. return val || '--';
  194. };
  195. /**
  196. * 详情点击显示
  197. */
  198. const handleItemClick = (item: any, type: number) => {
  199. activeItem.value = item;
  200. activeTabType.value = type;
  201. detailPopup.value.open();
  202. };
  203. const closePopup = () => {
  204. detailPopup.value.close();
  205. };
  206. // 经费模块内部的子 Tab 配置
  207. const fundingTabs = ref([
  208. { name: '入账记录' },
  209. { name: '外拨记录' },
  210. { name: '支出记录' }
  211. ]);
  212. /**
  213. * 切换子 Tab 处理
  214. */
  215. const handleTabClick = (item: any) => {
  216. currentTab.value = item.index;
  217. };
  218. /**
  219. * 监听项目 ID 变化,触发经费数据加载
  220. */
  221. watch(() => props.projectId, (id) => {
  222. if (id) {
  223. projectStore.fetchFunding(id, props.projectType);
  224. }
  225. }, { immediate: true });
  226. /**
  227. * 金额格式化处理
  228. * 使用全局统一的千分位格式化工具
  229. */
  230. const amountUnitFormatter = (num: any) => {
  231. return formatWithComma(num);
  232. };
  233. /**
  234. * 获取项目类型名称 (内部逻辑)
  235. */
  236. const getProjectType = (code: string | number) => {
  237. if (code == 10) return '纵向项目';
  238. if (code == 20) return '横向项目';
  239. if (code == 30) return '内部项目';
  240. if (code == 40) return '重点学科';
  241. return '未知';
  242. };
  243. /**
  244. * 获取支出记录审批状态说明
  245. */
  246. const getExpenseStatusLabel = (status: string | number) => {
  247. if (status == 10) return '待审核';
  248. if (status == 20) return '已通过';
  249. if (status == 30) return '已拒绝';
  250. return '未知';
  251. };
  252. /**
  253. * 获取支出记录审批状态对应的 UI 类型
  254. */
  255. const getExpenseStatusType = (status: string | number) => {
  256. if (status == 10) return 'info';
  257. if (status == 20) return 'success';
  258. if (status == 30) return 'error';
  259. return 'default';
  260. };
  261. /**
  262. * 统一附件打开/预览处理
  263. */
  264. const handleDownload = (file: any) => {
  265. if (!file.fileUrl) return;
  266. previewFile(file.fileUrl, file.fileName);
  267. };
  268. </script>
  269. <style lang="scss" scoped>
  270. .module-container {
  271. min-height: 400rpx;
  272. position: relative;
  273. }
  274. .content-wrapper {
  275. display: flex;
  276. flex-direction: column;
  277. gap: 30rpx;
  278. }
  279. .tabs-wrapper {
  280. background-color: #fff;
  281. border-radius: 16rpx;
  282. overflow: hidden;
  283. box-shadow: 0 4rpx 16rpx rgba(0,0,0,0.03);
  284. }
  285. .summary-card {
  286. background: linear-gradient(135deg, #ffffff 0%, #f6faff 100%);
  287. border-radius: 20rpx;
  288. padding: 30rpx;
  289. box-shadow: 0 8rpx 24rpx rgba(28, 155, 253, 0.08);
  290. display: flex;
  291. flex-direction: column;
  292. gap: 30rpx;
  293. .sum-row {
  294. display: flex;
  295. justify-content: space-between;
  296. .sum-item {
  297. flex: 1;
  298. display: flex;
  299. flex-direction: column;
  300. align-items: center;
  301. .sum-label {
  302. font-size: 24rpx;
  303. color: #909399;
  304. margin-bottom: 12rpx;
  305. }
  306. .sum-value {
  307. font-size: 38rpx;
  308. font-weight: 700;
  309. font-family: din;
  310. letter-spacing: 0.5px;
  311. .unit {
  312. font-size: 22rpx;
  313. margin-left: 6rpx;
  314. font-weight: 400;
  315. color: #999;
  316. }
  317. }
  318. .text-blue { color: #1c9bfd; }
  319. .text-green { color: #52c41a; }
  320. .text-red { color: #ff4d4f; }
  321. .text-orange { color: #faad14; }
  322. }
  323. }
  324. }
  325. .detail-popup-content {
  326. padding: 40rpx 30rpx;
  327. max-height: 85vh;
  328. display: flex;
  329. flex-direction: column;
  330. .popup-header {
  331. display: flex;
  332. justify-content: space-between;
  333. align-items: center;
  334. margin-bottom: 40rpx;
  335. padding: 0 10rpx;
  336. .title {
  337. font-size: 34rpx;
  338. font-weight: bold;
  339. color: #333;
  340. }
  341. }
  342. .detail-scroll {
  343. flex: 1;
  344. overflow: hidden;
  345. }
  346. .detail-list {
  347. display: flex;
  348. flex-direction: column;
  349. gap: 30rpx;
  350. .detail-item {
  351. display: flex;
  352. justify-content: space-between;
  353. align-items: flex-start;
  354. padding-bottom: 24rpx;
  355. border-bottom: 1rpx solid #efefef;
  356. .label {
  357. font-size: 28rpx;
  358. color: #909399;
  359. flex-shrink: 0;
  360. margin-right: 30rpx;
  361. }
  362. .value-wrap {
  363. display: flex;
  364. align-items: baseline;
  365. justify-content: flex-end;
  366. flex: 1;
  367. .value {
  368. font-size: 28rpx;
  369. color: #333;
  370. text-align: right;
  371. word-break: break-all;
  372. &.amount {
  373. color: #1c9bfd;
  374. font-weight: bold;
  375. font-family: din;
  376. font-size: 32rpx;
  377. }
  378. }
  379. .unit {
  380. font-size: 20rpx;
  381. color: #999;
  382. margin-left: 4rpx;
  383. }
  384. }
  385. }
  386. .attachment-section {
  387. margin-top: 20rpx;
  388. .label {
  389. font-size: 28rpx;
  390. color: #909399;
  391. display: block;
  392. margin-bottom: 16rpx;
  393. }
  394. }
  395. }
  396. }
  397. .section {
  398. width: 100%;
  399. }
  400. </style>