Files
order_site/apps/frontend/src/views/admin/AdminOrdersView.vue
T
2026-04-23 14:46:06 +08:00

672 lines
16 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, ref } from 'vue'
import AdminPaginationBar from '@/components/admin/AdminPaginationBar.vue'
import AdminStatusTag from '@/components/admin/AdminStatusTag.vue'
import { fetchAdminOrders } from '@/services/admin'
import type { AdminOrderListItem, AdminPagination } from '@/types/admin'
import { adminPayStatusOptions } from '@/utils/admin-options'
import { formatAdminDateTime } from '@/utils/admin-time'
const loading = ref(true)
const errorMessage = ref('')
const platformOrderId = ref('')
const payStatus = ref('')
const skuCode = ref('')
const dateFrom = ref('')
const dateTo = ref('')
const items = ref<AdminOrderListItem[]>([])
const pagination = ref<AdminPagination>({
page: 1,
pageSize: 20,
total: 0,
})
const summary = computed(() => ({
paidCount: items.value.filter((item) => item.payStatus === 'paid').length,
refundedCount: items.value.filter((item) => item.payStatus === 'refunded').length,
bindingReadyCount: items.value.filter((item) => item.completedBindingTaskCount > 0).length,
}))
async function loadOrders(page = pagination.value.page) {
loading.value = true
errorMessage.value = ''
try {
const response = await fetchAdminOrders({
page,
pageSize: pagination.value.pageSize,
platformOrderId: platformOrderId.value.trim(),
payStatus: payStatus.value.trim(),
skuCode: skuCode.value.trim(),
dateFrom: dateFrom.value.trim(),
dateTo: dateTo.value.trim(),
})
items.value = response.data.items
pagination.value = response.data.pagination
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '读取订单列表失败'
} finally {
loading.value = false
}
}
function resetFilters() {
platformOrderId.value = ''
payStatus.value = ''
skuCode.value = ''
dateFrom.value = ''
dateTo.value = ''
void loadOrders(1)
}
function resolveBuyerLabel(item: AdminOrderListItem) {
return item.buyerName || item.buyerId || '未识别'
}
function resolveContactLabel(item: AdminOrderListItem) {
return item.receiverContact || '未提供'
}
function resolveShopLabel(item: AdminOrderListItem) {
return item.shopName || item.shopId || '未识别店铺'
}
function resolveItemSummary(item: AdminOrderListItem) {
return item.itemSummary || '未识别商品'
}
function resolveBindingTone(completedCount: number, totalCount: number) {
if (completedCount <= 0) {
return 'warning'
}
return completedCount === totalCount ? 'success' : 'primary'
}
function resolveBindingSummary(item: AdminOrderListItem) {
return `共 ${item.totalBindingCount} 条 · 预占 ${item.reservedBindingCount} · 已消耗 ${item.consumedBindingCount} · 已释放 ${item.releasedBindingCount}`
}
onMounted(loadOrders)
</script>
<template>
<section class="admin-panel">
<header class="panel-header">
<div class="panel-header-copy">
<h1>订单列表</h1>
<p>查看平台订单支付状态商品摘要和对应任务推进情况</p>
</div>
<div class="panel-header-badge"> {{ pagination.total }} 条订单</div>
</header>
<section class="filters-card">
<div class="filters-grid">
<label class="filter-field">
<span>平台订单号</span>
<input v-model="platformOrderId" class="text-input" placeholder="支持模糊匹配" />
</label>
<label class="filter-field">
<span>支付状态</span>
<select v-model="payStatus" class="text-input select-input">
<option v-for="option in adminPayStatusOptions" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</label>
<label class="filter-field">
<span>SKU</span>
<input v-model="skuCode" class="text-input" placeholder="如 dnf-cdk-a" />
</label>
<label class="filter-field">
<span>开始日期</span>
<input v-model="dateFrom" class="text-input" type="date" />
</label>
<label class="filter-field">
<span>结束日期</span>
<input v-model="dateTo" class="text-input" type="date" />
</label>
</div>
<div class="filters-actions">
<span class="filters-hint">订单信息支付状态绑定完成度和金额时间已拆成独立信息块更方便排查</span>
<div class="filters-button-row">
<el-button round @click="resetFilters">重置</el-button>
<el-button round type="primary" @click="() => loadOrders(1)">查询</el-button>
</div>
</div>
</section>
<section v-if="!loading && items.length > 0" class="summary-grid">
<article class="summary-card summary-card-success">
<span class="summary-label">当前页已支付</span>
<strong class="summary-value">{{ summary.paidCount }}</strong>
<span class="summary-meta">已完成付款可继续跟进交付</span>
</article>
<article class="summary-card summary-card-danger">
<span class="summary-label">当前页已退款</span>
<strong class="summary-value">{{ summary.refundedCount }}</strong>
<span class="summary-meta">需要重点确认订单状态和任务收口</span>
</article>
<article class="summary-card summary-card-primary">
<span class="summary-label">当前页有完整绑定</span>
<strong class="summary-value">{{ summary.bindingReadyCount }}</strong>
<span class="summary-meta">至少有 1 个任务已完成完整绑定</span>
</article>
</section>
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
<div v-if="loading" class="empty-block">订单列表加载中</div>
<div v-else-if="items.length === 0" class="empty-block">当前筛选条件下暂无订单</div>
<div v-else class="orders-shell">
<div class="orders-toolbar">
<div>
<strong>订单明细</strong>
<span> {{ pagination.page }} 当前展示 {{ items.length }} </span>
</div>
</div>
<div class="order-list">
<article v-for="item in items" :key="item.orderId" class="order-card">
<div class="order-card-top">
<div class="order-identity">
<RouterLink :to="`/admin/orders/${item.orderId}`" class="cell-link order-no" :title="item.platformOrderId">
{{ item.platformOrderId }}
</RouterLink>
<div class="order-meta-row">
<span class="compact-chip">{{ item.platform || '-' }}</span>
<span class="compact-chip">{{ resolveShopLabel(item) }}</span>
<span class="compact-chip compact-chip--muted">买家 {{ resolveBuyerLabel(item) }}</span>
</div>
</div>
<AdminStatusTag :status="item.payStatus" />
</div>
<div class="order-grid">
<section class="order-section">
<span class="section-label">订单信息</span>
<div class="info-cell">
<span class="info-title">{{ resolveBuyerLabel(item) }}</span>
<span class="info-subline">联系方式 {{ resolveContactLabel(item) }}</span>
<span class="info-subline">店铺 {{ resolveShopLabel(item) }}</span>
<span class="info-subline">更新时间 {{ formatAdminDateTime(item.updatedAt) }}</span>
</div>
</section>
<section class="order-section">
<span class="section-label">商品摘要</span>
<div class="info-cell">
<span class="info-title" :title="resolveItemSummary(item)">{{ resolveItemSummary(item) }}</span>
<span class="info-subline">{{ item.itemCount }} 合计 {{ item.totalQuantity }} </span>
<span class="info-subline" :title="resolveBindingSummary(item)">{{ resolveBindingSummary(item) }}</span>
</div>
</section>
<section class="order-section">
<span class="section-label">订单进度</span>
<div class="status-stack">
<AdminStatusTag :status="item.payStatus" />
<AdminStatusTag :status="item.orderStatus" />
<AdminStatusTag :status="item.systemBindingStatus" />
<AdminStatusTag :status="item.userBindingStatus" />
</div>
</section>
<section class="order-section">
<span class="section-label">金额 / 时间</span>
<div class="info-cell">
<span class="info-title amount-text">{{ item.totalAmount }} {{ item.currency }}</span>
<span class="info-subline">支付 {{ formatAdminDateTime(item.paidAt) }}</span>
<span class="info-subline">创建 {{ formatAdminDateTime(item.createdAt) }}</span>
</div>
</section>
<section class="order-section">
<span class="section-label">任务进度</span>
<div class="task-progress-inline">
<span class="progress-chip" :data-tone="resolveBindingTone(item.completedBindingTaskCount, item.taskCount)">
完整绑定 {{ item.completedBindingTaskCount }}/{{ item.taskCount }}
</span>
<span class="progress-chip" :data-tone="resolveBindingTone(item.systemBoundTaskCount, item.taskCount)">
系统绑定 {{ item.systemBoundTaskCount }}/{{ item.taskCount }}
</span>
</div>
</section>
</div>
</article>
</div>
<AdminPaginationBar
:page="pagination.page"
:page-size="pagination.pageSize"
:total="pagination.total"
:loading="loading"
@change="loadOrders"
/>
</div>
</section>
</template>
<style scoped>
.admin-panel {
display: grid;
gap: 14px;
min-width: 0;
}
.panel-header {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
}
.panel-header-copy {
min-width: 0;
}
.panel-header h1 {
margin: 0;
font-size: 34px;
line-height: 1.05;
letter-spacing: -0.04em;
color: #1d3555;
}
.panel-header p {
margin: 6px 0 0;
font-size: 14px;
color: #64748b;
}
.panel-header-badge {
flex: 0 0 auto;
min-height: 34px;
padding: 0 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(86, 108, 138, 0.12);
color: #38506e;
font-size: 12px;
font-weight: 700;
display: inline-flex;
align-items: center;
}
.filters-card,
.summary-card,
.orders-shell,
.empty-block,
.error-copy {
padding: 16px 18px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.94);
border: 1px solid rgba(86, 108, 138, 0.1);
box-shadow: 0 14px 36px rgba(30, 49, 78, 0.06);
}
.filters-card {
display: grid;
gap: 14px;
}
.filters-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
}
.filter-field {
display: grid;
gap: 6px;
min-width: 0;
}
.filter-field span {
color: #5d6f87;
font-size: 12px;
font-weight: 600;
}
.filters-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.filters-hint {
color: #64748b;
font-size: 12px;
}
.filters-button-row {
display: flex;
gap: 10px;
flex-shrink: 0;
}
.text-input {
width: 100%;
min-height: 38px;
padding: 0 12px;
border-radius: 12px;
border: 1px solid rgba(86, 108, 138, 0.18);
background: rgba(255, 255, 255, 0.94);
color: #24364e;
font-size: 13px;
}
.select-input {
appearance: none;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.summary-card {
display: grid;
gap: 6px;
position: relative;
overflow: hidden;
}
.summary-card::after {
content: '';
position: absolute;
inset: auto -26px -26px auto;
width: 96px;
height: 96px;
border-radius: 999px;
opacity: 0.14;
}
.summary-card-success::after {
background: #12b76a;
}
.summary-card-danger::after {
background: #f04438;
}
.summary-card-primary::after {
background: #2563eb;
}
.summary-label {
color: #51647d;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
}
.summary-value {
font-size: 34px;
line-height: 1;
color: #1d3555;
}
.summary-meta {
color: #70829a;
font-size: 12px;
}
.error-copy {
margin: 0;
color: #b42318;
}
.empty-block {
color: #64748b;
}
.orders-shell {
display: grid;
gap: 14px;
min-width: 0;
}
.orders-toolbar strong {
display: block;
color: #1f324a;
font-size: 14px;
}
.orders-toolbar span {
display: block;
margin-top: 4px;
color: #64748b;
font-size: 12px;
}
.order-list {
display: grid;
gap: 12px;
}
.order-card {
display: grid;
gap: 14px;
padding: 14px;
border-radius: 16px;
background: linear-gradient(180deg, rgba(248, 251, 255, 0.98) 0%, #ffffff 100%);
border: 1px solid rgba(86, 108, 138, 0.12);
}
.order-card-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.order-identity {
min-width: 0;
display: grid;
gap: 8px;
}
.cell-link {
color: inherit;
text-decoration: none;
}
.cell-link:hover {
color: #175cd3;
}
.order-no {
display: inline-block;
max-width: 100%;
font-size: 20px;
line-height: 1.1;
font-weight: 800;
color: #132238;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-meta-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.compact-chip {
display: inline-flex;
align-items: center;
min-height: 28px;
max-width: 100%;
padding: 0 10px;
border-radius: 999px;
background: #f4f7fb;
border: 1px solid rgba(86, 108, 138, 0.1);
color: #44556c;
font-size: 12px;
font-weight: 600;
}
.compact-chip--muted {
color: #6b7c93;
}
.order-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(205px, 1fr));
gap: 12px;
min-width: 0;
}
.order-section {
display: grid;
gap: 8px;
min-width: 0;
padding: 12px;
border-radius: 14px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(86, 108, 138, 0.08);
}
.section-label {
color: #71839b;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.info-cell {
display: grid;
gap: 5px;
min-width: 0;
}
.info-title {
display: block;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #101828;
font-weight: 700;
}
.info-subline {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #475467;
font-size: 12px;
}
.amount-text {
white-space: nowrap;
}
.status-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.task-progress-inline {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.progress-chip {
display: inline-flex;
align-items: center;
min-height: 28px;
padding: 0 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
border: 1px solid transparent;
white-space: nowrap;
}
.progress-chip[data-tone='success'] {
color: #0f766e;
background: #ecfdf3;
border-color: #a6f4c5;
}
.progress-chip[data-tone='primary'] {
color: #175cd3;
background: #eff8ff;
border-color: #b2ddff;
}
.progress-chip[data-tone='warning'] {
color: #b54708;
background: #fffaeb;
border-color: #fedf89;
}
@media (max-width: 1400px) {
.filters-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 1180px) {
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 980px) {
.panel-header {
flex-direction: column;
align-items: flex-start;
}
.filters-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.filters-actions {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 720px) {
.panel-header h1 {
font-size: 28px;
}
.filters-grid,
.summary-grid {
grid-template-columns: 1fr;
}
.filters-button-row {
width: 100%;
}
.filters-button-row :deep(.el-button) {
flex: 1 1 auto;
}
.order-card-top {
flex-direction: column;
align-items: flex-start;
}
.order-no {
font-size: 18px;
}
}
</style>