فهرست منبع

feat(用户中心): 添加课题组经费统计功能

- 在用户中心页面新增经费统计卡片,展示可用余额
- 添加经费统计详情弹窗,显示授信额度、锁定金额等详细信息
- 实现经费卡列表弹窗,展示各经费卡的具体信息
- 在API层添加获取经费账户列表的接口方法
- 调整导航项布局,从4列改为3列以适应新功能
张旭伟 4 روز پیش
والد
کامیت
be7efa8696
2فایلهای تغییر یافته به همراه347 افزوده شده و 4 حذف شده
  1. 4 1
      src/api/finace/index.ts
  2. 343 3
      src/view/user/index.vue

+ 4 - 1
src/api/finace/index.ts

@@ -6,6 +6,9 @@ export function useFinaceApi() {
     // 获取资金流水
     getList: (query?: object) => {
       return request.postRequest(finacePath, 'FinanceOrder', 'GetFinBillableOrderList', query);
-    }
+    },
+    getFinanceAccountList(query?: object) {
+			return request.postRequest(finacePath, 'Finance', 'GetFinanceAccountList', query);
+		},
   }
 }

+ 343 - 3
src/view/user/index.vue

@@ -48,6 +48,14 @@
           </li>
         </ul>
       </div>
+      <div class="card" @click="showFinancePopup = true">
+        <h4>课题组经费统计</h4>
+        <div class="balance-display">
+          <span class="amount">{{ financeStats.calculatedBalance.toFixed(2) }}</span>
+          <p class="label">可用余额(¥)</p>
+          <van-icon name="arrow" class="arrow-icon" />
+        </div>
+      </div>
       <div class="card" @click="showOperatingGuidelines">
         <h4>平台入室操作指引</h4>
       </div>
@@ -70,6 +78,89 @@
       </van-button>
     </footer>
 
