优化订单列表ui
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { formatStatusWithRaw } from '@/utils/admin-display'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
status: string
|
||||
}>()
|
||||
showRaw?: boolean
|
||||
}>(), {
|
||||
showRaw: true,
|
||||
})
|
||||
|
||||
type ElTagType = 'success' | 'primary' | 'warning' | 'danger' | 'info'
|
||||
|
||||
@@ -85,12 +88,16 @@ function resolveLabel(status: string) {
|
||||
user_binding: '用户绑定中',
|
||||
}
|
||||
|
||||
if (!props.showRaw) {
|
||||
return labelMap[normalized.toLowerCase()] || normalized
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(normalized, labelMap)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-tag :type="resolveType(props.status)" size="small">
|
||||
<el-tag :type="resolveType(props.status)" size="small" :title="props.showRaw ? undefined : props.status">
|
||||
{{ resolveLabel(props.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
|
||||
@@ -13,8 +13,7 @@ const errorMessage = ref('')
|
||||
const platformOrderId = ref('')
|
||||
const payStatus = ref('')
|
||||
const skuCode = ref('')
|
||||
const dateFrom = ref('')
|
||||
const dateTo = ref('')
|
||||
const dateRange = ref<string[]>([])
|
||||
const items = ref<AdminOrderListItem[]>([])
|
||||
const pagination = ref<AdminPagination>({
|
||||
page: 1,
|
||||
@@ -26,6 +25,7 @@ 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,
|
||||
issueCount: items.value.filter((item) => hasOrderIssue(item)).length,
|
||||
}))
|
||||
|
||||
async function loadOrders(page = pagination.value.page) {
|
||||
@@ -39,8 +39,8 @@ async function loadOrders(page = pagination.value.page) {
|
||||
platformOrderId: platformOrderId.value.trim(),
|
||||
payStatus: payStatus.value.trim(),
|
||||
skuCode: skuCode.value.trim(),
|
||||
dateFrom: dateFrom.value.trim(),
|
||||
dateTo: dateTo.value.trim(),
|
||||
dateFrom: dateRange.value[0] || '',
|
||||
dateTo: dateRange.value[1] || '',
|
||||
})
|
||||
items.value = response.data.items
|
||||
pagination.value = response.data.pagination
|
||||
@@ -55,8 +55,7 @@ function resetFilters() {
|
||||
platformOrderId.value = ''
|
||||
payStatus.value = ''
|
||||
skuCode.value = ''
|
||||
dateFrom.value = ''
|
||||
dateTo.value = ''
|
||||
dateRange.value = []
|
||||
void loadOrders(1)
|
||||
}
|
||||
|
||||
@@ -85,6 +84,25 @@ function resolveBindingSummary(item: AdminOrderListItem) {
|
||||
return `共 ${item.totalBindingCount} 条 · 预占 ${item.reservedBindingCount} · 已消耗 ${item.consumedBindingCount} · 已释放 ${item.releasedBindingCount}`
|
||||
}
|
||||
|
||||
function hasOrderIssue(item: AdminOrderListItem) {
|
||||
return ['refunded', 'failed', 'unpaid'].includes(item.payStatus)
|
||||
|| item.systemBindingStatus === 'binding_exception'
|
||||
|| item.userBindingStatus === 'binding_exception'
|
||||
}
|
||||
|
||||
function resolveFollowUpText(item: AdminOrderListItem) {
|
||||
if (item.userBindingStatus === 'binding_exception' || item.systemBindingStatus === 'binding_exception') return '绑定异常'
|
||||
if (item.completedBindingTaskCount <= 0) return '等待完整绑定'
|
||||
if (item.systemBoundTaskCount < item.taskCount) return '系统绑定未完成'
|
||||
return '链路正常'
|
||||
}
|
||||
|
||||
function resolveFollowUpTone(item: AdminOrderListItem) {
|
||||
if (hasOrderIssue(item)) return 'danger'
|
||||
if (item.completedBindingTaskCount <= 0 || item.systemBoundTaskCount < item.taskCount) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
onMounted(loadOrders)
|
||||
</script>
|
||||
|
||||
@@ -104,11 +122,15 @@ onMounted(loadOrders)
|
||||
<el-option v-for="opt in adminPayStatusOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</el-select>
|
||||
<el-input v-model="skuCode" placeholder="SKU,如 dnf-cdk-a" clearable @keyup.enter="loadOrders(1)" />
|
||||
<el-date-picker v-model="dateFrom" type="date" placeholder="开始日期" value-format="YYYY-MM-DD" />
|
||||
<el-date-picker v-model="dateTo" type="date" placeholder="结束日期" value-format="YYYY-MM-DD" />
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<span class="filter-hint">订单信息、支付状态、绑定完成度和金额时间已拆成独立信息块,更方便排查。</span>
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
class="date-range-filter"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
range-separator="至"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<div class="filter-buttons">
|
||||
<el-button round @click="resetFilters">重置</el-button>
|
||||
<el-button round type="primary" @click="loadOrders(1)">查询</el-button>
|
||||
@@ -117,22 +139,12 @@ onMounted(loadOrders)
|
||||
</el-card>
|
||||
|
||||
<!-- 摘要 -->
|
||||
<div v-if="!loading && items.length > 0" class="summary-grid">
|
||||
<el-card shadow="never" class="summary-card summary-card--success">
|
||||
<span class="summary-label">当前页已支付</span>
|
||||
<strong class="summary-value">{{ summary.paidCount }}</strong>
|
||||
<span class="summary-meta">已完成付款,可继续跟进交付</span>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="summary-card summary-card--danger">
|
||||
<span class="summary-label">当前页已退款</span>
|
||||
<strong class="summary-value">{{ summary.refundedCount }}</strong>
|
||||
<span class="summary-meta">需要重点确认订单状态和任务收口</span>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="summary-card summary-card--primary">
|
||||
<span class="summary-label">当前页有完整绑定</span>
|
||||
<strong class="summary-value">{{ summary.bindingReadyCount }}</strong>
|
||||
<span class="summary-meta">至少有 1 个任务已完成完整绑定</span>
|
||||
</el-card>
|
||||
<div v-if="!loading && items.length > 0" class="summary-strip">
|
||||
<span class="summary-title">当前页</span>
|
||||
<span class="summary-metric" data-tone="success">已支付 <strong>{{ summary.paidCount }}</strong></span>
|
||||
<span class="summary-metric" data-tone="danger">已退款 <strong>{{ summary.refundedCount }}</strong></span>
|
||||
<span class="summary-metric" data-tone="primary">完整绑定 <strong>{{ summary.bindingReadyCount }}</strong></span>
|
||||
<span class="summary-metric" :data-tone="summary.issueCount > 0 ? 'warning' : 'muted'">需关注 <strong>{{ summary.issueCount }}</strong></span>
|
||||
</div>
|
||||
|
||||
<el-alert v-if="errorMessage" :title="errorMessage" type="error" show-icon :closable="false" style="margin-top: 16px" />
|
||||
@@ -162,21 +174,23 @@ onMounted(loadOrders)
|
||||
<span class="compact-chip compact-chip--muted">买家 {{ resolveBuyerLabel(item) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<AdminStatusTag :status="item.payStatus" />
|
||||
<div class="order-top-actions">
|
||||
<AdminStatusTag :status="item.payStatus" :show-raw="false" />
|
||||
<RouterLink :to="`/admin/orders/${item.orderId}`" class="detail-link">详情</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="order-grid">
|
||||
<section class="order-section">
|
||||
<div class="order-row-grid">
|
||||
<section class="order-field order-field--buyer">
|
||||
<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">
|
||||
<section class="order-field order-field--item">
|
||||
<span class="section-label">商品摘要</span>
|
||||
<div class="info-cell">
|
||||
<span class="info-title" :title="resolveItemSummary(item)">{{ resolveItemSummary(item) }}</span>
|
||||
@@ -185,26 +199,27 @@ onMounted(loadOrders)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="order-section">
|
||||
<span class="section-label">订单进度</span>
|
||||
<section class="order-field order-field--status">
|
||||
<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" />
|
||||
<AdminStatusTag :status="item.orderStatus" :show-raw="false" />
|
||||
<AdminStatusTag :status="item.systemBindingStatus" :show-raw="false" />
|
||||
<AdminStatusTag :status="item.userBindingStatus" :show-raw="false" />
|
||||
<span class="follow-up-chip" :data-tone="resolveFollowUpTone(item)">{{ resolveFollowUpText(item) }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="order-section">
|
||||
<section class="order-field order-field--amount">
|
||||
<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>
|
||||
<span class="info-subline">更新 {{ formatAdminDateTime(item.updatedAt) }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="order-section">
|
||||
<section class="order-field order-field--tasks">
|
||||
<span class="section-label">任务进度</span>
|
||||
<div class="task-progress-inline">
|
||||
<span class="progress-chip" :data-tone="resolveBindingTone(item.completedBindingTaskCount, item.taskCount)">
|
||||
@@ -253,61 +268,55 @@ onMounted(loadOrders)
|
||||
|
||||
.filter-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: minmax(220px, 1.1fr) minmax(170px, 0.8fr) minmax(220px, 1fr) minmax(280px, 1.2fr) auto;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.filter-hint { color: var(--text-secondary); font-size: var(--text-xs); }
|
||||
.date-range-filter { width: 100%; }
|
||||
.filter-buttons { display: flex; gap: 10px; flex-shrink: 0; }
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: var(--space-3);
|
||||
.summary-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
min-height: 46px;
|
||||
padding: 8px 12px;
|
||||
margin-top: var(--space-4);
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-lg);
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
box-shadow: var(--shadow-sm);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summary-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: auto -26px -26px auto;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: var(--radius-full);
|
||||
opacity: 0.14;
|
||||
}
|
||||
|
||||
.summary-card--success::after { background: var(--color-success); }
|
||||
.summary-card--danger::after { background: var(--color-danger); }
|
||||
.summary-card--primary::after { background: var(--color-primary); }
|
||||
|
||||
.summary-label {
|
||||
.summary-title {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: var(--text-3xl);
|
||||
line-height: 1;
|
||||
color: var(--text-primary);
|
||||
.summary-metric {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid transparent;
|
||||
color: var(--text-secondary);
|
||||
background: #f8fafc;
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.summary-meta { color: var(--text-muted); font-size: var(--text-xs); }
|
||||
.summary-metric strong { color: var(--text-primary); font-size: var(--text-base); line-height: 1; }
|
||||
.summary-metric[data-tone='success'] { color: var(--color-success-dark); background: var(--color-success-light); border-color: #bbf7d0; }
|
||||
.summary-metric[data-tone='danger'] { color: var(--color-danger-dark); background: var(--color-danger-light); border-color: #fecaca; }
|
||||
.summary-metric[data-tone='primary'] { color: var(--color-primary-dark); background: var(--color-primary-light); border-color: #bfdbfe; }
|
||||
.summary-metric[data-tone='warning'] { color: var(--color-warning-dark); background: var(--color-warning-light); border-color: #fed7aa; }
|
||||
.summary-metric[data-tone='muted'] { color: var(--text-secondary); background: #f8fafc; border-color: var(--border-default); }
|
||||
|
||||
.table-card { margin-top: var(--space-4); overflow: visible; }
|
||||
|
||||
@@ -319,16 +328,22 @@ onMounted(loadOrders)
|
||||
|
||||
.order-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: linear-gradient(180deg, rgba(248, 251, 255, 0.98) 0%, #ffffff 100%);
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
border: 1px solid var(--border-default);
|
||||
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||||
}
|
||||
|
||||
.order-card:hover {
|
||||
border-color: var(--border-hover);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.order-card-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
@@ -351,12 +366,35 @@ onMounted(loadOrders)
|
||||
|
||||
.order-meta-row { display: flex; flex-wrap: wrap; gap: var(--space-2); }
|
||||
|
||||
.order-top-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-full);
|
||||
border: 1px solid var(--border-default);
|
||||
color: var(--color-primary-dark);
|
||||
background: var(--color-primary-light);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.detail-link:hover { border-color: var(--border-hover); background: #dbeafe; }
|
||||
|
||||
.compact-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
max-width: 100%;
|
||||
padding: 0 10px;
|
||||
min-height: 24px;
|
||||
max-width: 240px;
|
||||
padding: 0 9px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #f4f7fb;
|
||||
border: 1px solid var(--border-default);
|
||||
@@ -367,21 +405,21 @@ onMounted(loadOrders)
|
||||
|
||||
.compact-chip--muted { color: var(--text-muted); }
|
||||
|
||||
.order-grid {
|
||||
.order-row-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(205px, 1fr));
|
||||
gap: var(--space-3);
|
||||
grid-template-columns: minmax(150px, 0.95fr) minmax(190px, 1.25fr) minmax(230px, 1.35fr) minmax(170px, 1fr) minmax(150px, 0.9fr);
|
||||
column-gap: var(--space-4);
|
||||
row-gap: var(--space-3);
|
||||
min-width: 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.order-section {
|
||||
.order-field {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
border: 1px solid var(--border-default);
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(86, 108, 138, 0.1);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
@@ -398,14 +436,16 @@ onMounted(loadOrders)
|
||||
.amount-text { white-space: nowrap; }
|
||||
|
||||
.status-stack { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
|
||||
.status-stack :deep(.el-tag) { max-width: 100%; }
|
||||
|
||||
.task-progress-inline { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
|
||||
|
||||
.progress-chip {
|
||||
.progress-chip,
|
||||
.follow-up-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
min-height: 26px;
|
||||
padding: 0 9px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
@@ -416,19 +456,30 @@ onMounted(loadOrders)
|
||||
.progress-chip[data-tone='success'] { color: #0f766e; background: var(--color-success-light); border-color: #a6f4c5; }
|
||||
.progress-chip[data-tone='primary'] { color: var(--color-primary-dark); background: var(--color-primary-light); border-color: #b2ddff; }
|
||||
.progress-chip[data-tone='warning'] { color: #b54708; background: var(--color-warning-light); border-color: #fedf89; }
|
||||
.follow-up-chip[data-tone='success'] { color: #0f766e; background: var(--color-success-light); border-color: #a6f4c5; }
|
||||
.follow-up-chip[data-tone='warning'] { color: #b54708; background: var(--color-warning-light); border-color: #fedf89; }
|
||||
.follow-up-chip[data-tone='danger'] { color: var(--color-danger-dark); background: var(--color-danger-light); border-color: #fecaca; }
|
||||
|
||||
@media (max-width: 1200px) { .filter-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@media (max-width: 1200px) {
|
||||
.filter-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.date-range-filter { grid-column: span 2; }
|
||||
.filter-buttons { justify-content: flex-end; }
|
||||
.order-row-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.filter-actions { flex-direction: column; align-items: flex-start; }
|
||||
.date-range-filter { grid-column: span 2; }
|
||||
.filter-buttons { justify-content: flex-start; }
|
||||
.order-row-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.filter-grid,
|
||||
.summary-grid { grid-template-columns: 1fr; }
|
||||
.filter-grid { grid-template-columns: 1fr; }
|
||||
.date-range-filter { grid-column: auto; }
|
||||
.filter-buttons { width: 100%; }
|
||||
.filter-buttons :deep(.el-button) { flex: 1 1 auto; }
|
||||
.order-card-top { flex-direction: column; align-items: flex-start; }
|
||||
.order-top-actions { width: 100%; justify-content: space-between; }
|
||||
.order-row-grid { grid-template-columns: 1fr; }
|
||||
.order-no { font-size: 18px; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user