| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- <script setup lang="ts">
- import { computed, onMounted, reactive, ref } from 'vue'
- import { ElMessage } from 'element-plus'
- import { View } from '@element-plus/icons-vue'
- import { advanceApi, reimburseApi } from '../../api'
- import { statusLabel, statusTone, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
- import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
- import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
- import BaseDialog from '../../components/base/BaseDialog.vue'
- import BaseInput from '../../components/base/BaseInput.vue'
- import BaseSelect from '../../components/base/BaseSelect.vue'
- import BaseTag from '../../components/base/BaseTag.vue'
- import BaseOpButton from '../../components/base/BaseOpButton.vue'
- import ApprovalFlowView from '../../components/base/ApprovalFlowView.vue'
- import BaseClampText from '../../components/base/BaseClampText.vue'
- import { usePagedTable } from '../../hooks/usePagedTable'
- type DialogKey = 'viewDetail'
- const showMoreFilters = ref(false)
- const loadingMap = reactive<Record<string, boolean>>({})
- const dialogs = reactive<Record<DialogKey, boolean>>({
- viewDetail: false
- })
- const mergedRows = ref<any[]>([])
- const filterForm = reactive({
- code: '',
- bizType: '' as string,
- status: '' as string,
- applyDate: null as string | null,
- amountRange: ''
- })
- function filterMineRows(rows: any[]) {
- let list = [...rows]
- const code = filterForm.code.trim()
- if (code) list = list.filter((r: any) => String(r.applyCode || '').includes(code))
- if (filterForm.bizType && filterForm.bizType !== '全部') {
- list = list.filter((r: any) => r.bizType === filterForm.bizType)
- }
- if (filterForm.status && filterForm.status !== '全部') {
- const map: Record<string, string> = {
- 待提交: 'draft',
- 审批中: 'pending',
- 审批通过: 'approved',
- 审批驳回: 'rejected',
- }
- const want = map[filterForm.status]
- if (want) list = list.filter((r: any) => r.status === want)
- }
- if (filterForm.applyDate) {
- const d = filterForm.applyDate
- list = list.filter((r: any) => String(r.applyTime || '').slice(0, 10) === d)
- }
- if (filterForm.amountRange?.trim()) {
- const parts = filterForm.amountRange.split(/[-–]/).map((x: string) => Number(String(x).trim()))
- if (parts.length >= 2 && !parts.some((n) => Number.isNaN(n))) {
- const lo = Math.min(parts[0], parts[1])
- const hi = Math.max(parts[0], parts[1])
- list = list.filter((r: any) => {
- const amt = Number(r.amount || 0)
- return amt >= lo && amt <= hi
- })
- }
- }
- return list
- }
- const filteredMergedRows = computed(() => filterMineRows(mergedRows.value))
- const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 10 })
- const currentRow = ref<any>(null)
- const viewFlowNodes = ref<any[]>([])
- const viewFlowLoading = ref(false)
- const stats = [
- { name: '全部申请', value: '-' },
- { name: '审批中', value: '-' },
- { name: '已通过', value: '-' },
- { name: '已驳回', value: '-' }
- ]
- const bizTypeOptions = ['全部', '报销单', '备用金']
- const statusOptions = ['全部', '待提交', '审批中', '审批通过', '审批驳回']
- async function loadData() {
- try {
- const [advRes, reimRes, advSt, reimSt]: any[] = await Promise.all([
- advanceApi.my({ page: 1, pageSize: 999 }),
- reimburseApi.my({ page: 1, pageSize: 999 }),
- advanceApi.myStats(),
- reimburseApi.myStats()
- ])
- const advRows = (advRes.data?.rows || []).map((r: any) => ({
- ...r, bizType: '备用金', title: r.reason,
- applyCode: r.applyCode || r.code
- }))
- const reimRows = (reimRes.data?.rows || []).map((r: any) => ({
- ...r, bizType: '报销单', title: r.title,
- applyCode: r.applyCode || r.code
- }))
- mergedRows.value = [...advRows, ...reimRows]
- const a = advSt.data || {}
- const r = reimSt.data || {}
- const totalCount = Number(r.totalCount ?? 0) + Number(a.totalCount ?? 0)
- const pending = Number(r.pendingCount ?? 0) + Number(a.pendingCount ?? 0)
- const approved =
- Number(r.approvedCount ?? 0) + Number(a.approvedPassCount ?? 0) + Number(a.issuedCount ?? 0)
- const rejected = Number(r.rejectedCount ?? 0) + Number(a.rejectedCount ?? 0)
- stats[0].value = String(totalCount)
- stats[1].value = String(pending)
- stats[2].value = String(approved)
- stats[3].value = String(rejected)
- } catch {
- // error handled by interceptor
- }
- }
- function toggleMoreFilters() {
- showMoreFilters.value = !showMoreFilters.value
- }
- function handleSearch() {
- pager.currentPage.value = 1
- }
- function handleReset() {
- Object.assign(filterForm, { code: '', bizType: '', status: '', applyDate: null, amountRange: '' })
- pager.currentPage.value = 1
- }
- async function openViewDetail(row: any) {
- currentRow.value = row
- viewFlowNodes.value = []
- viewFlowLoading.value = true
- dialogs.viewDetail = true
- try {
- const api = row.bizType === '备用金' ? advanceApi : reimburseApi
- const res: any = await api.getApprovals(row.id)
- viewFlowNodes.value = (res.data || []).sort((a: any, b: any) => a.nodeOrder - b.nodeOrder)
- } catch (e: any) {
- ElMessage.error(e.message || '加载审批流程失败')
- } finally {
- viewFlowLoading.value = false
- }
- }
- onMounted(() => {
- loadData()
- })
- </script>
- <template>
- <div>
- <div class="page-head">
- <h1>我的申请单</h1>
- <p class="desc">查看本人发起的所有申请单据及审批进度。</p>
- </div>
- <div class="stats">
- <div v-for="item in stats" :key="item.name" class="stat">
- <p class="k">{{ item.name }}</p>
- <p class="v">{{ item.value }}</p>
- </div>
- </div>
- <ListPageScaffold
- panel-title="我的申请单列表"
- :show-more="showMoreFilters"
- :has-expandable-filters="true"
- :selected-count="0"
- :rows="pager.pagedRows.value"
- :range-text="pager.rangeText.value"
- :total="pager.total.value"
- :current-page="pager.currentPage.value"
- :page-size="pager.pageSize.value"
- @toggle-more="toggleMoreFilters"
- @reset="handleReset"
- @search="handleSearch"
- @update:current-page="pager.currentPage.value = $event"
- @update:page-size="pager.pageSize.value = $event"
- @size-change="pager.onSizeChange"
- >
- <template #filters>
- <div class="filter-grid">
- <div class="filter-field">
- <label>单号</label>
- <BaseInput v-model="filterForm.code" placeholder="请输入单号" />
- </div>
- <div class="filter-field">
- <label>业务类型</label>
- <BaseSelect v-model="filterForm.bizType" placeholder="请选择业务类型">
- <el-option v-for="t in bizTypeOptions" :key="t" :label="t" :value="t" />
- </BaseSelect>
- </div>
- <div class="filter-field">
- <label>状态</label>
- <BaseSelect v-model="filterForm.status" placeholder="请选择状态">
- <el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
- </BaseSelect>
- </div>
- <template v-if="showMoreFilters">
- <div class="filter-field">
- <label>申请时间</label>
- <el-date-picker
- v-model="filterForm.applyDate"
- type="date"
- value-format="YYYY-MM-DD"
- placeholder="选择日期"
- clearable
- class="bx-date-picker"
- />
- </div>
- <div class="filter-field">
- <label>金额区间</label>
- <BaseInput v-model="filterForm.amountRange" placeholder="如 1000-5000" />
- </div>
- </template>
- </div>
- </template>
- <template #toolbar />
- <template #table-columns>
- <el-table-column type="index" label="序号" width="90" />
- <el-table-column prop="applyTime" label="申请时间" min-width="160" />
- <el-table-column prop="applyCode" label="申请单号" min-width="140" />
- <el-table-column prop="bizType" label="业务类型" min-width="100">
- <template #default="{ row }">
- <BaseTag :tone="row.bizType === '报销单' ? 'processing' : 'info'">{{ row.bizType }}</BaseTag>
- </template>
- </el-table-column>
- <el-table-column prop="title" label="申请标题" min-width="180" />
- <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
- <el-table-column prop="status" label="状态" min-width="110">
- <template #default="{ row }">
- <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
- </template>
- </el-table-column>
- <el-table-column prop="currentNode" label="当前节点" min-width="130">
- <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="{ row }">
- <BaseOpButton primary :icon="View" @click="openViewDetail(row)">查看</BaseOpButton>
- </template>
- </el-table-column>
- </template>
- </ListPageScaffold>
- <BaseDialog v-model="dialogs.viewDetail" title="申请单详情" width="920px" class="dialog-view-xl">
- <div class="dlg-view">
- <div class="dlg-view-hero">
- <div class="dlg-view-hero-top">
- <h2 class="dlg-view-title">{{ currentRow?.applyCode || '-' }}</h2>
- <div class="dlg-view-tags">
- <BaseTag :tone="statusTone(currentRow?.status)">{{ statusLabel(currentRow?.status) }}</BaseTag>
- </div>
- </div>
- <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b> · 业务类型:{{ currentRow?.bizType || '-' }}</p>
- </div>
- <div class="dlg-view-section">
- <div class="dlg-kv dlg-kv--triple">
- <div class="k">申请单号</div>
- <div class="v">{{ currentRow?.applyCode || '-' }}</div>
- <div class="k">业务类型</div>
- <div class="v">{{ currentRow?.bizType || '-' }}</div>
- <div class="k">申请标题</div>
- <div class="v"><BaseClampText :text="currentRow?.title" /></div>
- <div class="k">申请金额(元)</div>
- <div class="v">{{ formatMoneyYuan(currentRow?.amount || 0) }}</div>
- <div class="k">状态</div>
- <div class="v">{{ statusLabel(currentRow?.status) }}</div>
- <div class="k">当前节点</div>
- <div class="v">{{ workflowNodeDisplay(currentRow?.currentNode, currentRow?.status) }}</div>
- <div class="k">申请时间</div>
- <div class="v">{{ currentRow?.applyTime || '-' }}</div>
- </div>
- </div>
- <div class="dlg-view-section">
- <h4>审批进度</h4>
- <div class="flow-scroll">
- <ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
- </div>
- </div>
- </div>
- </BaseDialog>
- </div>
- </template>
|