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

491 lines
14 KiB
Vue

<script setup lang="ts">
import { ArrowLeft, Refresh, Search } from '@element-plus/icons-vue'
import { computed, onMounted, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchAdminOrders, type AdminOrderQuery, type Order } from '@/features/orders'
import {
orderHandoffStatusLabel,
orderStatusLabel,
settlementStatusLabel,
} from '@/shared/utils/statusLabels'
import { formatDateTime } from '@/shared/utils/time'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import { formatListingNo } from '@/shared/utils/listingDisplay'
import { adminPath } from '@/shared/utils/adminPath'
import { useAdminListState } from '@/features/admin/composables/useAdminListState'
import AdminTablePagination from '../components/AdminTablePagination.vue'
const filters = reactive<AdminOrderQuery>({
keyword: '',
status: '',
handoff_status: '',
handoff_mode: '',
settlement_status: '',
offline_settlement_status: '',
})
const loading = ref(false)
const orders = ref<Order[]>([])
const total = ref(0)
const currentPage = ref(1)
const currentPageSize = ref(20)
const route = useRoute()
const router = useRouter()
const canReturnToWithdrawals = computed(() => route.query.from === 'withdrawals')
const { restoreScroll } = useAdminListState({
key: 'orders',
filters,
currentPage,
pageSize: currentPageSize,
})
const orderStatusOptions = [
{ label: '待支付', value: 'pending_payment' },
{ label: '待交接', value: 'pending_handoff' },
{ label: '使用中', value: 'renting' },
{ label: '逾期中', value: 'overdue' },
{ label: '待号主确认结账', value: 'pending_checkout_confirm' },
{ label: '待租客确认修正', value: 'pending_checkout_accept' },
{ label: '结账争议中', value: 'checkout_disputing' },
{ label: '申诉中', value: 'disputing' },
{ label: '客服介入', value: 'abnormal' },
{ label: '已完成', value: 'completed' },
{ label: '客服已关闭', value: 'closed' },
{ label: '租客已取消', value: 'cancelled' },
] as const
const handoffStatusOptions = [
{ label: '待号主交接', value: 'pending_owner' },
{ label: '号主交接超时', value: 'owner_timeout' },
{ label: '待租客确认', value: 'pending_renter_confirm' },
{ label: '已确认收号', value: 'received' },
{ label: '归还逾期', value: 'return_overdue' },
{ label: '待号主确认结账', value: 'pending_owner_checkout' },
{ label: '待租客确认修正', value: 'pending_renter_checkout' },
{ label: '已归还', value: 'returned' },
{ label: '客服关闭', value: 'admin_closed' },
{ label: '客服标记异常', value: 'admin_abnormal' },
] as const
const handoffModeOptions = [
{ label: '正常订单', value: 'owner' },
{ label: '平台代管', value: 'platform' },
] as const
const settlementStatusOptions = [
{ label: '未结算', value: 'unsettled' },
{ label: '待结算', value: 'pending' },
{ label: '冻结中', value: 'frozen' },
{ label: '已结算', value: 'settled' },
{ label: '已退款', value: 'refunded' },
{ label: '结算已取消', value: 'cancelled' },
{ label: '结算已关闭', value: 'closed' },
{ label: '争议中', value: 'disputed' },
{ label: '已仲裁', value: 'arbitrated' },
] as const
onMounted(async () => {
// 支持从提现审核等页面跳转预填号主/租客搜索
const keyword = route.query.keyword
if (typeof keyword === 'string' && keyword.trim()) {
filters.keyword = keyword.trim()
}
await loadOrders()
restoreScroll()
})
async function loadOrders() {
loading.value = true
try {
const result = await fetchAdminOrders(currentPage.value, currentPageSize.value, {
keyword: filters.keyword?.trim(),
status: filters.status,
handoff_status: filters.handoff_status,
handoff_mode: filters.handoff_mode,
settlement_status: filters.settlement_status,
offline_settlement_status: filters.offline_settlement_status,
})
orders.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
async function handlePageChange() {
await loadOrders()
}
async function queryOrders() {
currentPage.value = 1
await loadOrders()
}
function resetFilters() {
filters.keyword = ''
filters.status = ''
filters.handoff_status = ''
filters.handoff_mode = ''
filters.settlement_status = ''
filters.offline_settlement_status = ''
void queryOrders()
}
function returnToWithdrawals() {
const query: Record<string, string> = {}
const returnQueryKeys = [
'withdrawal_status',
'withdrawal_user_id',
'withdrawal_page',
'withdrawal_page_size',
] as const
for (const key of returnQueryKeys) {
const value = route.query[key]
if (typeof value === 'string' && value) query[key] = value
}
void router.push({ path: adminPath('withdrawals'), query })
}
function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(Number(cent || 0))
return 0
}
function money(value: unknown) {
return formatMoney(Number(value || 0))
}
function userText(value: string | number | undefined) {
return value || '-'
}
function isPlatformManaged(row: Order) {
return row.handoff_mode === 'platform' || row.settlement_mode === 'platform_managed'
}
function ownerActualIncome(row: Order) {
if (row.settlement_status !== 'settled' && row.settlement_status !== 'arbitrated') return null
const value = row.checkout?.owner_income_amount_cent
return typeof value === 'number' ? value : null
}
</script>
<template>
<section class="page admin-orders-page">
<div class="page-header-row">
<div class="page-header">
<p class="eyebrow">Orders</p>
<h1>订单管理</h1>
<p>查看全量订单、交接状态、金额和结算状态。</p>
</div>
<div class="toolbar-actions">
<el-button v-if="canReturnToWithdrawals" :icon="ArrowLeft" @click="returnToWithdrawals">
返回提现审核
</el-button>
<el-button :icon="Refresh" @click="loadOrders">刷新</el-button>
</div>
</div>
<div class="order-filter-bar">
<el-form inline @submit.prevent>
<el-form-item label="关键词">
<el-input
v-model="filters.keyword"
clearable
placeholder="订单号 / 商品编号 / 账号 / 手机号"
style="width: 280px"
@keyup.enter="queryOrders"
@clear="queryOrders"
/>
</el-form-item>
<el-form-item label="订单状态">
<el-select
v-model="filters.status"
clearable
placeholder="全部"
style="width: 150px"
@change="queryOrders"
>
<el-option
v-for="item in orderStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="交接状态">
<el-select
v-model="filters.handoff_status"
clearable
placeholder="全部"
style="width: 170px"
@change="queryOrders"
>
<el-option
v-for="item in handoffStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="订单类型">
<el-select
v-model="filters.handoff_mode"
clearable
placeholder="全部"
style="width: 130px"
@change="queryOrders"
>
<el-option
v-for="item in handoffModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="结算状态">
<el-select
v-model="filters.settlement_status"
clearable
placeholder="全部"
style="width: 140px"
@change="queryOrders"
>
<el-option
v-for="item in settlementStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="线下结算">
<el-select
v-model="filters.offline_settlement_status"
clearable
placeholder="全部"
style="width: 150px"
@change="queryOrders"
>
<el-option label="待确认线下结算" value="pending" />
<el-option label="已确认线下结算" value="settled" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" :icon="Search" :loading="loading" @click="queryOrders"
>查询</el-button
>
<el-button @click="resetFilters">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table
v-loading="loading"
class="table-panel admin-orders-table"
:data="orders"
empty-text="未找到匹配的订单"
>
<el-table-column label="订单信息" min-width="230">
<template #default="{ row }">
<div class="order-main-cell">
<RouterLink class="order-link" :to="adminPath(`orders/${row.id}`)">
{{ row.order_no }}
</RouterLink>
<span>商品编号 {{ formatListingNo(row.listing_no, row.listing_id) }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="账号" min-width="170" show-overflow-tooltip>
<template #default="{ row }">
<div class="stacked-cell">
<strong>{{ row.title }}</strong>
<span>{{ row.server_region }} / {{ row.login_platform }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="用户" min-width="170">
<template #default="{ row }">
<div class="stacked-cell compact">
<span>租客 {{ userText(row.renter_phone || row.renter_id) }}</span>
<span v-if="isPlatformManaged(row)" class="platform-managed-owner">号主 平台代管</span>
<span v-else>号主 {{ userText(row.owner_phone || row.owner_id) }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="状态" width="150">
<template #default="{ row }">
<div class="stacked-cell compact">
<strong>{{ orderStatusLabel(row.status) }}</strong>
<span>{{ orderHandoffStatusLabel(row) }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="结算" width="110">
<template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template>
</el-table-column>
<el-table-column label="金额" width="130" align="right">
<template #default="{ row }">
<div class="stacked-cell money-cell">
<strong>¥{{ money(amountYuan(row.rent_amount_cent)) }}</strong>
<span>押金 ¥{{ money(amountYuan(row.deposit_amount_cent)) }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="号主实际收入" width="130" align="right">
<template #default="{ row }">
<strong v-if="ownerActualIncome(row) !== null" class="owner-income-cell">
¥{{ money(amountYuan(ownerActualIncome(row))) }}
</strong>
<span v-else class="owner-income-empty">-</span>
</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="88" align="center">
<template #default="{ row }">
<RouterLink :to="adminPath(`orders/${row.id}`)">
<el-button size="small">详情</el-button>
</RouterLink>
</template>
</el-table-column>
</el-table>
<AdminTablePagination
v-if="total > 0"
v-model:current-page="currentPage"
v-model:page-size="currentPageSize"
:total="total"
:loading="loading"
@page-change="handlePageChange"
/>
</section>
</template>
<style scoped>
.admin-orders-page {
min-width: 0;
}
.order-filter-bar {
margin-bottom: 16px;
border-radius: 12px;
background: #ffffff;
padding: 16px 18px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.order-filter-bar :deep(.el-form) {
display: flex;
align-items: flex-end;
flex-wrap: wrap;
gap: 0 14px;
}
.order-filter-bar :deep(.el-form-item) {
margin-right: 0;
margin-bottom: 16px;
}
.order-filter-bar :deep(.el-form-item__label) {
color: #6b7785;
font-size: 12px;
font-weight: 600;
}
.admin-orders-table {
--el-table-fixed-right-column: #ffffff;
}
.admin-orders-table :deep(.el-table__inner-wrapper) {
min-width: 0;
}
.admin-orders-table :deep(.el-scrollbar__wrap) {
overflow-x: hidden !important;
}
.admin-orders-table :deep(.el-scrollbar__bar.is-horizontal) {
display: none;
}
.order-main-cell,
.stacked-cell {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
line-height: 1.35;
}
.order-main-cell span,
.stacked-cell span {
overflow: hidden;
color: #8f9bba;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.stacked-cell strong {
overflow: hidden;
color: #1b2559;
font-size: 14px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.stacked-cell.compact {
gap: 2px;
}
.stacked-cell .platform-managed-owner {
color: #d97706;
font-weight: 700;
}
.money-cell {
align-items: flex-end;
}
.owner-income-cell {
color: #d97706;
font-size: 14px;
}
.owner-income-empty {
color: #8f9bba;
}
.order-link {
overflow: hidden;
color: #2f6bff;
font-weight: 700;
text-decoration: none;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-link:hover {
color: #174edb;
}
@media (max-width: 900px) {
.order-filter-bar :deep(.el-input),
.order-filter-bar :deep(.el-select) {
width: 100% !important;
}
.order-filter-bar :deep(.el-form-item) {
width: 100%;
}
}
</style>