1221 lines
42 KiB
Vue
1221 lines
42 KiB
Vue
<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>
|