Files
hfb_sys/frontend/src/features/admin/views/AdminPickupView.vue
T

1221 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Download, EditPen, Plus, Refresh, Search, Setting, Shop } from '@element-plus/icons-vue'
import {
cancelAdminPickup,
confirmAdminPickupOfflineSettlement,
completeAdminPickup,
createAdminPickupFinancialAdjustment,
createAdminPickup,
exportAdminPickups,
fetchAdminPickups,
fetchAdminPickupShopOptions,
fetchAvailableListings,
updateAdminPickupProfit,
updateAdminPickupThirdPartyOrderNo,
type AdminPickup,
type AvailableListing,
type AvailableListingSourceType,
type PickupAccountSource,
type PickupOfflineSettlementStatus,
} from '@/features/admin/api/adminPickup'
import PickupShopDialog from '@/features/admin/components/PickupShopDialog.vue'
import { useAdminSessionStore } from '@/stores/adminSession'
import { centToYuan, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
import { formatDateTime } from '@/shared/utils/time'
import { pickupStatusLabel } from '@/shared/utils/statusLabels'
import { adminPath } from '@/shared/utils/adminPath'
const adminSession = useAdminSessionStore()
const currentAdminName = computed(() => adminSession.nickname || adminSession.username || '')
const loading = ref(false)
const exporting = ref(false)
const items = ref<AdminPickup[]>([])
const total = ref(0)
const filterTimeRange = ref<[string, string] | null>(null)
const filters = reactive({
page: 1,
page_size: 20,
keyword: '',
status: '',
shop_name: '',
account_source: '' as PickupAccountSource | '',
offline_settlement_status: '' as PickupOfflineSettlementStatus | '',
start_time: '',
end_time: '',
})
const hasActiveFilters = computed(
() =>
Boolean(filters.keyword.trim()) ||
Boolean(filters.status) ||
Boolean(filters.shop_name) ||
Boolean(filters.account_source) ||
Boolean(filters.offline_settlement_status) ||
Boolean(filters.start_time) ||
Boolean(filters.end_time)
)
const createDialogVisible = ref(false)
const completeDialogVisible = ref(false)
const profitDialogVisible = ref(false)
const financialAdjustmentDialogVisible = ref(false)
const thirdPartyOrderNoDialogVisible = ref(false)
const shopMgrVisible = ref(false)
const activePickupId = ref(0)
const activePickup = ref<AdminPickup | null>(null)
const profitSaving = ref(false)
const financialAdjustmentSaving = ref(false)
const thirdPartyOrderNoSaving = ref(false)
const thirdPartyOrderNoForm = reactive({ third_party_order_no: '' })
const createForm = reactive({
listing_id: null as number | null,
operator: '',
platform: '',
third_party_order_no: '',
shop_name: '',
order_amount: 0,
deposit_amount: 0,
profit_amount: 0,
remark: '',
})
const completeForm = reactive({ settle_amount: 0, profit_amount: 0, complete_remark: '' })
const profitForm = reactive({ profit_amount: 0, reason: '' })
const financialAdjustmentForm = reactive({ profit_amount: 0, settle_amount: 0, reason: '' })
const listingOptions = ref<AvailableListing[]>([])
const listingLoading = ref(false)
const listingKeyword = ref('')
const listingSourceType = ref<AvailableListingSourceType>('')
let listingRequestID = 0
const shopOptions = ref<string[]>([])
const shopOptionsLoading = ref(false)
const selectedListing = computed(
() => listingOptions.value.find(item => item.id === createForm.listing_id) || null
)
const selectedPriceSnapshot = computed(() => {
const item = selectedListing.value
if (!item) return null
const totalCent = normalizeCent(item.owner_total_price_cent || item.owner_price_cent)
let pureCent = normalizeCent(item.owner_pure_price_cent)
let extraCent = normalizeCent(item.owner_extra_item_price_cent)
if (pureCent <= 0 && extraCent <= 0) {
pureCent = totalCent
} else if (pureCent <= 0) {
pureCent = Math.max(totalCent - extraCent, 0)
} else if (extraCent <= 0) {
extraCent = Math.max(totalCent - pureCent, 0)
}
return { pureCent, extraCent, totalCent }
})
onMounted(() => {
loadList()
loadShopOptions()
})
async function loadList() {
loading.value = true
try {
const result = await fetchAdminPickups(filters)
items.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
function handleSearch() {
filters.page = 1
loadList()
}
function handleTimeRangeChange(val: [string, string] | null) {
if (val && val.length === 2) {
filters.start_time = val[0]
filters.end_time = val[1]
} else {
filters.start_time = ''
filters.end_time = ''
}
handleSearch()
}
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.shop_name = ''
filters.account_source = ''
filters.offline_settlement_status = ''
filters.start_time = ''
filters.end_time = ''
filterTimeRange.value = null
filters.page = 1
loadList()
}
function handlePageChange(p: number) {
filters.page = p
loadList()
}
function handleSizeChange(s: number) {
filters.page_size = s
filters.page = 1
loadList()
}
async function handleExportCommand(command: string) {
const exportAll = command === 'all'
if (!exportAll && hasActiveFilters.value && total.value === 0) {
ElMessage.warning('当前筛选条件下没有可导出的数据')
return
}
exporting.value = true
try {
const result = await exportAdminPickups(
exportAll
? {}
: {
keyword: filters.keyword.trim(),
status: filters.status,
shop_name: filters.shop_name,
account_source: filters.account_source,
offline_settlement_status: filters.offline_settlement_status,
start_time: filters.start_time,
end_time: filters.end_time,
}
)
downloadBlob(result.blob, result.filename)
ElMessage.success(result.rowCount > 0 ? `已导出 ${result.rowCount} 条提号记录` : '导出成功')
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '导出失败')
} finally {
exporting.value = false
}
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
function openCreateDialog() {
createForm.listing_id = null
createForm.operator = currentAdminName.value || ''
createForm.platform = ''
createForm.third_party_order_no = ''
createForm.shop_name = ''
createForm.order_amount = 0
createForm.deposit_amount = 0
createForm.profit_amount = 0
createForm.remark = ''
listingOptions.value = []
listingKeyword.value = ''
listingSourceType.value = ''
createDialogVisible.value = true
void searchListings('')
}
async function loadShopOptions() {
shopOptionsLoading.value = true
try {
shopOptions.value = await fetchAdminPickupShopOptions()
} finally {
shopOptionsLoading.value = false
}
}
async function searchListings(keyword: string) {
listingKeyword.value = keyword.trim()
const requestID = ++listingRequestID
listingLoading.value = true
try {
const result = await fetchAvailableListings(
listingKeyword.value,
1,
20,
listingSourceType.value
)
if (requestID === listingRequestID) {
listingOptions.value = result.items
}
} finally {
if (requestID === listingRequestID) {
listingLoading.value = false
}
}
}
function handleListingSourceChange() {
createForm.listing_id = null
listingOptions.value = []
void searchListings(listingKeyword.value)
}
async function handleCreate() {
if (!createForm.listing_id) {
ElMessage.warning('请选择要提号的账号')
return
}
if (createForm.order_amount < 0) {
ElMessage.warning('下单金额不能为负数')
return
}
if (createForm.deposit_amount < 0) {
ElMessage.warning('押金金额不能为负数')
return
}
if (createForm.profit_amount < 0) {
ElMessage.warning('线下利润不能为负数')
return
}
loading.value = true
try {
await createAdminPickup({
listing_id: createForm.listing_id,
operator: createForm.operator.trim(),
platform: createForm.platform.trim(),
third_party_order_no: createForm.third_party_order_no.trim(),
shop_name: createForm.shop_name.trim(),
order_amount_cent: yuanToCent(createForm.order_amount),
deposit_amount_cent: yuanToCent(createForm.deposit_amount),
profit_amount_cent: yuanToCent(createForm.profit_amount),
remark: createForm.remark.trim(),
})
ElMessage.success('提号订单已创建')
createDialogVisible.value = false
loadShopOptions()
loadList()
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '创建失败')
} finally {
loading.value = false
}
}
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(effectiveSettleCent(row))}?`,
'确认线下结算',
{
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 openThirdPartyOrderNoDialog(row: AdminPickup) {
activePickupId.value = row.id
activePickup.value = row
thirdPartyOrderNoForm.third_party_order_no = row.third_party_order_no || ''
thirdPartyOrderNoDialogVisible.value = true
}
async function handleSaveThirdPartyOrderNo() {
const orderNo = thirdPartyOrderNoForm.third_party_order_no.trim()
if (!orderNo) {
ElMessage.warning('请输入第三方单号')
return
}
if (!activePickupId.value) return
thirdPartyOrderNoSaving.value = true
try {
const updated = await updateAdminPickupThirdPartyOrderNo(activePickupId.value, {
third_party_order_no: orderNo,
})
ElMessage.success('第三方单号已更新')
thirdPartyOrderNoDialogVisible.value = false
if (activePickup.value) {
activePickup.value.third_party_order_no = updated.third_party_order_no
}
const target = items.value.find(item => item.id === activePickupId.value)
if (target) {
target.third_party_order_no = updated.third_party_order_no
}
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '保存失败')
} finally {
thirdPartyOrderNoSaving.value = false
}
}
function openProfitDialog(row: AdminPickup) {
activePickupId.value = row.id
profitForm.profit_amount = centToYuan(row.profit_amount_cent)
profitForm.reason = ''
profitDialogVisible.value = true
}
function openFinancialAdjustmentDialog(row: AdminPickup) {
activePickupId.value = row.id
activePickup.value = row
financialAdjustmentForm.profit_amount = centToYuan(effectiveProfitCent(row))
financialAdjustmentForm.settle_amount = centToYuan(effectiveSettleCent(row))
financialAdjustmentForm.reason = ''
financialAdjustmentDialogVisible.value = true
}
async function handleFinancialAdjustment() {
if (!financialAdjustmentForm.reason.trim()) {
ElMessage.warning('请输入调整原因')
return
}
if (financialAdjustmentForm.profit_amount < 0 || financialAdjustmentForm.settle_amount < 0) {
ElMessage.warning('金额不能小于 0')
return
}
financialAdjustmentSaving.value = true
try {
await createAdminPickupFinancialAdjustment(activePickupId.value, {
profit_amount_cent: yuanToCent(financialAdjustmentForm.profit_amount),
settle_amount_cent: yuanToCent(financialAdjustmentForm.settle_amount),
reason: financialAdjustmentForm.reason.trim(),
})
ElMessage.success(
activePickup.value?.settlement_mode === 'platform_managed'
? '财务调整已创建,请确认线下补款或追回'
: '财务调整已创建'
)
financialAdjustmentDialogVisible.value = false
loadList()
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '调整失败')
} finally {
financialAdjustmentSaving.value = false
}
}
async function handleComplete() {
if (completeForm.settle_amount <= 0) {
ElMessage.warning('请输入结算金额')
return
}
if (completeForm.profit_amount < 0) {
ElMessage.warning('线下利润不能为负数')
return
}
loading.value = true
try {
await completeAdminPickup(activePickupId.value, {
settle_amount_cent: yuanToCent(completeForm.settle_amount),
profit_amount_cent: yuanToCent(completeForm.profit_amount),
complete_remark: completeForm.complete_remark,
})
ElMessage.success(
activePickup.value?.settlement_mode === 'platform_managed'
? '提号已完成,待线下结算'
: '提号已完成,已给卖家结算'
)
completeDialogVisible.value = false
loadList()
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '完成失败')
} finally {
loading.value = false
}
}
async function handleUpdateProfit() {
if (profitForm.profit_amount < 0) {
ElMessage.warning('线下利润不能为负数')
return
}
profitSaving.value = true
try {
await updateAdminPickupProfit(activePickupId.value, {
profit_amount_cent: yuanToCent(profitForm.profit_amount),
reason: profitForm.reason,
})
ElMessage.success('线下利润已更新')
profitDialogVisible.value = false
loadList()
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '修改失败')
} finally {
profitSaving.value = false
}
}
async function handleCancel(id: number) {
try {
const { value } = await ElMessageBox.prompt('请输入取消原因', '取消提号', {
confirmButtonText: '确认取消',
cancelButtonText: '返回',
inputPattern: /.+/,
inputErrorMessage: '取消原因不能为空',
})
loading.value = true
try {
await cancelAdminPickup(id, value)
ElMessage.success('提号已取消')
loadList()
} catch (e: unknown) {
ElMessage.error(errorMessage(e) || '取消失败')
} finally {
loading.value = false
}
} catch {
/* 用户放弃 */
}
}
function statusType(status: string) {
if (status === 'picking_up') return 'warning'
if (status === 'completed') return 'success'
if (status === 'cancelled') return 'info'
return ''
}
function errorMessage(e: unknown) {
if (typeof e === 'object' && e !== null && 'message' in e) {
return String((e as { message?: unknown }).message)
}
return ''
}
function ratioText(value: number) {
const ratio = Number(value || 0)
if (ratio <= 0) return '-'
const rounded = Math.round(ratio * 100) / 100
return `1:${Number.isInteger(rounded) ? rounded : rounded.toFixed(2)}`
}
function normalizeCent(value: number | undefined | null) {
const cent = Number(value || 0)
if (!Number.isFinite(cent)) return 0
return Math.max(Math.round(cent), 0)
}
function effectiveProfitCent(row: AdminPickup) {
return Number.isFinite(row.effective_profit_amount_cent)
? row.effective_profit_amount_cent
: row.profit_amount_cent
}
function effectiveSettleCent(row: AdminPickup) {
return Number.isFinite(row.effective_settle_amount_cent)
? row.effective_settle_amount_cent
: row.settle_amount_cent
}
function listingOwnerTotalCent(item: AvailableListing) {
return normalizeCent(item.owner_total_price_cent || item.owner_price_cent)
}
function isExternalSource(source: PickupAccountSource) {
return source === 'external_platform_managed'
}
function pickupSourceLabel(source: PickupAccountSource) {
return isExternalSource(source) ? '外部上传 · 平台代管' : '站内上传'
}
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
}
function listingOptionLabel(item: AvailableListing) {
return `${listingSourceText(item)} · ${item.listing_no} · ${item.account_title} · ${formatCentWithSymbol(listingOwnerTotalCent(item))} / ${formatCentWithSymbol(item.listing_price_cent)}`
}
</script>
<template>
<section class="page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Admin Pickup</p>
<h1>线下提号</h1>
<p>管理员线下提号,站内上传结算到钱包,平台代管通过线下结算确认打款。</p>
</div>
<div class="toolbar-actions">
<el-dropdown
trigger="click"
:disabled="exporting || loading"
@command="handleExportCommand"
>
<el-button :icon="Download" :loading="exporting">导出 CSV</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="filtered">导出筛选结果</el-dropdown-item>
<el-dropdown-item command="all">导出全部</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button :icon="Shop" @click="shopMgrVisible = true">店铺管理</el-button>
<el-button type="primary" :icon="Plus" @click="openCreateDialog">创建提号</el-button>
</div>
</div>
<div class="pickup-filter-bar">
<el-form inline @submit.prevent>
<el-form-item label="关键词" class="pickup-keyword-filter">
<el-input
v-model="filters.keyword"
placeholder="提号编号 / 上架编号 / 账号标题 / 手机号 / 第三方单号 / 操作人"
clearable
@keyup.enter="handleSearch"
@clear="handleSearch"
/>
</el-form-item>
<el-form-item label="提号状态">
<el-select v-model="filters.status" clearable placeholder="全部" @change="handleSearch">
<el-option label="提号中" value="picking_up" />
<el-option label="已完成" value="completed" />
<el-option label="已取消" value="cancelled" />
</el-select>
</el-form-item>
<el-form-item label="店铺">
<el-select
v-model="filters.shop_name"
filterable
clearable
placeholder="全部"
@change="handleSearch"
>
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
</el-select>
</el-form-item>
<el-form-item label="账号来源">
<el-select
v-model="filters.account_source"
clearable
placeholder="全部"
@change="handleSearch"
>
<el-option label="外部上传 · 平台代管" value="external_platform_managed" />
<el-option label="站内上传" value="internal" />
</el-select>
</el-form-item>
<el-form-item label="线下结算">
<el-select
v-model="filters.offline_settlement_status"
clearable
placeholder="全部"
@change="handleSearch"
>
<el-option label="待确认" value="pending" />
<el-option label="已确认" value="settled" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="filterTimeRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
style="width: 350px"
@change="handleTimeRangeChange"
/>
</el-form-item>
<el-form-item class="pickup-filter-actions">
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
<el-button @click="resetFilters">重置</el-button>
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
</el-form-item>
</el-form>
</div>
<el-table v-loading="loading" :data="items" class="table-panel">
<el-table-column prop="pickup_no" label="提号编号" width="190" />
<el-table-column prop="account_title" label="账号" min-width="180" />
<el-table-column label="区服/平台" width="150">
<template #default="{ row }"
>{{ row.server_region || '-' }} / {{ row.login_platform || '-' }}</template
>
</el-table-column>
<el-table-column label="上架编号" width="120">
<template #default="{ row }">{{ row.listing_no }}</template>
</el-table-column>
<el-table-column label="卖家" width="140">
<template #default="{ row }">{{ row.owner_phone }}</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" width="105">
<template #default="{ row }">{{ row.operator || '-' }}</template>
</el-table-column>
<el-table-column label="账号来源" width="180">
<template #default="{ row }">
<div class="source-cell">
<el-tag :type="pickupSourceTagType(row.account_source)" effect="light" size="small">
{{ pickupSourceLabel(row.account_source) }}
</el-tag>
<small v-if="row.source_channel">{{ row.source_channel }}</small>
</div>
</template>
</el-table-column>
<el-table-column label="交易渠道" width="110">
<template #default="{ row }">{{ row.platform || '-' }}</template>
</el-table-column>
<el-table-column label="第三方单号" min-width="150" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.third_party_order_no">{{ row.third_party_order_no }}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="提号备注" min-width="140" show-overflow-tooltip>
<template #default="{ row }">
<span v-if="row.remark">{{ row.remark }}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="店铺名称" min-width="130">
<template #default="{ row }">{{ row.shop_name || '-' }}</template>
</el-table-column>
<el-table-column label="下单金额" width="115">
<template #default="{ row }">
<span v-if="row.order_amount_cent > 0">{{
formatCentWithSymbol(row.order_amount_cent)
}}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="押金金额" width="115">
<template #default="{ row }">
<span v-if="row.deposit_amount_cent > 0">{{
formatCentWithSymbol(row.deposit_amount_cent)
}}</span>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column label="线下利润" width="120">
<template #default="{ row }">{{ formatCentWithSymbol(effectiveProfitCent(row)) }}</template>
</el-table-column>
<el-table-column label="结算金额" width="130">
<template #default="{ row }">
<span v-if="row.status === 'completed'">{{
formatCentWithSymbol(effectiveSettleCent(row))
}}</span>
<span v-else class="text-muted">待结算</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="statusType(row.status)" effect="light">
{{ pickupStatusLabel(row.status) }}
</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="430" fixed="right">
<template #default="{ row }">
<div class="action-buttons">
<RouterLink :to="adminPath(`pickup/${row.id}`)">
<el-button size="small">详情</el-button>
</RouterLink>
<el-button
v-if="row.status !== 'cancelled'"
:type="row.third_party_order_no ? '' : 'warning'"
size="small"
plain
:icon="EditPen"
@click="openThirdPartyOrderNoDialog(row)"
>
{{ row.third_party_order_no ? '改单号' : '补录单号' }}
</el-button>
<el-button
v-if="row.status === 'picking_up'"
type="success"
size="small"
plain
:icon="EditPen"
@click="openProfitDialog(row)"
>
改利润
</el-button>
<el-button
v-if="row.status === 'picking_up'"
type="primary"
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 === 'completed'"
size="small"
plain
:icon="EditPen"
@click="openFinancialAdjustmentDialog(row)"
>
财务调整
</el-button>
<el-button
v-if="row.status === 'picking_up'"
type="danger"
size="small"
plain
@click="handleCancel(row.id)"
>
取消
</el-button>
</div>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<el-pagination
:current-page="filters.page"
:page-size="filters.page_size"
:page-sizes="[20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
<!-- 创建提号 -->
<el-dialog
v-model="createDialogVisible"
title="创建线下提号"
width="min(720px, calc(100vw - 32px))"
>
<el-form label-width="95px" class="create-pickup-form">
<!-- 第 1 行:账号来源 + 操作人(双列网格) -->
<el-row :gutter="16">
<el-col :xs="24" :sm="14">
<el-form-item label="账号来源">
<el-radio-group
v-model="listingSourceType"
size="small"
@change="handleListingSourceChange"
>
<el-radio-button value="">全部</el-radio-button>
<el-radio-button value="external">外部上传 · 平台代管</el-radio-button>
<el-radio-button value="internal">站内上传</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="10">
<el-form-item label="操作人">
<el-input v-model="createForm.operator" placeholder="经办人/客服姓名" clearable />
</el-form-item>
</el-col>
</el-row>
<!-- 第 2 行:选择账号(全宽) -->
<el-form-item label="选择账号" required>
<el-select
v-model="createForm.listing_id"
filterable
remote
clearable
:remote-method="searchListings"
:loading="listingLoading"
placeholder="输入上架编号或账号标题搜索"
style="width: 100%"
>
<el-option
v-for="item in listingOptions"
:key="item.id"
:label="listingOptionLabel(item)"
:value="item.id"
>
<div class="listing-option">
<el-tag
:type="pickupSourceTagType(item.account_source)"
effect="light"
size="small"
>
{{ pickupSourceLabel(item.account_source) }}
</el-tag>
<span class="listing-option-title"
>{{ item.listing_no }} · {{ item.account_title }}</span
>
<span class="listing-option-price">
{{ formatCentWithSymbol(listingOwnerTotalCent(item)) }} /
{{ formatCentWithSymbol(item.listing_price_cent) }}
</span>
</div>
</el-option>
</el-select>
<div class="form-hint">仅显示已发布、已审核、未在交易中的账号</div>
</el-form-item>
<el-form-item v-if="selectedListing" label="来源确认">
<div class="selected-source">
<el-tag :type="pickupSourceTagType(selectedListing.account_source)" effect="light">
{{ pickupSourceLabel(selectedListing.account_source) }}
</el-tag>
<span v-if="selectedListing.source_channel">
来源渠道:{{ selectedListing.source_channel }}
</span>
<span v-if="selectedListing.is_external_upload">
交接:{{
selectedListing.handoff_mode === 'platform'
? '平台代管'
: selectedListing.handoff_mode
}}
</span>
</div>
</el-form-item>
<el-form-item v-if="selectedListing && selectedPriceSnapshot" label="价格快照">
<div class="price-snapshot">
<span>纯比价格 {{ formatCentWithSymbol(selectedPriceSnapshot.pureCent) }}</span>
<span>额外物品价格 {{ formatCentWithSymbol(selectedPriceSnapshot.extraCent) }}</span>
<span>号主总价 {{ formatCentWithSymbol(selectedPriceSnapshot.totalCent) }}</span>
<span>网站售价 {{ formatCentWithSymbol(selectedListing.listing_price_cent) }}</span>
<span>网站加价 {{ formatCentWithSymbol(selectedListing.website_profit_cent) }}</span>
<span>回收 {{ ratioText(selectedListing.seller_ratio) }}</span>
<span>售卖 {{ ratioText(selectedListing.buyer_ratio) }}</span>
</div>
</el-form-item>
<!-- 第 3 行:第三方订单号 + 交易渠道(双列网格) -->
<el-row :gutter="16">
<el-col :xs="24" :sm="12">
<el-form-item label="第三方单号">
<el-input
v-model="createForm.third_party_order_no"
placeholder="淘宝/拼多多/外部订单号"
clearable
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12">
<el-form-item label="交易渠道">
<el-input
v-model="createForm.platform"
placeholder="如 淘宝 / 微信 / QQ / 闲鱼"
clearable
/>
</el-form-item>
</el-col>
</el-row>
<!-- 第 4 行:店铺名称(全宽,带快捷管理) -->
<el-form-item label="店铺名称">
<div style="display: flex; gap: 8px; width: 100%">
<el-select
v-model="createForm.shop_name"
filterable
allow-create
default-first-option
clearable
:loading="shopOptionsLoading"
placeholder="选择配置店铺或输入新店铺"
style="flex: 1"
>
<el-option v-for="name in shopOptions" :key="name" :label="name" :value="name" />
</el-select>
<el-button :icon="Setting" plain @click="shopMgrVisible = true">店铺管理</el-button>
</div>
<div class="form-hint">首次使用的店铺可直接输入名称创建,也可通过「店铺管理」预设</div>
</el-form-item>
<!-- 第 5 行:金额信息(三列并排卡片) -->
<div class="amount-group-card">
<div class="amount-group-title">金额核算</div>
<el-row :gutter="16">
<el-col :xs="24" :sm="8">
<el-form-item label="下单金额(元)" class="amount-item">
<el-input-number
v-model="createForm.order_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="押金金额(元)" class="amount-item">
<el-input-number
v-model="createForm.deposit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="8">
<el-form-item label="线下利润(元)" class="amount-item">
<el-input-number
v-model="createForm.profit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<div class="form-hint" style="margin-top: -6px; margin-bottom: 4px">
电商平台实际成交利润,财务统计按该金额汇总
</div>
</div>
<!-- 第 6 行:备注说明(全宽) -->
<el-form-item label="备注" style="margin-top: 14px">
<el-input
v-model="createForm.remark"
type="textarea"
:rows="2"
placeholder="线下交易说明(可选)"
/>
</el-form-item>
<el-alert type="info" :closable="false" show-icon>
创建后账号将被锁定为"提号中",卖家收到通知;不会立即结算,需手动完成结算。
</el-alert>
</el-form>
<template #footer>
<el-button @click="createDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleCreate">创建提号</el-button>
</template>
</el-dialog>
<!-- 店铺管理弹窗 -->
<PickupShopDialog v-model="shopMgrVisible" @updated="loadShopOptions" />
<!-- 完成提号 -->
<el-dialog v-model="completeDialogVisible" title="完成提号" width="480px">
<el-form label-width="110px">
<el-form-item label="结算金额(元)" required>
<el-input-number
v-model="completeForm.settle_amount"
:min="0.01"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
<div class="form-hint">
{{
activePickup?.settlement_mode === 'platform_managed'
? '记录待线下支付的结算金额'
: '给卖家钱包增加的可用余额'
}}
</div>
</el-form-item>
<el-form-item label="线下利润(元)">
<el-input-number
v-model="completeForm.profit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
<div class="form-hint">可在结算时修正为最终利润,财务统计使用该值</div>
</el-form-item>
<el-form-item label="完成备注">
<el-input
v-model="completeForm.complete_remark"
type="textarea"
:rows="3"
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>
</template>
</el-dialog>
<el-dialog v-model="profitDialogVisible" title="修改线下利润" width="460px">
<el-form label-width="110px">
<el-form-item label="线下利润(元)" required>
<el-input-number
v-model="profitForm.profit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="修改原因">
<el-input
v-model="profitForm.reason"
type="textarea"
:rows="3"
maxlength="255"
show-word-limit
placeholder="可选"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="profitDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="profitSaving" @click="handleUpdateProfit">
保存
</el-button>
</template>
</el-dialog>
<el-dialog v-model="financialAdjustmentDialogVisible" title="完成后财务调整" width="500px">
<el-form label-width="120px">
<el-form-item label="目标利润(元)">
<el-input-number
v-model="financialAdjustmentForm.profit_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="目标打款金额(元)">
<el-input-number
v-model="financialAdjustmentForm.settle_amount"
:min="0"
:step="1"
:precision="2"
controls-position="right"
style="width: 100%"
/>
<div class="form-hint">
{{
activePickup?.settlement_mode === 'platform_managed'
? '代管账号会生成待线下补款或待追回记录'
: '增加金额会立即补入号主钱包,减少金额会生成待追回记录'
}}
</div>
</el-form-item>
<el-form-item label="调整原因" required>
<el-input
v-model="financialAdjustmentForm.reason"
type="textarea"
:rows="3"
maxlength="255"
show-word-limit
placeholder="请输入调整原因"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="financialAdjustmentDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="financialAdjustmentSaving"
@click="handleFinancialAdjustment"
>
确认调整
</el-button>
</template>
</el-dialog>
<!-- 补录/修改第三方单号 -->
<el-dialog
v-model="thirdPartyOrderNoDialogVisible"
:title="activePickup?.third_party_order_no ? '修改第三方单号' : '补录第三方单号'"
width="460px"
>
<el-form :model="thirdPartyOrderNoForm" label-width="110px">
<el-form-item label="提号单号">
<span class="text-muted">{{ activePickup?.pickup_no }}</span>
</el-form-item>
<el-form-item label="当前单号">
<span>{{ activePickup?.third_party_order_no || '(未录入)' }}</span>
</el-form-item>
<el-form-item label="第三方单号" required>
<el-input
v-model="thirdPartyOrderNoForm.third_party_order_no"
placeholder="请输入淘宝/拼多多/外部订单号"
maxlength="128"
show-word-limit
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="thirdPartyOrderNoDialogVisible = false">取消</el-button>
<el-button
type="primary"
:loading="thirdPartyOrderNoSaving"
@click="handleSaveThirdPartyOrderNo"
>
保存
</el-button>
</template>
</el-dialog>
</section>
</template>
<style scoped src="./AdminPickupView.css"></style>