+    <van-popup position="bottom" v-model:show="showFinancePopup" round>
+      <div class="finance-detail">
+        <div class="detail-header">
+          <h3>经费统计详情</h3>
+          <van-icon name="cross" @click="showFinancePopup = false" />
+        </div>
+        <div class="detail-content">
+          <div class="detail-item" @click="showListPopup = true">
+            <span class="label">经费卡数量</span>
+            <div class="value-with-arrow">
+              <span class="value">{{ financeStats.accountCount }}</span>
+              <van-icon name="arrow" />
+            </div>
+          </div>
+          <div class="detail-item">
+            <span class="label">授信额度</span>
+            <span class="value">¥{{ financeStats.totalCredit.toFixed(2) }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">可用余额</span>
+            <span class="value">¥{{ financeStats.totalBalance.toFixed(2) }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">锁定金额</span>
+            <span class="value">¥{{ financeStats.totalLocked.toFixed(2) }}</span>
+          </div>
+          <div class="detail-item highlight">
+            <span class="label">最终可用额度</span>
+            <span class="value">¥{{ financeStats.calculatedBalance.toFixed(2) }}</span>
+          </div>
+          <div class="divider"></div>
+          <div class="detail-item">
+            <span class="label">已出账单</span>
+            <span class="value">¥{{ financeStats.totalIssued.toFixed(2) }}</span>
+          </div>
+          <div class="detail-item">
+            <span class="label">未出账单</span>
+            <span class="value">¥{{ financeStats.totalUnpaid.toFixed(2) }}</span>
+          </div>
+        </div>
+        <div class="detail-footer">
+          <p class="formula-tip">* 计算公式:授信额度 + 可用余额 - 锁定金额</p>
+        </div>
+      </div>
+    </van-popup>
+
+    <van-popup position="bottom" v-model:show="showListPopup" round closeable>
+      <div class="finance-list">
+        <div class="list-header">
+          <h3>经费卡列表</h3>
+        </div>
+        <div class="list-content">
+          <div v-for="item in financeList" :key="item.id" class="finance-card-item">
+            <div class="card-title">
+              <span class="account-no">{{ item.finAccount }}</span>
+              <span class="org-name">{{ item.finOrgName }}-{{ item.projName }}</span>
+            </div>
+            <div class="card-details">
+              <div class="grid">
+                <div class="grid-item">
+                  <p class="label">可用余额</p>
+                  <p class="value">¥{{ (item.finAvailBalance || 0).toFixed(2) }}</p>
+                </div>
+                <div class="grid-item">
+                  <p class="label">授信额度</p>
+                  <p class="value">¥{{ (item.finCreditLimit || 0).toFixed(2) }}</p>
+                </div>
+                <div class="grid-item">
+                  <p class="label">锁定金额</p>
+                  <p class="value">¥{{ (item.finLockAmount || 0).toFixed(2) }}</p>
+                </div>
+                <div class="grid-item">
+                  <p class="label">已出账单</p>
+                  <p class="value">¥{{ (item.finIssuedBill || 0).toFixed(2) }}</p>
+                </div>
+              </div>
+            </div>
+          </div>
+          <van-empty v-if="financeList.length === 0" description="暂无经费卡信息" />
+        </div>
+      </div>
+    </van-popup>
+
     <van-popup position="bottom" v-model:show="showPopup" :style="{ padding: '20px' }">
       <div class="operation-guide">
         <div class="guide-header">
@@ -121,6 +212,7 @@ import { useUserInfo } from '/@/stores/userInfo'
 import { Local, Session } from '/@/utils/storage'
 import { useBillApi } from '/@/api/instr/finance/bill'
 import { useLoginApi } from '/@/api/login'
+import { useFinaceApi } from '/@/api/finace'
 
 import { scanCodeWxUrl, InstSwitchType } from '/@/constants/pageConstants'
 
@@ -128,6 +220,7 @@ import wx from 'weixin-js-sdk'
 
 const billApi = useBillApi()
 const loginApi = useLoginApi()
+const financeApi = useFinaceApi()
 
 const router = useRouter()
 const storesUseUserInfo = useUserInfo()
@@ -144,6 +237,21 @@ const billInfo = ref<{
   totalFee: 0,
 })
 
+const financeStats = ref({
+  accountCount: 0,
+  totalBalance: 0,
+  totalCredit: 0,
+  totalLocked: 0,
+  totalIssued: 0,
+  totalUnpaid: 0,
+  calculatedBalance: 0, // 授信 + 可用 - 锁定
+})
+
+const financeList = ref<any[]>([])
+
+const showFinancePopup = ref<boolean>(false)
+const showListPopup = ref<boolean>(false)
+
 const handleStepList = ref([
   {
     title: '第一步:入室培训和考试',
@@ -239,8 +347,37 @@ const getBillInfo = async () => {
   billInfo.value = res?.data
 }
 
+// 获取经费卡统计信息
+const getFinanceStats = async () => {
+  if (!userInfos.value?.projectGroup?.id) return
+  const [err, res]: ToResponse = await to(
+    financeApi.getFinanceAccountList({
+      projId: userInfos.value.projectGroup.id,
+    })
+  )
+  if (err) return
+  const list = res?.data?.list || []
+  financeList.value = list
+  const totalBalance = list.reduce((sum: number, item: any) => sum + (item.finAvailBalance || 0), 0)
+  const totalCredit = list.reduce((sum: number, item: any) => sum + (item.finCreditLimit || 0), 0)
+  const totalLocked = list.reduce((sum: number, item: any) => sum + (item.finLockAmount || 0), 0)
+  const totalIssued = list.reduce((sum: number, item: any) => sum + (item.finIssuedBill || 0), 0)
+  const totalUnpaid = list.reduce((sum: number, item: any) => sum + (item.finUnpaidBill || 0), 0)
+
+  financeStats.value = {
+    accountCount: list.length,
+    totalBalance,
+    totalCredit,
+    totalLocked,
+    totalIssued,
+    totalUnpaid,
+    calculatedBalance: totalCredit + totalBalance - totalLocked,
+  }
+}
+
 onMounted(() => {
   getBillInfo()
+  getFinanceStats()
 })
 </script>
 
@@ -342,6 +479,35 @@ onMounted(() => {
         }
       }
 
+      .balance-display {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        padding: 15px 0;
+        position: relative;
+
+        .amount {
+          font-size: 24px;
+          font-weight: bold;
+          color: #1c9bfd;
+        }
+
+        .label {
+          font-size: 14px;
+          color: #666;
+          margin-top: 5px;
+        }
+
+        .arrow-icon {
+          position: absolute;
+          right: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          color: #ccc;
+          font-size: 18px;
+        }
+      }
+
       .nav {
         display: flex;
         margin: 10px 0;
@@ -349,21 +515,25 @@ onMounted(() => {
         justify-content: space-between;
 
         li {
-          flex: 0 0 25%;
+          flex: 0 0 33.33%;
           display: flex;
           flex-direction: column;
           align-items: center;
           justify-content: center;
 
           span {
-            font-size: 20px;
+            font-size: 16px;
+            font-weight: bold;
+            color: #333;
           }
 
           p {
             margin-top: 4px;
+            font-size: 12px;
+            color: #666;
           }
 
-          &:nth-child(n + 5) {
+          &:nth-child(n + 4) {
             margin-top: 10px;
           }
         }
@@ -381,6 +551,176 @@ onMounted(() => {
   padding: 20px !important;
 }
 
+.finance-detail {
+  .detail-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #eee;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #333;
+    }
+
+    .van-icon {
+      font-size: 18px;
+      color: #999;
+      cursor: pointer;
+    }
+  }
+
+  .detail-content {
+    .detail-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 15px;
+      font-size: 14px;
+
+      .label {
+        color: #666;
+      }
+
+      .value {
+        color: #333;
+        font-weight: 500;
+      }
+
+      .value-with-arrow {
+        display: flex;
+        align-items: center;
+        color: #1c9bfd;
+        font-weight: 500;
+
+        .van-icon {
+          margin-left: 4px;
+          font-size: 14px;
+        }
+      }
+
+      &.highlight {
+        margin-top: 5px;
+        padding-top: 15px;
+        border-top: 1px solid #f5f5f5;
+
+        .label {
+          color: #333;
+          font-weight: 600;
+        }
+
+        .value {
+          color: #1c9bfd;
+          font-size: 18px;
+          font-weight: bold;
+        }
+      }
+    }
+
+    .divider {
+      height: 1px;
+      background-color: #eee;
+      margin: 15px 0;
+    }
+  }
+
+  .detail-footer {
+    margin-top: 20px;
+    padding: 15px;
+    background: #f8f9fa;
+    border-radius: 8px;
+
+    .formula-tip {
+      margin: 0;
+      font-size: 12px;
+      color: #999;
+      line-height: 1.4;
+    }
+  }
+}
+
+.finance-list {
+  max-height: 80vh;
+  display: flex;
+  flex-direction: column;
+
+  .list-header {
+    padding: 20px;
+    border-bottom: 1px solid #f5f5f5;
+
+    h3 {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #333;
+      text-align: center;
+    }
+  }
+
+  .list-content {
+    flex: 1;
+    overflow-y: auto;
+    padding: 10px 15px 30px;
+
+    .finance-card-item {
+      background: #f8f9fa;
+      border-radius: 12px;
+      padding: 15px;
+      margin-bottom: 12px;
+      border: 1px solid #eee;
+
+      .card-title {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        margin-bottom: 12px;
+        padding-bottom: 10px;
+        border-bottom: 1px dashed #ddd;
+
+        .account-no {
+          font-size: 15px;
+          font-weight: bold;
+          color: #333;
+        }
+
+        .org-name {
+          font-size: 12px;
+          color: #1c9bfd;
+          background: rgba(28, 155, 253, 0.1);
+          padding: 2px 8px;
+          border-radius: 4px;
+        }
+      }
+
+      .card-details {
+        .grid {
+          display: grid;
+          grid-template-columns: repeat(2, 1fr);
+          gap: 12px;
+
+          .grid-item {
+            .label {
+              font-size: 12px;
+              color: #999;
+              margin-bottom: 4px;
+            }
+
+            .value {
+              font-size: 14px;
+              font-weight: 500;
+              color: #333;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
 .operation-guide {
   max-height: 70vh;
   overflow-y: auto;