ApprovalPendingPanel.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415
  1. <template>
  2. <div class="freight-tab-panel">
  3. <div class="filter-block">
  4. <el-form :model="searchForm" class="filter-form" @submit.prevent>
  5. <div class="filter-grid">
  6. <el-form-item label="结算单号">
  7. <el-input
  8. v-model="searchForm.q"
  9. placeholder="搜索结算单号"
  10. clearable
  11. @keyup.enter="handleSearch"
  12. />
  13. </el-form-item>
  14. </div>
  15. <div class="filter-actions">
  16. <div class="filter-actions-end">
  17. <el-button class="btn-ghost" :loading="loading" @click="handleReset">
  18. <el-icon><RefreshLeft /></el-icon>重置
  19. </el-button>
  20. <el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
  21. <el-icon><Search /></el-icon>搜索
  22. </el-button>
  23. </div>
  24. </div>
  25. </el-form>
  26. </div>
  27. <div class="list-toolbar">
  28. <div class="list-toolbar-row">
  29. <ListQuickSearchInput
  30. v-model="quickSearchKeyword"
  31. placeholder="结算单号"
  32. @search="handleQuickSearch"
  33. @clear="handleQuickSearchClear"
  34. />
  35. <el-button
  36. class="btn-main"
  37. type="primary"
  38. :disabled="
  39. selectedRows.length !== 1 ||
  40. !selectedRows[0]?.canApprove ||
  41. !selectedRows[0]?.currentApprovalId
  42. "
  43. @click="handleToolbarApprove"
  44. >
  45. <el-icon><CircleCheck /></el-icon>通过
  46. </el-button>
  47. <el-button
  48. class="btn-ghost"
  49. type="danger"
  50. :disabled="
  51. selectedRows.length !== 1 ||
  52. !selectedRows[0]?.canApprove ||
  53. !selectedRows[0]?.currentApprovalId
  54. "
  55. @click="handleToolbarReject"
  56. >
  57. <el-icon><CircleClose /></el-icon>驳回
  58. </el-button>
  59. <ListExportDropdown
  60. :export-fn="freightApi.export"
  61. :get-filter-params="getSettlementExportFilterParams"
  62. :has-selection="true"
  63. :selected-ids="selectedRows.map((r) => r.id)"
  64. :visible-columns="settlementExportColumns"
  65. filename="待审批结算单.xlsx"
  66. />
  67. <ListToolbarEndActions
  68. :loading="loading"
  69. :column-defs="listColumnDefs"
  70. v-model:visible-columns="visibleColumns"
  71. v-model:check-all="checkAll"
  72. :is-indeterminate="isIndeterminate"
  73. @refresh="fetchData"
  74. @reset-columns="resetToDefault"
  75. />
  76. </div>
  77. </div>
  78. <div class="list-body list-wrap">
  79. <div ref="pendingTableWrapRef" class="table-scroll bx-list-table-wrap">
  80. <ListTable table-key="pl-freight-components-approval-pending-panel"
  81. class="list-table"
  82. :data="settlementList"
  83. v-loading="loading"
  84. stripe
  85. row-key="id"
  86. @selection-change="handleSelectionChange"
  87. @row-dblclick="handleView"
  88. @header-dragend="() => pendingStatsRowRef?.resync?.()"
  89. >
  90. <el-table-column type="selection" min-width="55" align="center" header-align="center" />
  91. <el-table-column
  92. type="index"
  93. label="序号"
  94. min-width="68"
  95. align="center"
  96. header-align="center"
  97. :index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
  98. />
  99. <el-table-column prop="settlementNo" label="结算单号" min-width="180" show-overflow-tooltip>
  100. <template #default="{ row }">
  101. <el-link type="primary" @click="handleView(row)">{{ row.settlementNo }}</el-link>
  102. </template>
  103. </el-table-column>
  104. <el-table-column
  105. v-if="isColumnVisible('totalFreight')"
  106. prop="totalFreight"
  107. label="运费合计(元)"
  108. min-width="130"
  109. align="right"
  110. >
  111. <template #default="{ row }">{{ formatAmount(row.totalFreight) }}</template>
  112. </el-table-column>
  113. <el-table-column
  114. v-if="isColumnVisible('planCount')"
  115. prop="planCount"
  116. label="关联计划数"
  117. min-width="110"
  118. align="center"
  119. />
  120. <el-table-column
  121. v-if="isColumnVisible('licensePlateSummary')"
  122. prop="licensePlateSummary"
  123. label="车牌摘要"
  124. min-width="140"
  125. show-overflow-tooltip
  126. />
  127. <el-table-column
  128. v-if="isColumnVisible('currentApproverName')"
  129. prop="currentApproverName"
  130. label="当前审批人"
  131. min-width="110"
  132. align="center"
  133. >
  134. <template #default="{ row }">{{ row.currentApproverName || '—' }}</template>
  135. </el-table-column>
  136. <el-table-column
  137. v-if="isColumnVisible('submitterName')"
  138. prop="submitterName"
  139. label="提交人"
  140. min-width="100"
  141. align="center"
  142. >
  143. <template #default="{ row }">{{ row.submitterName || '—' }}</template>
  144. </el-table-column>
  145. <el-table-column
  146. v-if="isColumnVisible('createTime')"
  147. prop="createTime"
  148. label="提交时间"
  149. min-width="160"
  150. align="center"
  151. >
  152. <template #default="{ row }">{{ formatDateTime(row.createTime) }}</template>
  153. </el-table-column>
  154. </ListTable>
  155. <ListColumnStatsRow
  156. ref="pendingStatsRowRef"
  157. storage-key="pl-freight-approval-pending-column-stats"
  158. :columns="pendingStatsRowColumns"
  159. :data="settlementList"
  160. :table-wrap-el="pendingTableWrapRef"
  161. :get-cell-value="getPendingStatCellValue"
  162. />
  163. </div>
  164. <div class="pagination list-pager-bar">
  165. <div class="list-pager-selected">
  166. 已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
  167. </div>
  168. <el-pagination
  169. v-model:current-page="pagination.page"
  170. v-model:page-size="pagination.pageSize"
  171. :total="pagination.total"
  172. :page-sizes="[10, 20, 100, 500]"
  173. layout="total, sizes, prev, pager, next"
  174. @size-change="handleSizeChange"
  175. @current-change="handlePageChange"
  176. />
  177. </div>
  178. </div>
  179. <PlFreightSettlementViewDialog ref="viewDialogRef">
  180. <template v-if="viewingRow" #hero-actions>
  181. <el-button
  182. v-if="viewingRow.canApprove && viewingRow.currentApprovalId"
  183. class="btn-main"
  184. type="primary"
  185. @click="handleApprove(viewingRow)"
  186. >
  187. <el-icon><CircleCheck /></el-icon>通过
  188. </el-button>
  189. <el-button
  190. v-if="viewingRow.canApprove && viewingRow.currentApprovalId"
  191. class="btn-ghost"
  192. type="danger"
  193. @click="handleReject(viewingRow)"
  194. >
  195. <el-icon><CircleClose /></el-icon>驳回
  196. </el-button>
  197. </template>
  198. </PlFreightSettlementViewDialog>
  199. <PlFreightApprovalDialog ref="approvalDialogRef" @success="handleMutationSuccess" />
  200. </div>
  201. </template>
  202. <script setup lang="ts">
  203. import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
  204. import ListToolbarEndActions from '@/components/business/list-toolbar-end-actions/index.vue'
  205. import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
  206. import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
  207. import type { StatsRowColumn } from '@/utils/list-column-stats'
  208. import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
  209. import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
  210. import { CircleCheck, CircleClose, RefreshLeft, Search } from '@element-plus/icons-vue'
  211. import { freightApi, type FreightSettlement } from '@/api/pl'
  212. import PlFreightApprovalDialog from '@/components/business/pl-freight-approval-dialog/index.vue'
  213. import PlFreightSettlementViewDialog from '@/components/business/pl-freight-settlement-view-dialog/index.vue'
  214. import { formatMoneyDisplay } from '@/utils/money-format'
  215. import { formatDateTime } from '@/utils/format'
  216. const listColumnDefs = [
  217. { prop: 'totalFreight', label: '运费合计(元)' },
  218. { prop: 'planCount', label: '关联计划数' },
  219. { prop: 'licensePlateSummary', label: '车牌摘要' },
  220. { prop: 'currentApproverName', label: '当前审批人' },
  221. { prop: 'submitterName', label: '提交人' },
  222. { prop: 'createTime', label: '提交时间' }
  223. ]
  224. const {
  225. visibleColumns,
  226. checkAll,
  227. isIndeterminate,
  228. isColumnVisible,
  229. resetToDefault
  230. } = usePersistedListColumns('pl-freight-approval-pending-columns', listColumnDefs)
  231. const pendingTableWrapRef = ref<HTMLElement | null>(null)
  232. const pendingStatsRowRef = ref<{ resync?: () => void } | null>(null)
  233. const pendingStatsRowColumns = computed((): StatsRowColumn[] => {
  234. const cols: StatsRowColumn[] = [
  235. { key: '__selection', interactive: false },
  236. { key: '__index', interactive: false },
  237. { key: 'settlementNo', interactive: true, prop: 'settlementNo' }
  238. ]
  239. for (const def of listColumnDefs) {
  240. if (!isColumnVisible(def.prop)) continue
  241. cols.push({
  242. key: def.prop,
  243. interactive: true,
  244. prop: def.prop,
  245. money: def.prop === 'totalFreight'
  246. })
  247. }
  248. return cols
  249. })
  250. function getPendingStatCellValue(row: FreightSettlement, prop: string): unknown {
  251. if (prop === 'currentApproverName') return row.currentApproverName || ''
  252. if (prop === 'submitterName') return row.submitterName || ''
  253. if (prop === 'createTime') {
  254. return row.createTime ? formatDateTime(row.createTime) : ''
  255. }
  256. return (row as any)?.[prop]
  257. }
  258. const loading = ref(false)
  259. const settlementList = ref<FreightSettlement[]>([])
  260. const selectedRows = ref<FreightSettlement[]>([])
  261. function getSettlementExportFilterParams() {
  262. return {
  263. tab: 'approval_pending',
  264. q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
  265. }
  266. }
  267. const settlementExportColumns = computed(() => {
  268. const cols = [...visibleColumns.value]
  269. if (!cols.includes('settlementNo')) cols.unshift('settlementNo')
  270. return cols
  271. })
  272. const viewingRow = ref<FreightSettlement | null>(null)
  273. const viewDialogRef = ref<InstanceType<typeof PlFreightSettlementViewDialog>>()
  274. const approvalDialogRef = ref<InstanceType<typeof PlFreightApprovalDialog>>()
  275. const quickSearchKeyword = ref('')
  276. const searchForm = reactive({ q: '' })
  277. const pagination = reactive({
  278. page: 1,
  279. pageSize: 20,
  280. total: 0
  281. })
  282. function formatAmount(amount: number) {
  283. if (!amount) return '¥0.00'
  284. return formatMoneyDisplay(amount)
  285. }
  286. function handleSelectionChange(selection: FreightSettlement[]) {
  287. selectedRows.value = selection
  288. }
  289. async function fetchData() {
  290. loading.value = true
  291. try {
  292. const res = await freightApi.list({
  293. tab: 'approval_pending',
  294. q: quickSearchKeyword.value.trim() || searchForm.q || undefined,
  295. page: pagination.page,
  296. pageSize: pagination.pageSize
  297. })
  298. const data = res.data ?? res
  299. settlementList.value = data.list || []
  300. pagination.total = data.total || 0
  301. } finally {
  302. loading.value = false
  303. }
  304. }
  305. function handleSearch() {
  306. quickSearchKeyword.value = ''
  307. pagination.page = 1
  308. fetchData()
  309. }
  310. function handleReset() {
  311. searchForm.q = ''
  312. quickSearchKeyword.value = ''
  313. pagination.page = 1
  314. fetchData()
  315. }
  316. function handleQuickSearch() {
  317. pagination.page = 1
  318. fetchData()
  319. }
  320. function handleQuickSearchClear() {
  321. quickSearchKeyword.value = ''
  322. pagination.page = 1
  323. fetchData()
  324. }
  325. function handlePageChange() {
  326. fetchData()
  327. }
  328. function handleSizeChange() {
  329. pagination.page = 1
  330. fetchData()
  331. }
  332. function handleView(row: FreightSettlement) {
  333. viewingRow.value = row
  334. viewDialogRef.value?.open(row.id)
  335. }
  336. function handleApprove(row: FreightSettlement) {
  337. if (!row.currentApprovalId) return
  338. approvalDialogRef.value?.openApprove(row.id, row.currentApprovalId)
  339. }
  340. function handleReject(row: FreightSettlement) {
  341. if (!row.currentApprovalId) return
  342. approvalDialogRef.value?.openReject(row.id, row.currentApprovalId)
  343. }
  344. function syncViewingRowFromList() {
  345. if (!viewingRow.value?.id) return
  346. const updated = settlementList.value.find(row => row.id === viewingRow.value!.id)
  347. if (updated) viewingRow.value = updated
  348. }
  349. async function handleMutationSuccess() {
  350. await fetchData()
  351. syncViewingRowFromList()
  352. await viewDialogRef.value?.reload()
  353. }
  354. function handleToolbarApprove() {
  355. if (selectedRows.value.length !== 1) return
  356. handleApprove(selectedRows.value[0])
  357. }
  358. function handleToolbarReject() {
  359. if (selectedRows.value.length !== 1) return
  360. handleReject(selectedRows.value[0])
  361. }
  362. function refresh() {
  363. fetchData()
  364. }
  365. useDbInstantSearch(() => ({ q: searchForm.q }), handleSearch)
  366. onMounted(() => {
  367. fetchData()
  368. })
  369. defineExpose({ refresh })
  370. </script>
  371. <style scoped lang="scss">
  372. .bx-list-table-wrap {
  373. min-width: 0;
  374. }
  375. </style>