Commit 1f9c1b4a0e1f82dcc713b0155fba5aa89a6c2f75

Authored by shaofan
1 parent 22489d64

Refactor: 新增资金对账功能,扩展商户与骑手账单查询接口,完善路由配置,更新UI展示和交互逻辑。

src/api/index.ts
... ... @@ -114,6 +114,10 @@ export const merchantApi = {
114 114 request.post('/api/admin/merchant/store/updateFeeConfig', null,
115 115 { params: { storeId, freeShipping, upToSend } }),
116 116 delStore: (storeId: number) => request.delete('/api/admin/merchant/store/del', { params: { storeId } }),
  117 + merchantBillList: (params: any) => request.get('/api/admin/merchant/fund/merchant-bills', { params }),
  118 + merchantBillDetail: (params: any) => request.get('/api/admin/merchant/fund/merchant-bill-detail', { params }),
  119 + riderBillList: (params: any) => request.get('/api/admin/merchant/fund/rider-bills', { params }),
  120 + riderBillDetail: (params: any) => request.get('/api/admin/merchant/fund/rider-bill-detail', { params }),
117 121 }
118 122  
119 123 // 骑手管理
... ...
src/router/index.ts
... ... @@ -46,6 +46,12 @@ const router = createRouter({
46 46 meta: { title: '店铺管理' },
47 47 },
48 48 {
  49 + path: 'merchant/fund',
  50 + name: 'MerchantFund',
  51 + component: () => import('@/views/merchant/FundReconciliation.vue'),
  52 + meta: { title: '资金对账' },
  53 + },
  54 + {
49 55 path: 'rider',
50 56 name: 'Rider',
51 57 component: () => import('@/views/rider/RiderList.vue'),
... ...
src/views/merchant/FundReconciliation.vue 0 → 100644
  1 +<template>
  2 + <div>
  3 + <a-card title="资金对账" :bordered="false" class="list-table-card">
  4 + <div class="soft-page-stack">
  5 + <div class="soft-note-card">
  6 + <strong>对账说明</strong>
  7 + <p>本页仅提供商户账单与自营骑手账单查询,不包含第三方运力账单与骑手提现审核。</p>
  8 + </div>
  9 +
  10 + <div class="fund-tabs">
  11 + <button
  12 + v-for="item in tabs"
  13 + :key="item.key"
  14 + type="button"
  15 + class="fund-tab"
  16 + :class="{ 'is-active': activeTab === item.key }"
  17 + @click="switchTab(item.key)"
  18 + >
  19 + {{ item.label }}
  20 + </button>
  21 + </div>
  22 +
  23 + <div class="list-toolbar">
  24 + <div class="list-toolbar-left">
  25 + <a-select
  26 + v-if="isAdmin"
  27 + v-model:value="filterCityId"
  28 + placeholder="租户"
  29 + allowClear
  30 + class="list-filter"
  31 + @change="handleFilterChange"
  32 + >
  33 + <a-select-option v-for="item in cityList" :key="item.id" :value="item.id">{{ item.name }}</a-select-option>
  34 + </a-select>
  35 + <a-date-picker
  36 + v-model:value="startDate"
  37 + value-format="YYYY-MM-DD"
  38 + placeholder="开始日期"
  39 + class="list-filter"
  40 + @change="handleFilterChange"
  41 + />
  42 + <a-date-picker
  43 + v-model:value="endDate"
  44 + value-format="YYYY-MM-DD"
  45 + placeholder="结束日期"
  46 + class="list-filter"
  47 + @change="handleFilterChange"
  48 + />
  49 + <a-input-search
  50 + v-model:value="keyword"
  51 + :placeholder="activeTab === 'merchant' ? '搜索店铺名' : '搜索骑手姓名/手机号'"
  52 + class="list-search"
  53 + @search="handleKeywordSearch"
  54 + />
  55 + </div>
  56 + </div>
  57 +
  58 + <a-table
  59 + v-if="activeTab === 'merchant'"
  60 + :dataSource="merchantList"
  61 + :columns="merchantColumns"
  62 + :loading="loading"
  63 + rowKey="outStoreId"
  64 + :pagination="pagination"
  65 + @change="handleTableChange"
  66 + >
  67 + <template #bodyCell="{ column, record }">
  68 + <template v-if="column.key === 'cityName'">
  69 + {{ getCityName(record.cityId) }}
  70 + </template>
  71 + <template v-else-if="column.key === 'orderAmount'">
  72 + ¥{{ formatMoney(record.orderAmount) }}
  73 + </template>
  74 + <template v-else-if="column.key === 'deliveryAmount'">
  75 + ¥{{ formatMoney(record.deliveryAmount) }}
  76 + </template>
  77 + <template v-else-if="column.key === 'refundAmount'">
  78 + ¥{{ formatMoney(record.refundAmount) }}
  79 + </template>
  80 + <template v-else-if="column.key === 'merchantReceivableAmount'">
  81 + ¥{{ formatMoney(record.merchantReceivableAmount) }}
  82 + </template>
  83 + <template v-else-if="column.key === 'action'">
  84 + <a @click="openMerchantDetail(record)">查看明细</a>
  85 + </template>
  86 + </template>
  87 + </a-table>
  88 +
  89 + <a-table
  90 + v-else
  91 + :dataSource="riderList"
  92 + :columns="riderColumns"
  93 + :loading="loading"
  94 + rowKey="riderId"
  95 + :pagination="pagination"
  96 + @change="handleTableChange"
  97 + >
  98 + <template #bodyCell="{ column, record }">
  99 + <template v-if="column.key === 'deliveryAmount'">
  100 + ¥{{ formatMoney(record.deliveryAmount) }}
  101 + </template>
  102 + <template v-else-if="column.key === 'riderIncomeAmount'">
  103 + ¥{{ formatMoney(record.riderIncomeAmount) }}
  104 + </template>
  105 + <template v-else-if="column.key === 'refundAdjustAmount'">
  106 + ¥{{ formatMoney(record.refundAdjustAmount) }}
  107 + </template>
  108 + <template v-else-if="column.key === 'settleableAmount'">
  109 + ¥{{ formatMoney(record.settleableAmount) }}
  110 + </template>
  111 + <template v-else-if="column.key === 'action'">
  112 + <a @click="openRiderDetail(record)">查看明细</a>
  113 + </template>
  114 + </template>
  115 + </a-table>
  116 + </div>
  117 + </a-card>
  118 +
  119 + <a-modal v-model:open="detailVisible" :title="detailTitle" :footer="null" width="920px">
  120 + <div class="soft-page-stack">
  121 + <div class="soft-note-card">
  122 + <strong>{{ detailTitle }}</strong>
  123 + <p>{{ activeDetailTab === 'merchant' ? '展示商户账单关联订单明细。' : '展示骑手账单关联订单明细。' }}</p>
  124 + </div>
  125 +
  126 + <a-table
  127 + :dataSource="detailList"
  128 + :columns="detailColumns"
  129 + :loading="detailLoading"
  130 + rowKey="orderId"
  131 + :pagination="detailPagination"
  132 + @change="handleDetailTableChange"
  133 + >
  134 + <template #bodyCell="{ column, record }">
  135 + <template v-if="column.key === 'status'">
  136 + <a-tag :color="statusColor[record.status] || 'default'">{{ statusMap[record.status] || `状态${record.status}` }}</a-tag>
  137 + </template>
  138 + <template v-else-if="column.key === 'payTime'">
  139 + {{ formatTime(record.payTime) }}
  140 + </template>
  141 + <template v-else-if="column.key === 'completeTime'">
  142 + {{ formatTime(record.completeTime) }}
  143 + </template>
  144 + <template v-else-if="column.key === 'orderAmount' || column.key === 'deliveryAmount' || column.key === 'refundAmount' || column.key === 'riderIncomeAmount' || column.key === 'settleableAmount'">
  145 + ¥{{ formatMoney(record[column.key]) }}
  146 + </template>
  147 + </template>
  148 + </a-table>
  149 + </div>
  150 + </a-modal>
  151 + </div>
  152 +</template>
  153 +
  154 +<script setup lang="ts">
  155 +import { computed, onMounted, ref } from 'vue'
  156 +import type { TablePaginationConfig } from 'ant-design-vue'
  157 +import { merchantApi } from '@/api'
  158 +import { useRoleCityList } from '@/composables/useRoleCityList'
  159 +
  160 +const PAGE_SIZE = 20
  161 +const tabs = [
  162 + { key: 'merchant', label: '商户账单' },
  163 + { key: 'rider', label: '自营骑手账单' },
  164 +] as const
  165 +
  166 +const activeTab = ref<'merchant' | 'rider'>('merchant')
  167 +const activeDetailTab = ref<'merchant' | 'rider'>('merchant')
  168 +const loading = ref(false)
  169 +const detailLoading = ref(false)
  170 +const detailVisible = ref(false)
  171 +const detailTitle = ref('账单明细')
  172 +const merchantList = ref<any[]>([])
  173 +const riderList = ref<any[]>([])
  174 +const detailList = ref<any[]>([])
  175 +const total = ref(0)
  176 +const currentPage = ref(1)
  177 +const pageSize = ref(PAGE_SIZE)
  178 +const detailTotal = ref(0)
  179 +const detailPage = ref(1)
  180 +const detailPageSize = ref(PAGE_SIZE)
  181 +const filterCityId = ref<number | undefined>()
  182 +const keyword = ref('')
  183 +const startDate = ref('2026-04-01')
  184 +const endDate = ref('2026-04-30')
  185 +const currentOutStoreId = ref('')
  186 +const currentDetailCityId = ref<number | undefined>()
  187 +const currentRiderId = ref<number | undefined>()
  188 +const { isAdmin, managedCityId, cityList, loadCities, getCityName } = useRoleCityList()
  189 +
  190 +const statusMap: Record<number, string> = {
  191 + 2: '已支付',
  192 + 3: '已接单',
  193 + 4: '服务中',
  194 + 6: '已完成',
  195 + 7: '退款申请',
  196 + 8: '退款成功',
  197 + 9: '退款拒绝',
  198 +}
  199 +
  200 +const statusColor: Record<number, string> = {
  201 + 2: 'blue',
  202 + 3: 'cyan',
  203 + 4: 'processing',
  204 + 6: 'green',
  205 + 7: 'orange',
  206 + 8: 'green',
  207 + 9: 'red',
  208 +}
  209 +
  210 +const merchantColumns = computed(() => {
  211 + const base = [
  212 + { title: '店铺ID', dataIndex: 'storeId', width: 100 },
  213 + { title: '店铺名称', dataIndex: 'storeName', ellipsis: true },
  214 + { title: '外部门店编号', dataIndex: 'outStoreId', ellipsis: true },
  215 + { title: '完成订单数', dataIndex: 'orderCount', width: 120 },
  216 + { title: '订单金额合计', key: 'orderAmount', width: 130 },
  217 + { title: '配送费合计', key: 'deliveryAmount', width: 130 },
  218 + { title: '已退款金额', key: 'refundAmount', width: 130 },
  219 + { title: '商户应收参考金额', key: 'merchantReceivableAmount', width: 160 },
  220 + { title: '操作', key: 'action', width: 100 },
  221 + ]
  222 + return isAdmin.value ? [{ title: '租户', key: 'cityName', width: 120 }, ...base] : base
  223 +})
  224 +
  225 +const riderColumns = [
  226 + { title: '骑手ID', dataIndex: 'riderId', width: 100 },
  227 + { title: '骑手姓名', dataIndex: 'riderName', width: 120 },
  228 + { title: '手机号', dataIndex: 'mobile', width: 140 },
  229 + { title: '完成订单数', dataIndex: 'orderCount', width: 120 },
  230 + { title: '配送费合计', key: 'deliveryAmount', width: 130 },
  231 + { title: '骑手收入合计', key: 'riderIncomeAmount', width: 140 },
  232 + { title: '退款影响参考金额', key: 'refundAdjustAmount', width: 150 },
  233 + { title: '可结算参考金额', key: 'settleableAmount', width: 150 },
  234 + { title: '操作', key: 'action', width: 100 },
  235 +]
  236 +
  237 +const detailColumns = computed(() => {
  238 + if (activeDetailTab.value === 'merchant') {
  239 + return [
  240 + { title: '订单ID', dataIndex: 'orderId', width: 100 },
  241 + { title: '订单号', dataIndex: 'orderNo', ellipsis: true },
  242 + { title: '支付时间', key: 'payTime', width: 170 },
  243 + { title: '完成时间', key: 'completeTime', width: 170 },
  244 + { title: '状态', key: 'status', width: 120 },
  245 + { title: '订单金额', key: 'orderAmount', width: 120 },
  246 + { title: '配送费', key: 'deliveryAmount', width: 120 },
  247 + { title: '退款金额', key: 'refundAmount', width: 120 },
  248 + ]
  249 + }
  250 + return [
  251 + { title: '订单ID', dataIndex: 'orderId', width: 100 },
  252 + { title: '订单号', dataIndex: 'orderNo', ellipsis: true },
  253 + { title: '完成时间', key: 'completeTime', width: 170 },
  254 + { title: '状态', key: 'status', width: 120 },
  255 + { title: '配送费', key: 'deliveryAmount', width: 120 },
  256 + { title: '骑手收入', key: 'riderIncomeAmount', width: 120 },
  257 + { title: '退款影响金额', key: 'refundAmount', width: 130 },
  258 + { title: '可结算参考金额', key: 'settleableAmount', width: 140 },
  259 + ]
  260 +})
  261 +
  262 +const pagination = computed<TablePaginationConfig>(() => ({
  263 + current: currentPage.value,
  264 + pageSize: pageSize.value,
  265 + total: total.value,
  266 + showSizeChanger: false,
  267 + showTotal: (count) => `共 ${count} 条`,
  268 +}))
  269 +
  270 +const detailPagination = computed<TablePaginationConfig>(() => ({
  271 + current: detailPage.value,
  272 + pageSize: detailPageSize.value,
  273 + total: detailTotal.value,
  274 + showSizeChanger: false,
  275 + showTotal: (count) => `共 ${count} 条`,
  276 +}))
  277 +
  278 +function switchTab(tab: 'merchant' | 'rider') {
  279 + if (activeTab.value === tab) {
  280 + return
  281 + }
  282 + detailVisible.value = false
  283 + activeTab.value = tab
  284 + currentPage.value = 1
  285 + loadList()
  286 +}
  287 +
  288 +function handleFilterChange() {
  289 + detailVisible.value = false
  290 + currentPage.value = 1
  291 + loadList()
  292 +}
  293 +
  294 +function handleKeywordSearch() {
  295 + detailVisible.value = false
  296 + currentPage.value = 1
  297 + loadList()
  298 +}
  299 +
  300 +function handleTableChange(page: TablePaginationConfig) {
  301 + const nextPage = page.current ?? 1
  302 + if (nextPage === currentPage.value) {
  303 + return
  304 + }
  305 + currentPage.value = nextPage
  306 + loadList()
  307 +}
  308 +
  309 +function handleDetailTableChange(page: TablePaginationConfig) {
  310 + const nextPage = page.current ?? 1
  311 + if (nextPage === detailPage.value) {
  312 + return
  313 + }
  314 + detailPage.value = nextPage
  315 + loadDetail()
  316 +}
  317 +
  318 +function getQueryParams() {
  319 + return {
  320 + cityId: isAdmin.value ? filterCityId.value : managedCityId.value,
  321 + keyword: keyword.value || undefined,
  322 + startDate: startDate.value,
  323 + endDate: endDate.value,
  324 + page: currentPage.value,
  325 + }
  326 +}
  327 +
  328 +async function loadList() {
  329 + loading.value = true
  330 + try {
  331 + if (activeTab.value === 'merchant') {
  332 + const res: any = await merchantApi.merchantBillList(getQueryParams())
  333 + const data = res?.data ?? {}
  334 + merchantList.value = Array.isArray(data.list) ? data.list : []
  335 + total.value = Number(data.total ?? 0)
  336 + currentPage.value = Number(data.page ?? currentPage.value)
  337 + pageSize.value = Number(data.pageSize ?? PAGE_SIZE)
  338 + } else {
  339 + const res: any = await merchantApi.riderBillList(getQueryParams())
  340 + const data = res?.data ?? {}
  341 + riderList.value = Array.isArray(data.list) ? data.list : []
  342 + total.value = Number(data.total ?? 0)
  343 + currentPage.value = Number(data.page ?? currentPage.value)
  344 + pageSize.value = Number(data.pageSize ?? PAGE_SIZE)
  345 + }
  346 + } finally {
  347 + loading.value = false
  348 + }
  349 +}
  350 +
  351 +async function openMerchantDetail(record: any) {
  352 + activeDetailTab.value = 'merchant'
  353 + detailTitle.value = `${record.storeName} 账单明细`
  354 + currentOutStoreId.value = record.outStoreId
  355 + currentDetailCityId.value = record.cityId
  356 + detailPage.value = 1
  357 + detailVisible.value = true
  358 + await loadDetail()
  359 +}
  360 +
  361 +async function openRiderDetail(record: any) {
  362 + activeDetailTab.value = 'rider'
  363 + detailTitle.value = `${record.riderName} 账单明细`
  364 + currentDetailCityId.value = isAdmin.value ? filterCityId.value : managedCityId.value
  365 + currentRiderId.value = record.riderId
  366 + detailPage.value = 1
  367 + detailVisible.value = true
  368 + await loadDetail()
  369 +}
  370 +
  371 +async function loadDetail() {
  372 + detailLoading.value = true
  373 + try {
  374 + if (activeDetailTab.value === 'merchant') {
  375 + const res: any = await merchantApi.merchantBillDetail({
  376 + cityId: currentDetailCityId.value ?? (isAdmin.value ? filterCityId.value : managedCityId.value),
  377 + outStoreId: currentOutStoreId.value,
  378 + startDate: startDate.value,
  379 + endDate: endDate.value,
  380 + page: detailPage.value,
  381 + })
  382 + const data = res?.data ?? {}
  383 + detailList.value = Array.isArray(data.list) ? data.list : []
  384 + detailTotal.value = Number(data.total ?? 0)
  385 + detailPage.value = Number(data.page ?? detailPage.value)
  386 + detailPageSize.value = Number(data.pageSize ?? PAGE_SIZE)
  387 + } else {
  388 + const res: any = await merchantApi.riderBillDetail({
  389 + cityId: currentDetailCityId.value ?? (isAdmin.value ? filterCityId.value : managedCityId.value),
  390 + riderId: currentRiderId.value,
  391 + startDate: startDate.value,
  392 + endDate: endDate.value,
  393 + page: detailPage.value,
  394 + })
  395 + const data = res?.data ?? {}
  396 + detailList.value = Array.isArray(data.list) ? data.list : []
  397 + detailTotal.value = Number(data.total ?? 0)
  398 + detailPage.value = Number(data.page ?? detailPage.value)
  399 + detailPageSize.value = Number(data.pageSize ?? PAGE_SIZE)
  400 + }
  401 + } finally {
  402 + detailLoading.value = false
  403 + }
  404 +}
  405 +
  406 +function formatMoney(value: number | string | null | undefined) {
  407 + const amount = Number(value ?? 0)
  408 + return amount.toFixed(2)
  409 +}
  410 +
  411 +function formatTime(value: number | null | undefined) {
  412 + if (!value) {
  413 + return '-'
  414 + }
  415 + return new Date(Number(value) * 1000).toLocaleString('zh-CN', { hour12: false })
  416 +}
  417 +
  418 +onMounted(async () => {
  419 + await loadCities()
  420 + if (!isAdmin.value) {
  421 + filterCityId.value = managedCityId.value
  422 + }
  423 + currentDetailCityId.value = isAdmin.value ? filterCityId.value : managedCityId.value
  424 + await loadList()
  425 +})
  426 +</script>
  427 +
  428 +<style scoped>
  429 +.fund-tabs {
  430 + display: flex;
  431 + gap: 12px;
  432 + margin-bottom: 4px;
  433 +}
  434 +
  435 +.fund-tab {
  436 + border: 1px solid var(--line);
  437 + background: var(--panel-strong);
  438 + color: var(--text-main);
  439 + border-radius: 999px;
  440 + padding: 8px 18px;
  441 + font-size: 13px;
  442 + font-weight: 700;
  443 + cursor: pointer;
  444 + transition: all 0.2s ease;
  445 +}
  446 +
  447 +.fund-tab:hover {
  448 + border-color: rgba(140, 124, 240, 0.48);
  449 +}
  450 +
  451 +.fund-tab.is-active {
  452 + color: #fff;
  453 + border-color: transparent;
  454 + background: linear-gradient(135deg, #8c7cf0 0%, #c995ea 100%);
  455 + box-shadow: 0 12px 24px rgba(140, 124, 240, 0.24);
  456 +}
  457 +</style>
... ...