优化订单列表ui

This commit is contained in:
yml2213
2026-05-14 21:17:14 +08:00
parent a48fed16c7
commit a554fee36e
2 changed files with 165 additions and 107 deletions
@@ -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>
+155 -104
View File
@@ -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>