| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494 |
- <script setup lang="ts">
- import { inject, onMounted, reactive, ref } from 'vue'
- import { advanceApi, reimburseApi } from '../../api'
- import { statusLabel, statusTone, formatMoney, moneyTableFormatter } from '../../utils/format'
- import { useServerPaged } from '../../hooks/useServerPaged'
- import BaseTag from '../../components/base/BaseTag.vue'
- import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
- import BaseOpButton from '../../components/base/BaseOpButton.vue'
- import { openMenuTabKey } from '../../shell/openMenuTabKey'
- const openMenuTab = inject(openMenuTabKey, () => {})
- const loadingMap = reactive({
- stats: false,
- })
- const advanceStats = ref({
- totalCount: 0,
- pendingCount: 0,
- approvedPassCount: 0,
- rejectedCount: 0,
- issuedCount: 0,
- issuedAmount: 0,
- monthApplyAmount: 0,
- monthApplyCount: 0,
- pendingIssueCount: 0,
- offsetTotal: 0,
- })
- const reimburseStats = ref({
- totalCount: 0,
- pendingCount: 0,
- approvedCount: 0,
- rejectedCount: 0,
- approvedAmount: 0,
- monthApplyAmount: 0,
- monthApplyCount: 0,
- })
- const listTab = ref<'advance' | 'reimburse'>('advance')
- const {
- rows: advanceRows,
- total: advanceTotal,
- currentPage: advanceCurrentPage,
- pageSize: advancePageSize,
- loading: advanceLoading,
- rangeText: advanceRangeText,
- refresh: refreshAdvanceList,
- onSizeChange: onAdvanceSizeChange,
- } = useServerPaged(async (params) => {
- const res: any = await advanceApi.my(params)
- const d = res.data || {}
- return { rows: d.rows ?? [], total: Number(d.total ?? 0) }
- })
- const {
- rows: reimburseRows,
- total: reimburseTotal,
- currentPage: reimburseCurrentPage,
- pageSize: reimbursePageSize,
- loading: reimburseLoading,
- rangeText: reimburseRangeText,
- refresh: refreshReimburseList,
- onSizeChange: onReimburseSizeChange,
- } = useServerPaged(async (params) => {
- const res: any = await reimburseApi.my(params)
- const d = res.data || {}
- return { rows: d.rows ?? [], total: Number(d.total ?? 0) }
- })
- async function loadStats() {
- loadingMap.stats = true
- try {
- const [aRes, rRes]: any[] = await Promise.all([advanceApi.myStats(), reimburseApi.myStats()])
- const a = aRes.data || {}
- const r = rRes.data || {}
- advanceStats.value = {
- totalCount: Number(a.totalCount ?? 0),
- pendingCount: Number(a.pendingCount ?? 0),
- approvedPassCount: Number(a.approvedPassCount ?? 0),
- rejectedCount: Number(a.rejectedCount ?? 0),
- issuedCount: Number(a.issuedCount ?? 0),
- issuedAmount: Number(a.issuedAmount ?? 0),
- monthApplyAmount: Number(a.monthApplyAmount ?? 0),
- monthApplyCount: Number(a.monthApplyCount ?? 0),
- pendingIssueCount: Number(a.pendingIssueCount ?? 0),
- offsetTotal: Number(a.offsetTotal ?? 0),
- }
- reimburseStats.value = {
- totalCount: Number(r.totalCount ?? 0),
- pendingCount: Number(r.pendingCount ?? 0),
- approvedCount: Number(r.approvedCount ?? 0),
- rejectedCount: Number(r.rejectedCount ?? 0),
- approvedAmount: Number(r.approvedAmount ?? 0),
- monthApplyAmount: Number(r.monthApplyAmount ?? 0),
- monthApplyCount: Number(r.monthApplyCount ?? 0),
- }
- } finally {
- loadingMap.stats = false
- }
- }
- type TagTone = 'pending' | 'processing' | 'done' | 'warning' | 'error' | 'info'
- function elTagTone(s: unknown): TagTone {
- return statusTone(s) as TagTone
- }
- function goReimburseApply() {
- openMenuTab('reimburse-apply')
- }
- function goAdvanceApply() {
- openMenuTab('advance-apply')
- }
- onMounted(() => {
- loadStats()
- refreshAdvanceList()
- refreshReimburseList()
- })
- </script>
- <template>
- <div class="home-page">
- <div class="page-head">
- <h1>首页</h1>
- <p class="desc">个人备用金与报销概况、快捷发起与近期申请单。</p>
- </div>
- <div class="home-quick-row">
- <button type="button" class="home-quick-btn home-quick-btn--reim" @click="goReimburseApply">
- <span class="home-quick-bg" style="background-image: url(/2.png)" />
- <span class="home-quick-inner">
- <span class="home-quick-label">新增报销申请</span>
- </span>
- </button>
- <button type="button" class="home-quick-btn home-quick-btn--adv" @click="goAdvanceApply">
- <span class="home-quick-bg" style="background-image: url(/1.png)" />
- <span class="home-quick-inner">
- <span class="home-quick-label">新增备用金申请</span>
- </span>
- </button>
- </div>
- <div v-loading="loadingMap.stats" class="home-stats-row">
- <section class="home-stat-panel home-stat-panel--advance">
- <h2 class="home-stat-title">备用金申请</h2>
- <div class="home-stat-grid-wrap">
- <div class="home-stat-grid">
- <div class="home-stat-cell">
- <span class="k">申请总数</span>
- <span class="v">{{ advanceStats.totalCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">审批中</span>
- <span class="v">{{ advanceStats.pendingCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已通过</span>
- <span class="v">{{ advanceStats.approvedPassCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已驳回</span>
- <span class="v">{{ advanceStats.rejectedCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已发放笔数</span>
- <span class="v">{{ advanceStats.issuedCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">待发放</span>
- <span class="v">{{ advanceStats.pendingIssueCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已发放金额(元)</span>
- <span class="v">{{ formatMoney(advanceStats.issuedAmount) }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">本月申请金额 / 笔数</span>
- <span class="v">{{ formatMoney(advanceStats.monthApplyAmount) }} / {{ advanceStats.monthApplyCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">冲抵合计(元)</span>
- <span class="v">{{ formatMoney(advanceStats.offsetTotal) }}</span>
- </div>
- </div>
- </div>
- </section>
- <section class="home-stat-panel home-stat-panel--reimburse">
- <h2 class="home-stat-title">报销申请</h2>
- <div class="home-stat-grid-wrap">
- <div class="home-stat-grid">
- <div class="home-stat-cell">
- <span class="k">申请总数</span>
- <span class="v">{{ reimburseStats.totalCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">审批中</span>
- <span class="v">{{ reimburseStats.pendingCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已通过</span>
- <span class="v">{{ reimburseStats.approvedCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已驳回</span>
- <span class="v">{{ reimburseStats.rejectedCount }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">已通过金额(元)</span>
- <span class="v">{{ formatMoney(reimburseStats.approvedAmount) }}</span>
- </div>
- <div class="home-stat-cell">
- <span class="k">本月申请金额 / 笔数</span>
- <span class="v">{{ formatMoney(reimburseStats.monthApplyAmount) }} / {{ reimburseStats.monthApplyCount }}</span>
- </div>
- </div>
- </div>
- </section>
- </div>
- <div class="panel home-list-panel">
- <el-tabs v-model="listTab" class="home-tabs">
- <el-tab-pane label="我的备用金申请" name="advance">
- <div class="table-scroll" v-loading="advanceLoading">
- <el-table :data="advanceRows" stripe empty-text="暂无数据">
- <el-table-column type="index" label="序号" width="70" />
- <el-table-column prop="applyTime" label="申请时间" min-width="160" />
- <el-table-column prop="applyCode" label="申请单号" min-width="140" />
- <el-table-column prop="reason" label="事由" min-width="140" show-overflow-tooltip />
- <el-table-column prop="amount" label="金额(元)" width="120" :formatter="moneyTableFormatter" class-name="col-money" />
- <el-table-column prop="status" label="状态" width="110">
- <template #default="{ row }">
- <BaseTag :tone="elTagTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
- </template>
- </el-table-column>
- <el-table-column prop="currentNode" label="当前节点" min-width="120">
- <template #default="{ row }">
- <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
- <template #default>
- <BaseOpButton primary @click="openMenuTab('advance-apply')">去办理</BaseOpButton>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <div class="list-pager">
- <span class="list-range">{{ advanceRangeText }}</span>
- <div class="list-pager-right">
- <el-pagination
- v-model:current-page="advanceCurrentPage"
- v-model:page-size="advancePageSize"
- :total="advanceTotal"
- layout="sizes, prev, pager, next"
- :page-sizes="[10, 20, 50]"
- prev-text="上一页"
- next-text="下一页"
- @size-change="onAdvanceSizeChange"
- />
- <span class="pager-size-suffix">条</span>
- </div>
- </div>
- </el-tab-pane>
- <el-tab-pane label="我的报销申请" name="reimburse">
- <div class="table-scroll" v-loading="reimburseLoading">
- <el-table :data="reimburseRows" stripe empty-text="暂无数据">
- <el-table-column type="index" label="序号" width="70" />
- <el-table-column prop="applyTime" label="申请时间" min-width="160" />
- <el-table-column prop="applyCode" label="申请单号" min-width="140" />
- <el-table-column prop="title" label="标题" min-width="160" show-overflow-tooltip />
- <el-table-column prop="expenseName" label="费用项目" min-width="120" show-overflow-tooltip />
- <el-table-column prop="amount" label="金额(元)" width="120" :formatter="moneyTableFormatter" class-name="col-money" />
- <el-table-column prop="status" label="状态" width="110">
- <template #default="{ row }">
- <BaseTag :tone="elTagTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
- </template>
- </el-table-column>
- <el-table-column prop="currentNode" label="当前节点" min-width="120">
- <template #default="{ row }">
- <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
- </template>
- </el-table-column>
- <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
- <template #default>
- <BaseOpButton primary @click="openMenuTab('reimburse-apply')">去办理</BaseOpButton>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <div class="list-pager">
- <span class="list-range">{{ reimburseRangeText }}</span>
- <div class="list-pager-right">
- <el-pagination
- v-model:current-page="reimburseCurrentPage"
- v-model:page-size="reimbursePageSize"
- :total="reimburseTotal"
- layout="sizes, prev, pager, next"
- :page-sizes="[10, 20, 50]"
- prev-text="上一页"
- next-text="下一页"
- @size-change="onReimburseSizeChange"
- />
- <span class="pager-size-suffix">条</span>
- </div>
- </div>
- </el-tab-pane>
- </el-tabs>
- </div>
- </div>
- </template>
- <style scoped>
- .home-page {
- padding-bottom: 24px;
- }
- .home-stats-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 16px;
- margin-bottom: 20px;
- }
- @media (max-width: 1100px) {
- .home-stats-row {
- grid-template-columns: 1fr;
- }
- }
- .home-stat-panel {
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- box-shadow: var(--shadow);
- padding: 16px 18px;
- }
- .home-stat-title {
- margin: 0 0 12px;
- font-size: 15px;
- font-weight: 600;
- color: var(--text);
- }
- .home-stat-grid-wrap {
- min-width: 0;
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
- }
- /* 备用金:9 项固定两行(5 + 4) */
- .home-stat-panel--advance .home-stat-grid {
- display: grid;
- grid-template-columns: repeat(5, minmax(72px, 1fr));
- grid-template-rows: repeat(2, auto);
- gap: 8px 10px;
- }
- /* 报销:6 项固定两行(3 + 3) */
- .home-stat-panel--reimburse .home-stat-grid {
- display: grid;
- grid-template-columns: repeat(3, minmax(80px, 1fr));
- grid-template-rows: repeat(2, auto);
- gap: 8px 10px;
- }
- .home-stat-cell {
- display: flex;
- flex-direction: column;
- gap: 2px;
- min-width: 0;
- padding: 8px 10px;
- background: var(--table-head);
- border-radius: var(--radius-md);
- border: 1px solid var(--el-border-color-light);
- }
- .home-stat-panel--advance .home-stat-title {
- color: var(--primary-dark);
- }
- .home-stat-panel--reimburse .home-stat-title {
- color: #0369a1;
- }
- .home-stat-cell .k {
- font-size: 12px;
- line-height: 1.3;
- color: var(--text-muted);
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- overflow: hidden;
- word-break: break-word;
- }
- .home-stat-cell .v {
- font-size: 17px;
- font-weight: 600;
- color: var(--text);
- line-height: 1.25;
- word-break: break-all;
- }
- .home-quick-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 16px;
- margin-bottom: 20px;
- }
- @media (max-width: 560px) {
- .home-quick-row {
- grid-template-columns: 1fr;
- }
- }
- .home-quick-btn {
- position: relative;
- display: block;
- width: 100%;
- min-height: 140px;
- padding: 0;
- border: none;
- border-radius: var(--radius);
- overflow: hidden;
- cursor: pointer;
- text-align: left;
- box-shadow: var(--shadow);
- }
- .home-quick-btn:disabled {
- opacity: 0.75;
- cursor: wait;
- }
- .home-quick-bg {
- position: absolute;
- inset: 0;
- background-size: cover;
- background-position: center right;
- transition: transform 0.25s ease;
- }
- .home-quick-btn:hover .home-quick-bg {
- transform: scale(1.02);
- }
- .home-quick-inner {
- position: relative;
- z-index: 1;
- display: flex;
- align-items: flex-start;
- justify-content: flex-start;
- min-height: 140px;
- padding: 20px 22px;
- background: transparent;
- }
- .home-quick-label {
- font-size: 22px;
- font-weight: 700;
- letter-spacing: 0.02em;
- color: #fff;
- }
- @media (max-width: 560px) {
- .home-quick-label {
- font-size: 18px;
- }
- }
- .home-list-panel {
- padding: 8px 16px 16px;
- background: var(--surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- box-shadow: var(--shadow);
- }
- .home-tabs :deep(.el-tabs__header) {
- margin-bottom: 12px;
- }
- .home-tabs :deep(.el-tabs__item) {
- font-size: 14px;
- }
- </style>
|