MinePage.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. <script setup lang="ts">
  2. import { computed, onMounted, reactive, ref } from 'vue'
  3. import { ElMessage } from 'element-plus'
  4. import { View } from '@element-plus/icons-vue'
  5. import { advanceApi, reimburseApi } from '../../api'
  6. import { statusLabel, statusTone, formatMoneyYuan, moneyTableFormatter, workflowNodeDisplay } from '../../utils/format'
  7. import WorkflowNodeCell from '../../components/base/WorkflowNodeCell.vue'
  8. import ListPageScaffold from '../../components/template/ListPageScaffold.vue'
  9. import BaseDialog from '../../components/base/BaseDialog.vue'
  10. import BaseInput from '../../components/base/BaseInput.vue'
  11. import BaseSelect from '../../components/base/BaseSelect.vue'
  12. import BaseTag from '../../components/base/BaseTag.vue'
  13. import BaseOpButton from '../../components/base/BaseOpButton.vue'
  14. import ApprovalFlowView from '../../components/base/ApprovalFlowView.vue'
  15. import BaseClampText from '../../components/base/BaseClampText.vue'
  16. import { usePagedTable } from '../../hooks/usePagedTable'
  17. type DialogKey = 'viewDetail'
  18. const showMoreFilters = ref(false)
  19. const loadingMap = reactive<Record<string, boolean>>({})
  20. const dialogs = reactive<Record<DialogKey, boolean>>({
  21. viewDetail: false
  22. })
  23. const mergedRows = ref<any[]>([])
  24. const filterForm = reactive({
  25. code: '',
  26. bizType: '' as string,
  27. status: '' as string,
  28. applyDate: null as string | null,
  29. amountRange: ''
  30. })
  31. function filterMineRows(rows: any[]) {
  32. let list = [...rows]
  33. const code = filterForm.code.trim()
  34. if (code) list = list.filter((r: any) => String(r.applyCode || '').includes(code))
  35. if (filterForm.bizType && filterForm.bizType !== '全部') {
  36. list = list.filter((r: any) => r.bizType === filterForm.bizType)
  37. }
  38. if (filterForm.status && filterForm.status !== '全部') {
  39. const map: Record<string, string> = {
  40. 待提交: 'draft',
  41. 审批中: 'pending',
  42. 审批通过: 'approved',
  43. 审批驳回: 'rejected',
  44. }
  45. const want = map[filterForm.status]
  46. if (want) list = list.filter((r: any) => r.status === want)
  47. }
  48. if (filterForm.applyDate) {
  49. const d = filterForm.applyDate
  50. list = list.filter((r: any) => String(r.applyTime || '').slice(0, 10) === d)
  51. }
  52. if (filterForm.amountRange?.trim()) {
  53. const parts = filterForm.amountRange.split(/[-–]/).map((x: string) => Number(String(x).trim()))
  54. if (parts.length >= 2 && !parts.some((n) => Number.isNaN(n))) {
  55. const lo = Math.min(parts[0], parts[1])
  56. const hi = Math.max(parts[0], parts[1])
  57. list = list.filter((r: any) => {
  58. const amt = Number(r.amount || 0)
  59. return amt >= lo && amt <= hi
  60. })
  61. }
  62. }
  63. return list
  64. }
  65. const filteredMergedRows = computed(() => filterMineRows(mergedRows.value))
  66. const pager = usePagedTable(() => filteredMergedRows.value, { initialPageSize: 10 })
  67. const currentRow = ref<any>(null)
  68. const viewFlowNodes = ref<any[]>([])
  69. const viewFlowLoading = ref(false)
  70. const stats = [
  71. { name: '全部申请', value: '-' },
  72. { name: '审批中', value: '-' },
  73. { name: '已通过', value: '-' },
  74. { name: '已驳回', value: '-' }
  75. ]
  76. const bizTypeOptions = ['全部', '报销单', '备用金']
  77. const statusOptions = ['全部', '待提交', '审批中', '审批通过', '审批驳回']
  78. async function loadData() {
  79. try {
  80. const [advRes, reimRes, advSt, reimSt]: any[] = await Promise.all([
  81. advanceApi.my({ page: 1, pageSize: 999 }),
  82. reimburseApi.my({ page: 1, pageSize: 999 }),
  83. advanceApi.myStats(),
  84. reimburseApi.myStats()
  85. ])
  86. const advRows = (advRes.data?.rows || []).map((r: any) => ({
  87. ...r, bizType: '备用金', title: r.reason,
  88. applyCode: r.applyCode || r.code
  89. }))
  90. const reimRows = (reimRes.data?.rows || []).map((r: any) => ({
  91. ...r, bizType: '报销单', title: r.title,
  92. applyCode: r.applyCode || r.code
  93. }))
  94. mergedRows.value = [...advRows, ...reimRows]
  95. const a = advSt.data || {}
  96. const r = reimSt.data || {}
  97. const totalCount = Number(r.totalCount ?? 0) + Number(a.totalCount ?? 0)
  98. const pending = Number(r.pendingCount ?? 0) + Number(a.pendingCount ?? 0)
  99. const approved =
  100. Number(r.approvedCount ?? 0) + Number(a.approvedPassCount ?? 0) + Number(a.issuedCount ?? 0)
  101. const rejected = Number(r.rejectedCount ?? 0) + Number(a.rejectedCount ?? 0)
  102. stats[0].value = String(totalCount)
  103. stats[1].value = String(pending)
  104. stats[2].value = String(approved)
  105. stats[3].value = String(rejected)
  106. } catch {
  107. // error handled by interceptor
  108. }
  109. }
  110. function toggleMoreFilters() {
  111. showMoreFilters.value = !showMoreFilters.value
  112. }
  113. function handleSearch() {
  114. pager.currentPage.value = 1
  115. }
  116. function handleReset() {
  117. Object.assign(filterForm, { code: '', bizType: '', status: '', applyDate: null, amountRange: '' })
  118. pager.currentPage.value = 1
  119. }
  120. async function openViewDetail(row: any) {
  121. currentRow.value = row
  122. viewFlowNodes.value = []
  123. viewFlowLoading.value = true
  124. dialogs.viewDetail = true
  125. try {
  126. const api = row.bizType === '备用金' ? advanceApi : reimburseApi
  127. const res: any = await api.getApprovals(row.id)
  128. viewFlowNodes.value = (res.data || []).sort((a: any, b: any) => a.nodeOrder - b.nodeOrder)
  129. } catch (e: any) {
  130. ElMessage.error(e.message || '加载审批流程失败')
  131. } finally {
  132. viewFlowLoading.value = false
  133. }
  134. }
  135. onMounted(() => {
  136. loadData()
  137. })
  138. </script>
  139. <template>
  140. <div>
  141. <div class="page-head">
  142. <h1>我的申请单</h1>
  143. <p class="desc">查看本人发起的所有申请单据及审批进度。</p>
  144. </div>
  145. <div class="stats">
  146. <div v-for="item in stats" :key="item.name" class="stat">
  147. <p class="k">{{ item.name }}</p>
  148. <p class="v">{{ item.value }}</p>
  149. </div>
  150. </div>
  151. <ListPageScaffold
  152. panel-title="我的申请单列表"
  153. :show-more="showMoreFilters"
  154. :has-expandable-filters="true"
  155. :selected-count="0"
  156. :rows="pager.pagedRows.value"
  157. :range-text="pager.rangeText.value"
  158. :total="pager.total.value"
  159. :current-page="pager.currentPage.value"
  160. :page-size="pager.pageSize.value"
  161. @toggle-more="toggleMoreFilters"
  162. @reset="handleReset"
  163. @search="handleSearch"
  164. @update:current-page="pager.currentPage.value = $event"
  165. @update:page-size="pager.pageSize.value = $event"
  166. @size-change="pager.onSizeChange"
  167. >
  168. <template #filters>
  169. <div class="filter-grid">
  170. <div class="filter-field">
  171. <label>单号</label>
  172. <BaseInput v-model="filterForm.code" placeholder="请输入单号" />
  173. </div>
  174. <div class="filter-field">
  175. <label>业务类型</label>
  176. <BaseSelect v-model="filterForm.bizType" placeholder="请选择业务类型">
  177. <el-option v-for="t in bizTypeOptions" :key="t" :label="t" :value="t" />
  178. </BaseSelect>
  179. </div>
  180. <div class="filter-field">
  181. <label>状态</label>
  182. <BaseSelect v-model="filterForm.status" placeholder="请选择状态">
  183. <el-option v-for="s in statusOptions" :key="s" :label="s" :value="s" />
  184. </BaseSelect>
  185. </div>
  186. <template v-if="showMoreFilters">
  187. <div class="filter-field">
  188. <label>申请时间</label>
  189. <el-date-picker
  190. v-model="filterForm.applyDate"
  191. type="date"
  192. value-format="YYYY-MM-DD"
  193. placeholder="选择日期"
  194. clearable
  195. class="bx-date-picker"
  196. />
  197. </div>
  198. <div class="filter-field">
  199. <label>金额区间</label>
  200. <BaseInput v-model="filterForm.amountRange" placeholder="如 1000-5000" />
  201. </div>
  202. </template>
  203. </div>
  204. </template>
  205. <template #toolbar />
  206. <template #table-columns>
  207. <el-table-column type="index" label="序号" width="90" />
  208. <el-table-column prop="applyTime" label="申请时间" min-width="160" />
  209. <el-table-column prop="applyCode" label="申请单号" min-width="140" />
  210. <el-table-column prop="bizType" label="业务类型" min-width="100">
  211. <template #default="{ row }">
  212. <BaseTag :tone="row.bizType === '报销单' ? 'processing' : 'info'">{{ row.bizType }}</BaseTag>
  213. </template>
  214. </el-table-column>
  215. <el-table-column prop="title" label="申请标题" min-width="180" />
  216. <el-table-column prop="amount" label="申请金额(元)" :formatter="moneyTableFormatter" class-name="col-money" min-width="120" />
  217. <el-table-column prop="status" label="状态" min-width="110">
  218. <template #default="{ row }">
  219. <BaseTag :tone="statusTone(row.status)">{{ statusLabel(row.status) }}</BaseTag>
  220. </template>
  221. </el-table-column>
  222. <el-table-column prop="currentNode" label="当前节点" min-width="130">
  223. <template #default="{ row }">
  224. <WorkflowNodeCell :current-node="row.currentNode" :status="row.status" />
  225. </template>
  226. </el-table-column>
  227. <el-table-column label="操作" width="120" fixed="right" class-name="col-actions">
  228. <template #default="{ row }">
  229. <BaseOpButton primary :icon="View" @click="openViewDetail(row)">查看</BaseOpButton>
  230. </template>
  231. </el-table-column>
  232. </template>
  233. </ListPageScaffold>
  234. <BaseDialog v-model="dialogs.viewDetail" title="申请单详情" width="920px" class="dialog-view-xl">
  235. <div class="dlg-view">
  236. <div class="dlg-view-hero">
  237. <div class="dlg-view-hero-top">
  238. <h2 class="dlg-view-title">{{ currentRow?.applyCode || '-' }}</h2>
  239. <div class="dlg-view-tags">
  240. <BaseTag :tone="statusTone(currentRow?.status)">{{ statusLabel(currentRow?.status) }}</BaseTag>
  241. </div>
  242. </div>
  243. <p class="dlg-view-meta">金额(元):<b class="money-em">{{ formatMoneyYuan(currentRow?.amount || 0) }}</b> · 业务类型:{{ currentRow?.bizType || '-' }}</p>
  244. </div>
  245. <div class="dlg-view-section">
  246. <div class="dlg-kv dlg-kv--triple">
  247. <div class="k">申请单号</div>
  248. <div class="v">{{ currentRow?.applyCode || '-' }}</div>
  249. <div class="k">业务类型</div>
  250. <div class="v">{{ currentRow?.bizType || '-' }}</div>
  251. <div class="k">申请标题</div>
  252. <div class="v"><BaseClampText :text="currentRow?.title" /></div>
  253. <div class="k">申请金额(元)</div>
  254. <div class="v">{{ formatMoneyYuan(currentRow?.amount || 0) }}</div>
  255. <div class="k">状态</div>
  256. <div class="v">{{ statusLabel(currentRow?.status) }}</div>
  257. <div class="k">当前节点</div>
  258. <div class="v">{{ workflowNodeDisplay(currentRow?.currentNode, currentRow?.status) }}</div>
  259. <div class="k">申请时间</div>
  260. <div class="v">{{ currentRow?.applyTime || '-' }}</div>
  261. </div>
  262. </div>
  263. <div class="dlg-view-section">
  264. <h4>审批进度</h4>
  265. <div class="flow-scroll">
  266. <ApprovalFlowView :nodes="viewFlowNodes" :loading="viewFlowLoading" />
  267. </div>
  268. </div>
  269. </div>
  270. </BaseDialog>
  271. </div>
  272. </template>