feat(pickup): support platform managed offline settlement

This commit is contained in:
yml2213
2026-08-09 20:20:39 +08:00
parent 14bc91b007
commit 8fb7d3bac0
12 changed files with 499 additions and 128 deletions
@@ -2,6 +2,8 @@ import { apiClient } from '@/shared/api/client'
import type { ApiResponse, PaginatedResult } from '@/shared/types/types'
export type PickupAccountSource = 'internal' | 'external_platform_managed'
export type PickupSettlementMode = 'owner_wallet' | 'platform_managed'
export type PickupOfflineSettlementStatus = 'none' | 'pending' | 'settled'
export type AvailableListingSourceType = '' | 'external' | 'internal'
export interface AdminPickup {
@@ -20,6 +22,7 @@ export interface AdminPickup {
shop_name: string
account_source: PickupAccountSource
source_channel: string
settlement_mode: PickupSettlementMode
listing_price_cent: number
owner_price_cent: number
website_profit_cent: number
@@ -29,6 +32,10 @@ export interface AdminPickup {
account_snapshot?: Record<string, unknown>
settle_amount_cent: number
status: string
offline_settlement_status: PickupOfflineSettlementStatus
offline_settlement_remark: string
offline_settled_by?: number
offline_settled_at?: string
remark: string
complete_remark: string
created_at: string
@@ -79,6 +86,10 @@ export interface AdminPickupProfitUpdateRequest {
reason?: string
}
export interface AdminPickupOfflineSettlementRequest {
remark?: string
}
export interface AdminPickupListQuery {
page?: number
page_size?: number
@@ -150,6 +161,17 @@ export async function completeAdminPickup(id: number, req: AdminPickupCompleteRe
return data.data
}
export async function confirmAdminPickupOfflineSettlement(
id: number,
req: AdminPickupOfflineSettlementRequest = {}
) {
const { data } = await apiClient.post<ApiResponse<AdminPickup>>(
`/admin/pickups/${id}/offline-settlement`,
req
)
return data.data
}
export async function updateAdminPickupProfit(id: number, req: AdminPickupProfitUpdateRequest) {
const { data } = await apiClient.put<ApiResponse<AdminPickup>>(`/admin/pickups/${id}/profit`, req)
return data.data
@@ -157,6 +157,17 @@ function pickupSourceTagType(source: AdminPickup['account_source']) {
return source === 'external_platform_managed' ? 'primary' : 'info'
}
function settlementModeLabel(row: AdminPickup) {
return row.settlement_mode === 'platform_managed' ? '平台线下结算' : '号主钱包结算'
}
function offlineSettlementLabel(row: AdminPickup) {
if (row.settlement_mode !== 'platform_managed') return '钱包已结算'
if (row.offline_settlement_status === 'pending') return '待线下结算'
if (row.offline_settlement_status === 'settled') return '已线下结算'
return '待完成提号'
}
function ratioText(value: number) {
const ratio = Number(value || 0)
if (ratio <= 0) return '-'
@@ -317,6 +328,18 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
<span v-if="pickup.source_channel">{{ pickup.source_channel }}</span>
</dd>
</div>
<div>
<dt>结算方式</dt>
<dd>{{ settlementModeLabel(pickup) }}</dd>
</div>
<div>
<dt>结算状态</dt>
<dd>{{ offlineSettlementLabel(pickup) }}</dd>
</div>
<div v-if="pickup.settlement_mode === 'platform_managed'">
<dt>线下结算时间</dt>
<dd>{{ formatDateTime(pickup.offline_settled_at, '待确认') }}</dd>
</div>
<div>
<dt>交易渠道</dt>
<dd>{{ pickup.platform || '-' }}</dd>
@@ -371,6 +394,10 @@ function readSnapshotResources(summary: Record<string, unknown> | null): Snapsho
<dt>完成备注</dt>
<dd>{{ pickup.complete_remark || '-' }}</dd>
</div>
<div v-if="pickup.settlement_mode === 'platform_managed'" class="wide">
<dt>线下结算备注</dt>
<dd>{{ pickup.offline_settlement_remark || '-' }}</dd>
</div>
</dl>
</section>
@@ -4,6 +4,7 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { EditPen, Plus, Refresh, Search } from '@element-plus/icons-vue'
import {
cancelAdminPickup,
confirmAdminPickupOfflineSettlement,
completeAdminPickup,
createAdminPickup,
fetchAdminPickups,
@@ -36,6 +37,7 @@ const createDialogVisible = ref(false)
const completeDialogVisible = ref(false)
const profitDialogVisible = ref(false)
const activePickupId = ref(0)
const activePickup = ref<AdminPickup | null>(null)
const profitSaving = ref(false)
const createForm = reactive({
@@ -184,12 +186,41 @@ async function handleCreate() {
function openCompleteDialog(row: AdminPickup) {
activePickupId.value = row.id
activePickup.value = row
completeForm.settle_amount = 0
completeForm.profit_amount = centToYuan(row.profit_amount_cent)
completeForm.complete_remark = ''
completeDialogVisible.value = true
}
async function handleConfirmOfflineSettlement(row: AdminPickup) {
try {
const { value } = await ElMessageBox.prompt(
`确认已向卖家线下支付 ${formatCentWithSymbol(row.settle_amount_cent)}`,
'确认线下结算',
{
confirmButtonText: '确认已打款',
cancelButtonText: '取消',
inputType: 'textarea',
inputPlaceholder: '结算备注(可选)',
inputValidator: input => input.length <= 255 || '备注不能超过 255 个字符',
}
)
loading.value = true
try {
await confirmAdminPickupOfflineSettlement(row.id, { remark: value })
ElMessage.success('已确认线下结算')
loadList()
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '确认失败')
} finally {
loading.value = false
}
} catch {
/* 用户放弃 */
}
}
function openProfitDialog(row: AdminPickup) {
activePickupId.value = row.id
profitForm.profit_amount = centToYuan(row.profit_amount_cent)
@@ -213,7 +244,11 @@ async function handleComplete() {
profit_amount_cent: yuanToCent(completeForm.profit_amount),
complete_remark: completeForm.complete_remark,
})
ElMessage.success('提号已完成,已给卖家结算')
ElMessage.success(
activePickup.value?.settlement_mode === 'platform_managed'
? '提号已完成,待线下结算'
: '提号已完成,已给卖家结算'
)
completeDialogVisible.value = false
loadList()
} catch (e: unknown) {
@@ -310,6 +345,24 @@ function pickupSourceTagType(source: PickupAccountSource) {
return isExternalSource(source) ? 'primary' : 'info'
}
function isPlatformManaged(row: AdminPickup) {
return row.settlement_mode === 'platform_managed'
}
function offlineSettlementLabel(row: AdminPickup) {
if (!isPlatformManaged(row)) return '钱包已结算'
if (row.offline_settlement_status === 'pending') return '待线下结算'
if (row.offline_settlement_status === 'settled') return '已线下结算'
return '待完成提号'
}
function offlineSettlementTagType(row: AdminPickup) {
if (!isPlatformManaged(row)) return 'success'
if (row.offline_settlement_status === 'pending') return 'warning'
if (row.offline_settlement_status === 'settled') return 'success'
return 'info'
}
function listingSourceText(item: AvailableListing) {
const label = pickupSourceLabel(item.account_source)
return item.source_channel ? `${label} · ${item.source_channel}` : label
@@ -326,7 +379,7 @@ function listingOptionLabel(item: AvailableListing) {
<div class="page-header">
<p class="eyebrow">Admin Pickup</p>
<h1>线下提号</h1>
<p>管理员线下提号跳过支付与交接流程完成后给卖家结算余额</p>
<p>管理员线下提号站内上传结算到钱包平台代管通过线下结算确认打款</p>
</div>
<div class="toolbar-actions">
<el-button type="primary" :icon="Plus" @click="openCreateDialog">创建提号</el-button>
@@ -416,10 +469,17 @@ function listingOptionLabel(item: AvailableListing) {
</el-tag>
</template>
</el-table-column>
<el-table-column label="结算状态" width="125">
<template #default="{ row }">
<el-tag :type="offlineSettlementTagType(row)" effect="light">
{{ offlineSettlementLabel(row) }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" width="170">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="300" fixed="right">
<el-table-column label="操作" width="340" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<RouterLink :to="adminPath(`pickup/${row.id}`)">
@@ -441,7 +501,16 @@ function listingOptionLabel(item: AvailableListing) {
size="small"
@click="openCompleteDialog(row)"
>
完成结算
完成提号
</el-button>
<el-button
v-if="row.status === 'completed' && row.offline_settlement_status === 'pending'"
type="primary"
size="small"
plain
@click="handleConfirmOfflineSettlement(row)"
>
确认线下结算
</el-button>
<el-button
v-if="row.status === 'picking_up'"
@@ -599,8 +668,8 @@ function listingOptionLabel(item: AvailableListing) {
</template>
</el-dialog>
<!-- 完成结算 -->
<el-dialog v-model="completeDialogVisible" title="完成提号结算" width="480px">
<!-- 完成提号 -->
<el-dialog v-model="completeDialogVisible" title="完成提号" width="480px">
<el-form label-width="110px">
<el-form-item label="结算金额(元)" required>
<el-input-number
@@ -611,7 +680,13 @@ function listingOptionLabel(item: AvailableListing) {
controls-position="right"
style="width: 100%"
/>
<div class="form-hint">给卖家钱包增加的可用余额</div>
<div class="form-hint">
{{
activePickup?.settlement_mode === 'platform_managed'
? '记录待线下支付的结算金额'
: '给卖家钱包增加的可用余额'
}}
</div>
</el-form-item>
<el-form-item label="线下利润(元)">
<el-input-number
@@ -629,16 +704,16 @@ function listingOptionLabel(item: AvailableListing) {
v-model="completeForm.complete_remark"
type="textarea"
:rows="3"
placeholder="结算说明(可选)"
placeholder="完成说明(可选)"
/>
</el-form-item>
<el-alert type="warning" :closable="false">
完成后提号记录置为"已完成"卖家钱包立即入账账号下架不可再上架操作不可撤销
完成后账号下架不可再上架站内上传会立即入账卖家钱包平台代管会转为待线下结算实际打款后需单独确认
</el-alert>
</el-form>
<template #footer>
<el-button @click="completeDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleComplete">确认结算</el-button>
<el-button type="primary" :loading="loading" @click="handleComplete">确认完成</el-button>
</template>
</el-dialog>