Files
hfb_sys/frontend/src/features/admin/views/AdminPickupView.vue
T
yml2213 9ef0843eb0 feat(pickup): 新增管理员线下提号功能
- 新建 admin_pickups 表独立于 rental_orders,不污染订单状态机与财务统计口径
- 提号中/已完成/已取消三态:创建锁定账号,完成时给卖家钱包加可用余额并下架 listing,取消解锁账号
- 完成时 listing 置 completed,复用现有 listingLockedForOwnerMutation 拦截再上架
- 管理端提号管理页(列表/创建/完成/取消/可提号账号搜索)+ 卖家端提号记录页
- 财务仪表盘新增线下提号独立统计块,与正常订单口径分离
- 钱包流水 label、状态标签、菜单入口同步补齐
- 优化卖家服务悬浮菜单为一行四个,尺寸与配色对齐买家服务
2026-07-03 19:04:35 +08:00

359 lines
11 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 { 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>