feat(pickup): 新增管理员线下提号功能
- 新建 admin_pickups 表独立于 rental_orders,不污染订单状态机与财务统计口径 - 提号中/已完成/已取消三态:创建锁定账号,完成时给卖家钱包加可用余额并下架 listing,取消解锁账号 - 完成时 listing 置 completed,复用现有 listingLockedForOwnerMutation 拦截再上架 - 管理端提号管理页(列表/创建/完成/取消/可提号账号搜索)+ 卖家端提号记录页 - 财务仪表盘新增线下提号独立统计块,与正常订单口径分离 - 钱包流水 label、状态标签、菜单入口同步补齐 - 优化卖家服务悬浮菜单为一行四个,尺寸与配色对齐买家服务
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search } from '@element-plus/icons-vue'
|
||||
import {
|
||||
cancelAdminPickup,
|
||||
completeAdminPickup,
|
||||
createAdminPickup,
|
||||
fetchAdminPickups,
|
||||
fetchAvailableListings,
|
||||
type AdminPickup,
|
||||
type AvailableListing,
|
||||
} from '@/features/admin/api/adminPickup'
|
||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { pickupStatusLabel } from '@/shared/utils/statusLabels'
|
||||
|
||||
const loading = ref(false)
|
||||
const items = ref<AdminPickup[]>([])
|
||||
const total = ref(0)
|
||||
const filters = reactive({ page: 1, page_size: 20, keyword: '', status: '' })
|
||||
|
||||
const createDialogVisible = ref(false)
|
||||
const completeDialogVisible = ref(false)
|
||||
const activePickupId = ref(0)
|
||||
|
||||
const createForm = reactive({
|
||||
listing_id: null as number | null,
|
||||
platform: '',
|
||||
remark: '',
|
||||
})
|
||||
const completeForm = reactive({ settle_amount: 0, complete_remark: '' })
|
||||
|
||||
const listingOptions = ref<AvailableListing[]>([])
|
||||
const listingLoading = ref(false)
|
||||
|
||||
onMounted(loadList)
|
||||
|
||||
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 handlePageChange(p: number) {
|
||||
filters.page = p
|
||||
loadList()
|
||||
}
|
||||
function handleSizeChange(s: number) {
|
||||
filters.page_size = s
|
||||
filters.page = 1
|
||||
loadList()
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
createForm.listing_id = null
|
||||
createForm.platform = ''
|
||||
createForm.remark = ''
|
||||
listingOptions.value = []
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function searchListings(keyword: string) {
|
||||
if (!keyword) {
|
||||
listingOptions.value = []
|
||||
return
|
||||
}
|
||||
listingLoading.value = true
|
||||
try {
|
||||
const result = await fetchAvailableListings(keyword)
|
||||
listingOptions.value = result.items
|
||||
} finally {
|
||||
listingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
if (!createForm.listing_id) {
|
||||
ElMessage.warning('请选择要提号的账号')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
await createAdminPickup({
|
||||
listing_id: createForm.listing_id,
|
||||
platform: createForm.platform,
|
||||
remark: createForm.remark,
|
||||
})
|
||||
ElMessage.success('提号订单已创建')
|
||||
createDialogVisible.value = false
|
||||
loadList()
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(errorMessage(e) || '创建失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openCompleteDialog(id: number) {
|
||||
activePickupId.value = id
|
||||
completeForm.settle_amount = 0
|
||||
completeForm.complete_remark = ''
|
||||
completeDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function handleComplete() {
|
||||
if (completeForm.settle_amount <= 0) {
|
||||
ElMessage.warning('请输入结算金额')
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
await completeAdminPickup(activePickupId.value, {
|
||||
settle_amount_cent: Math.round(completeForm.settle_amount * 100),
|
||||
complete_remark: completeForm.complete_remark,
|
||||
})
|
||||
ElMessage.success('提号已完成,已给卖家结算')
|
||||
completeDialogVisible.value = false
|
||||
loadList()
|
||||
} catch (e: unknown) {
|
||||
ElMessage.error(errorMessage(e) || '完成失败')
|
||||
} finally {
|
||||
loading.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 ''
|
||||
}
|
||||
</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-button type="primary" :icon="Plus" @click="openCreateDialog">创建提号</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar">
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="提号编号 / 上架编号 / 账号标题"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-select v-model="filters.status" placeholder="状态" clearable style="width: 130px">
|
||||
<el-option label="提号中" value="picking_up" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="已取消" value="cancelled" />
|
||||
</el-select>
|
||||
<el-button type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button :icon="Refresh" @click="loadList">刷新</el-button>
|
||||
</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 label="交易渠道" width="110">
|
||||
<template #default="{ row }">{{ row.platform || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算金额" width="130">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.status === 'completed'">{{ formatCentWithSymbol(row.settle_amount_cent) }}</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="170">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'picking_up'"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="openCompleteDialog(row.id)"
|
||||
>
|
||||
完成结算
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'picking_up'"
|
||||
type="danger"
|
||||
size="small"
|
||||
plain
|
||||
@click="handleCancel(row.id)"
|
||||
>
|
||||
取消
|
||||
</el-button>
|
||||
<span v-if="row.status !== 'picking_up'" class="text-muted">—</span>
|
||||
</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="520px">
|
||||
<el-form label-width="110px">
|
||||
<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="`${item.listing_no} · ${item.account_title}`"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<div class="form-hint">仅显示已发布、已审核、未在交易中的账号</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="交易渠道">
|
||||
<el-input v-model="createForm.platform" placeholder="如 微信 / QQ / 闲鱼" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="createForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="线下交易说明(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-alert type="info" :closable="false">
|
||||
创建后账号将被锁定为"提号中",卖家收到通知;不会立即结算,需手动完成结算。
|
||||
</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>
|
||||
|
||||
<!-- 完成结算 -->
|
||||
<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">给卖家钱包增加的可用余额</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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-hint {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
.text-muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user