front/src/views/h5/customer-center/orders/index.vue
zzh a2e71a3a6b feat: 自动触发支持 AI 写公式,并完善下单失败与订单附件体验
触发条件接入 AI 助手(选模型、发送 skill、一键填入);预报时机并入下单并支持公式验证;下单失败可继续修改或存草稿;补充订单附件、H5 下单与渠道买价相关优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 20:54:28 +08:00

1015 lines
25 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.

<template>
<div class="h5-orders-page">
<button
v-if="todoCount > 0"
type="button"
class="h5-orders-todo h5-enter"
@click="activeGroup = actionCount > 0 ? 'actionNeeded' : 'issue'"
>
<span class="h5-orders-todo__dot" />
<span class="h5-orders-todo__body">
<strong>{{ t('h5.orders.todo') }}</strong>
<span>
<template v-if="actionCount > 0">
{{ t('h5.orders.group.actionNeeded') }} {{ actionCount }}
</template>
<template v-if="actionCount > 0 && issueCount > 0"> · </template>
<template v-if="issueCount > 0">
{{ t('h5.orders.group.issue') }} {{ issueCount }}
</template>
</span>
</span>
<span class="h5-orders-todo__go">{{ t('h5.me.todoGo') }}</span>
</button>
<div class="h5-orders-search">
<icon-search class="h5-orders-search__icon" />
<input
v-model="keyword"
class="h5-orders-search__input"
type="search"
enterkeyhint="search"
:placeholder="t('h5.orders.searchPlaceholder')"
@keyup.enter="reload"
/>
<button
v-if="keyword"
type="button"
class="h5-orders-search__clear"
aria-label="clear"
@click="keyword = ''; reload()"
>
×
</button>
</div>
<div class="h5-orders-tabs">
<button
v-for="group in groups"
:key="group.key"
type="button"
class="h5-orders-tab"
:class="{ 'is-active': activeGroup === group.key }"
@click="activeGroup = group.key"
>
{{ t(group.labelKey) }}
<em v-if="groupCount(group) > 0">{{ groupCount(group) }}</em>
</button>
</div>
<div v-if="loading && !orders.length" class="h5-skel">
<div v-for="n in 3" :key="n" class="h5-skel-card" />
</div>
<a-spin :loading="loading && orders.length > 0" class="h5-orders-spin">
<H5EmptyState
v-if="!loading && !orders.length"
:icon="IconList"
:text="t('h5.orders.empty')"
>
<a-button type="outline" size="small" @click="h5Navigate(router, '/h5/price')">
{{ t('h5.orders.emptyGoPrice') }}
</a-button>
</H5EmptyState>
<article
v-for="(row, index) in orders"
:key="row.id || row.waybillNo"
class="h5-order-card h5-enter"
:class="[`h5-order-card--${cardTone(row)}`, { 'is-pay': isPendingPay(row) }]"
:style="{ animationDelay: `${Math.min(index, 6) * 40}ms` }"
@click="goDetail(row)"
>
<span class="h5-order-card__blob" aria-hidden="true" />
<span class="h5-order-card__shine" aria-hidden="true" />
<div class="h5-order-card__main">
<div class="h5-order-card__body">
<!-- 客户第一眼:单号 + 状态 -->
<div class="h5-order-row1">
<button
type="button"
class="h5-order-no"
@click.stop="copyText(row.waybillNo || row.customerOrderNo)"
>
<span>{{ row.waybillNo || row.customerOrderNo || row.trackingNo || '-' }}</span>
<icon-copy />
</button>
<span class="h5-order-status" :style="statusStyle(row)">{{ statusLabel(row) }}</span>
</div>
<!-- 第二眼:线路 · 目的地 -->
<p v-if="channelLabel(row) || destinationLabel(row)" class="h5-order-route">
<span v-if="channelLabel(row)">{{ channelLabel(row) }}</span>
<template v-if="channelLabel(row) && destinationLabel(row)">
<i>·</i>
</template>
<span v-if="destinationLabel(row)" class="h5-order-route__dest">
{{ destinationLabel(row) }}
</span>
</p>
<!-- 辅助信息:普货 / 仓库,弱化 -->
<div
v-if="row.cargoAttributeLabel || row.warehouse"
class="h5-order-meta"
>
<span v-if="row.cargoAttributeLabel" class="h5-meta-tag">
{{ row.cargoAttributeLabel }}
</span>
<span v-if="row.warehouse" class="h5-meta-tag">
{{ row.warehouse }}
</span>
</div>
</div>
<div class="h5-order-side" @click.stop>
<span v-if="isPendingPay(row) || row.isPaid" class="h5-order-amount">
<template v-if="isPendingPay(row)">
<small>{{ t('h5.orders.payable') }}</small>
<em>¥{{ formatMoney(row.receivableAmount) }}</em>
</template>
<template v-else>
<span class="is-paid">{{ t('h5.orders.paid') }}</span>
</template>
</span>
<button
v-if="isPendingPay(row)"
type="button"
class="primary-btn primary-btn--danger"
@click="openPay(row)"
>
{{ t('h5.orders.pay') }}
</button>
<button
v-else-if="isShipping(row)"
type="button"
class="primary-btn"
@click="goDetail(row, 'track')"
>
{{ t('h5.orders.track') }}
</button>
<button
v-else-if="isIssue(row)"
type="button"
class="primary-btn primary-btn--warn"
@click="askAdvisor(row)"
>
{{ t('h5.orders.askAdvisor') }}
</button>
<button v-else type="button" class="primary-btn" @click="goDetail(row)">
{{ t('h5.orders.detail') }}
</button>
</div>
</div>
</article>
<a-button
v-if="hasMore"
long
class="h5-orders-more"
:loading="loadingMore"
@click="loadMore"
>
{{ t('h5.orders.loadMore') }}
</a-button>
</a-spin>
<H5OrderPaySheet
v-model:visible="payVisible"
:orders="payOrders"
@success="onPaySuccess"
@fail="onPayFail"
/>
<H5OrderDetailSheet
v-model:visible="detailVisible"
:order-id="detailOrderId"
:initial-tab="detailTab"
@paid="onPaySuccess"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { Message, Modal } from '@arco-design/web-vue';
import { IconCopy, IconList, IconSearch } from '@arco-design/web-vue/es/icon';
import { h5Navigate } from '@/utils/device';
import H5EmptyState from '../components/H5EmptyState.vue';
import {
getOrderStatusCounts,
postOrderList,
} from '@/api/order';
import {
getOrderStatusMeta,
resolveOrderStatusCode,
type OrderStatusCode,
} from '@/constants/orderStatus';
import {
H5_ORDER_GROUPS,
sumStatusCounts,
type H5OrderGroup,
type H5OrderGroupKey,
} from '@/constants/h5OrderStatus';
import {
buildAskAdvisorQuery,
buildWaybillContextFromRow,
} from '@/composables/useAskAdvisor';
import H5OrderPaySheet from '../components/H5OrderPaySheet.vue';
import H5OrderDetailSheet from '../components/H5OrderDetailSheet.vue';
defineOptions({ name: 'H5Orders' });
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const groups = H5_ORDER_GROUPS;
function resolveInitialGroup(): H5OrderGroupKey {
const raw = String(route.query.group || '');
if (groups.some((g) => g.key === raw)) return raw as H5OrderGroupKey;
return 'all';
}
const activeGroup = ref<H5OrderGroupKey>(resolveInitialGroup());
const keyword = ref('');
const loading = ref(false);
const loadingMore = ref(false);
const page = ref(1);
const pageSize = 20;
const total = ref(0);
const orders = ref<any[]>([]);
const statusCounts = ref<Record<string, number>>({});
const payVisible = ref(false);
const payOrders = ref<{ id: number | string; waybillNo?: string }[]>([]);
const detailVisible = ref(false);
const detailOrderId = ref<number | string>('');
const detailTab = ref<'overview' | 'track' | 'fee'>('overview');
const actionCount = computed(() =>
sumStatusCounts(statusCounts.value, ['pendingPayment']),
);
const issueCount = computed(() =>
sumStatusCounts(statusCounts.value, ['deliveryFailed', 'problematic', 'claimed']),
);
const todoCount = computed(() => actionCount.value + issueCount.value);
const hasMore = computed(() => orders.value.length < total.value);
function pick(raw: Record<string, any> | undefined, ...keys: string[]) {
if (!raw) return undefined;
for (const k of keys) {
const v = raw[k];
if (v !== undefined && v !== null && v !== '') return v;
}
return undefined;
}
function cargoLabel(raw: any) {
const value = String(pick(raw, 'cargoAttribute', 'CargoAttribute') ?? '').trim();
if (!value) return '';
const map: Record<string, string> = {
general: 'order.myOrders.drawer.cargoAttribute.general',
electronics: 'order.myOrders.drawer.cargoAttribute.electronics',
'3c': 'order.myOrders.drawer.cargoAttribute.electronics',
sensitive: 'order.myOrders.drawer.cargoAttribute.sensitive',
};
const key = map[value.toLowerCase()];
if (key) {
const label = t(key);
if (label && label !== key) return label;
}
return value;
}
function normalizeOrderRow(raw: any) {
const r = raw?.receiver || raw?.Receiver || {};
const serviceNo = pick(raw, 'serviceNo', 'ServiceNo', 'waybillNo', 'WaybillNo');
const clientNo = pick(raw, 'clientNo', 'ClientNo', 'customerOrderNo', 'CustomerOrderNo');
const trackNo = pick(raw, 'trackNo', 'TrackNo', 'trackingNo', 'TrackingNo');
const isPaidRaw = pick(raw, 'isPaid', 'IsPaid');
const warehouse = String(
pick(raw, 'warehouse', 'Warehouse', 'warehouseName', 'WarehouseName') ?? '',
).trim();
return {
...raw,
id: pick(raw, 'id', 'Id'),
waybillNo: serviceNo ?? '',
customerOrderNo: clientNo ?? '',
trackingNo: trackNo ?? '',
productName: pick(raw, 'productName', 'ProductName', 'productCode', 'ProductCode') ?? '',
channelName: pick(raw, 'channelName', 'ChannelName', 'channelCode', 'ChannelCode') ?? '',
isPaid: isPaidRaw === true || isPaidRaw === 1 || isPaidRaw === '1',
consigneeCountry: pick(raw, 'consigneeCountry', 'ConsigneeCountry') ?? pick(r, 'country', 'Country') ?? '',
consigneeCountryName:
pick(raw, 'consigneeCountryName', 'ConsigneeCountryName', 'chineseName', 'ChineseName') ??
pick(r, 'countryName', 'CountryName', 'chineseName', 'ChineseName') ??
'',
receivableAmount: pick(raw, 'receivableAmount', 'ReceivableAmount') ?? '',
status: resolveOrderStatusCode(raw),
createTime: pick(raw, 'createTime', 'CreateTime') ?? '',
warehouse,
cargoAttributeLabel: cargoLabel(raw),
};
}
function currentGroup(): H5OrderGroup {
return groups.find((g) => g.key === activeGroup.value) || groups[0];
}
function groupCount(group: H5OrderGroup) {
if (group.key === 'all') {
return Object.values(statusCounts.value).reduce((s, n) => s + (Number(n) || 0), 0);
}
return sumStatusCounts(statusCounts.value, group.statuses);
}
function statusLabel(row: any) {
const code = resolveOrderStatusCode(row);
const meta = getOrderStatusMeta(code);
if (!meta) return code || '-';
return t(meta.labelKey);
}
function statusStyle(row: any) {
const meta = getOrderStatusMeta(resolveOrderStatusCode(row));
return {
color: meta?.tagColor || '#4e5969',
background: meta?.tagBgColor || '#f2f3f5',
};
}
function channelLabel(row: any) {
return String(row.channelName || row.productName || '').trim();
}
function destinationLabel(row: any) {
const name = String(row.consigneeCountryName || '').trim();
const code = String(row.consigneeCountry || '').trim().toUpperCase();
if (name && code && name.toUpperCase() !== code) return `${name} ${code}`;
return name || code;
}
function isPendingPay(row: any) {
return resolveOrderStatusCode(row) === 'pendingPayment';
}
function isShipping(row: any) {
return resolveOrderStatusCode(row) === 'shipping';
}
function isIssue(row: any) {
const code = resolveOrderStatusCode(row) as OrderStatusCode;
return ['deliveryFailed', 'problematic', 'claimed'].includes(code);
}
function cardTone(row: any) {
if (isPendingPay(row)) return 'danger';
if (isIssue(row)) return 'warning';
if (isShipping(row)) return 'primary';
if (row.isPaid || resolveOrderStatusCode(row) === 'deliverySuccess') return 'success';
return 'neutral';
}
function formatMoney(val: unknown) {
const n = Number(val);
if (!Number.isFinite(n)) return String(val ?? '-');
return n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
async function copyText(text?: string) {
if (!text) return;
try {
await navigator.clipboard.writeText(text);
Message.success(t('h5.orders.copySuccess'));
} catch {
Message.error(t('h5.orders.copyFailed'));
}
}
async function fetchCounts() {
try {
const res = await getOrderStatusCounts();
statusCounts.value = res.counts || {};
} catch {
statusCounts.value = {};
}
}
/**
* 多状态分组:优先用单状态接口;多状态时拉 all 后在前端过滤(一期可行,量大再改后端 in 查询)
*/
async function fetchPage(pageNo: number, append: boolean) {
const group = currentGroup();
const singleStatus =
group.statuses.length === 1 ? group.statuses[0] : group.statuses.length === 0 ? 'all' : 'all';
const res = await postOrderList({
page: pageNo,
pageSize: group.statuses.length > 1 ? Math.max(pageSize, 50) : pageSize,
status: singleStatus,
keyword: keyword.value.trim(),
});
let items = (res.items || []).map(normalizeOrderRow);
if (group.statuses.length > 1) {
const set = new Set(group.statuses);
items = items.filter((row) => set.has(resolveOrderStatusCode(row) as OrderStatusCode));
}
if (append) {
orders.value = [...orders.value, ...items];
} else {
orders.value = items;
}
total.value =
group.statuses.length > 1 ? groupCount(group) || items.length : res.total || items.length;
page.value = pageNo;
}
async function reload() {
loading.value = true;
try {
await Promise.all([fetchCounts(), fetchPage(1, false)]);
} catch (e: any) {
Message.error(e?.message || t('h5.orders.empty'));
orders.value = [];
} finally {
loading.value = false;
}
}
async function loadMore() {
if (!hasMore.value || loadingMore.value) return;
loadingMore.value = true;
try {
await fetchPage(page.value + 1, true);
} finally {
loadingMore.value = false;
}
}
function goDetail(row: any, tab?: string) {
detailOrderId.value = row.id;
detailTab.value =
tab === 'track' || tab === 'fee' || tab === 'overview' ? tab : 'overview';
detailVisible.value = true;
}
function openPay(row: any) {
payOrders.value = [{ id: row.id, waybillNo: row.waybillNo }];
payVisible.value = true;
}
function onPaySuccess() {
void reload();
}
function isBalanceLowError(error: unknown) {
const msg = String((error as any)?.message || (error as any)?.Message || error || '');
return /余额不足|balance|insufficient/i.test(msg);
}
function onPayFail(error: unknown) {
if (!isBalanceLowError(error)) return;
Modal.confirm({
title: t('h5.pay.balanceLowTitle'),
content: t('h5.pay.balanceLow'),
okText: t('h5.pay.goRecharge'),
cancelText: t('h5.pay.later'),
onOk: () => {
h5Navigate(router, '/h5/recharge');
},
});
}
function askAdvisor(row: any) {
const ctx = buildWaybillContextFromRow(row, 'h5Orders');
h5Navigate(router, {
path: '/h5/assistant',
query: buildAskAdvisorQuery(ctx),
});
}
watch(
() => route.query.group,
(g) => {
const raw = String(g || '');
if (groups.some((item) => item.key === raw)) {
activeGroup.value = raw as H5OrderGroupKey;
}
},
);
watch(activeGroup, () => {
void reload();
});
async function tryOpenOrderFromQuery() {
const rawId = route.query.orderId;
const idStr = Array.isArray(rawId) ? rawId[0] : rawId;
const id = Number(idStr);
if (!Number.isFinite(id) || id <= 0) return;
goDetail({ id });
const nextQuery = { ...route.query } as Record<string, any>;
delete nextQuery.orderId;
await router.replace({ query: nextQuery });
}
watch(
() => route.query.orderId,
(orderId) => {
if (!orderId) return;
void tryOpenOrderFromQuery();
},
);
void reload().then(() => tryOpenOrderFromQuery());
</script>
<style lang="less" scoped>
.h5-orders-page {
padding: 12px 16px 24px;
background: linear-gradient(
180deg,
color-mix(in srgb, var(--app-primary-color) 8%, #f5f7f8) 0%,
#f5f7f8 140px
);
min-height: 100%;
box-sizing: border-box;
}
.h5-orders-todo {
width: 100%;
margin-bottom: 12px;
padding: 12px 14px;
border: none;
border-radius: 14px;
background:
linear-gradient(135deg, rgba(255, 247, 232, 0.95), rgba(255, 255, 255, 0.88));
color: #ff7d00;
display: flex;
align-items: center;
gap: 10px;
text-align: left;
cursor: pointer;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.85),
0 6px 16px rgba(255, 125, 0, 0.1);
}
.h5-orders-todo__dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: currentColor;
flex-shrink: 0;
animation: h5-todo-pulse 1.8s ease-out infinite;
}
@keyframes h5-todo-pulse {
0% {
box-shadow: 0 0 0 0 rgba(255, 125, 0, 0.45);
}
70% {
box-shadow: 0 0 0 8px rgba(255, 125, 0, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(255, 125, 0, 0);
}
}
.h5-orders-todo__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 2px;
font-size: 13px;
strong {
font-size: 12px;
opacity: 0.9;
}
}
.h5-orders-todo__go {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.h5-orders-search {
position: relative;
display: flex;
align-items: center;
margin-bottom: 8px;
height: 40px;
padding: 0 12px 0 36px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.92);
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.04);
}
.h5-orders-search__icon {
position: absolute;
left: 12px;
color: #c9cdd4;
font-size: 16px;
}
.h5-orders-search__input {
flex: 1;
min-width: 0;
height: 100%;
border: none;
outline: none;
background: transparent;
font-size: 14px;
color: #1d2129;
&::placeholder {
color: #c9cdd4;
}
}
.h5-orders-search__clear {
border: none;
background: transparent;
color: #c9cdd4;
font-size: 20px;
line-height: 1;
padding: 0 2px;
cursor: pointer;
}
.h5-orders-tabs {
display: flex;
gap: 2px;
overflow-x: auto;
padding: 2px 0 10px;
margin-bottom: 2px;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
}
}
.h5-orders-tab {
flex: 0 0 auto;
height: 28px;
padding: 0 10px;
border: none;
border-radius: 6px;
background: transparent;
font-size: 13px;
font-weight: 500;
color: #86909c;
white-space: nowrap;
cursor: pointer;
em {
margin-left: 3px;
font-style: normal;
font-size: 11px;
font-weight: 600;
opacity: 0.75;
}
&.is-active {
color: #4e5969;
background: rgba(15, 23, 42, 0.05);
font-weight: 600;
}
}
.h5-orders-spin {
width: 100%;
display: block;
}
.h5-skel {
display: grid;
gap: 10px;
}
.h5-skel-card {
height: 108px;
border-radius: 16px;
background: linear-gradient(90deg, #eef0f3 25%, #f7f8fa 37%, #eef0f3 63%);
background-size: 400% 100%;
animation: h5-skel 1.2s ease infinite;
}
@keyframes h5-skel {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 0;
}
}
.h5-orders-spin :deep(.h5-empty) {
p {
margin-bottom: 14px;
}
}
.h5-order-card {
--liquid-a: rgba(45, 140, 140, 0.1);
--liquid-b: rgba(125, 162, 255, 0.08);
--liquid-edge: rgba(255, 255, 255, 0.78);
--liquid-glow: rgba(45, 140, 140, 0.05);
position: relative;
z-index: 0;
overflow: hidden;
isolation: isolate;
margin-bottom: 12px;
padding: 16px 14px;
border-radius: 16px;
border: 1px solid var(--liquid-edge);
background:
linear-gradient(
155deg,
rgba(255, 255, 255, 0.9) 0%,
rgba(255, 255, 255, 0.6) 48%,
rgba(255, 255, 255, 0.82) 100%
);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.9),
inset 0 -1px 0 rgba(255, 255, 255, 0.18),
0 6px 18px var(--liquid-glow),
0 1px 2px rgba(15, 23, 42, 0.03);
backdrop-filter: blur(12px) saturate(1.15);
-webkit-backdrop-filter: blur(12px) saturate(1.15);
cursor: pointer;
text-align: left;
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
&--primary {
--liquid-a: rgba(0, 121, 121, 0.14);
--liquid-b: rgba(64, 128, 255, 0.1);
--liquid-glow: rgba(0, 121, 121, 0.08);
}
&--warning {
--liquid-a: rgba(255, 125, 0, 0.14);
--liquid-b: rgba(255, 182, 93, 0.12);
--liquid-glow: rgba(255, 125, 0, 0.08);
}
&--danger {
--liquid-a: rgba(245, 63, 63, 0.14);
--liquid-b: rgba(249, 137, 129, 0.1);
--liquid-glow: rgba(245, 63, 63, 0.08);
}
&--success {
--liquid-a: rgba(0, 180, 42, 0.12);
--liquid-b: rgba(76, 210, 99, 0.1);
--liquid-glow: rgba(0, 180, 42, 0.06);
}
&--neutral {
--liquid-a: rgba(45, 140, 140, 0.1);
--liquid-b: rgba(125, 162, 255, 0.08);
--liquid-glow: rgba(45, 140, 140, 0.05);
}
&:active {
transform: scale(0.992);
}
}
.h5-order-card__blob,
.h5-order-card__shine,
.h5-order-card__main {
position: relative;
z-index: 1;
}
.h5-order-card__blob {
position: absolute;
z-index: 0;
inset: -42% -18% auto auto;
width: 78%;
height: 125%;
border-radius: 42% 58% 48% 52% / 48% 42% 58% 52%;
background:
radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.55) 0%, transparent 42%),
radial-gradient(circle at 70% 65%, var(--liquid-a) 0%, transparent 58%),
radial-gradient(circle at 20% 80%, var(--liquid-b) 0%, transparent 55%);
filter: blur(12px);
opacity: 0.7;
pointer-events: none;
}
.h5-order-card__shine {
position: absolute;
z-index: 0;
inset: 0;
border-radius: inherit;
background: linear-gradient(
115deg,
transparent 28%,
rgba(255, 255, 255, 0.05) 42%,
rgba(255, 255, 255, 0.22) 50%,
rgba(255, 255, 255, 0.05) 58%,
transparent 72%
);
background-size: 220% 100%;
background-position: 110% 0;
opacity: 0.38;
pointer-events: none;
}
.h5-order-card__main {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.h5-order-card__body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
/* 第一眼:单号(主) + 状态(情绪) */
.h5-order-row1 {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.h5-order-no {
flex: 1;
min-width: 0;
margin: 0;
border: none;
background: transparent;
padding: 0;
font-size: 16px;
font-weight: 700;
color: #1d2129;
display: inline-flex;
align-items: center;
gap: 5px;
cursor: pointer;
text-align: left;
letter-spacing: 0.01em;
span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.arco-icon) {
flex-shrink: 0;
color: #c9cdd4;
font-size: 14px;
}
}
.h5-order-status {
flex-shrink: 0;
font-size: 12px;
padding: 3px 8px;
border-radius: 6px;
font-weight: 600;
white-space: nowrap;
}
/* 第二眼:线路 · 目的地 */
.h5-order-route {
margin: 0;
font-size: 13px;
line-height: 1.4;
color: #4e5969;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
i {
font-style: normal;
margin: 0 5px;
color: #c9cdd4;
}
&__dest {
color: #1d2129;
font-weight: 600;
}
}
/* 辅助:普货 / 仓库 — 无色弱标签 */
.h5-order-meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.h5-meta-tag {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
color: #86909c;
background: rgba(15, 23, 42, 0.04);
white-space: nowrap;
}
/* 右侧:金额 + 主操作 */
.h5-order-side {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
gap: 8px;
min-width: 72px;
}
.h5-order-amount {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 1px;
color: #f53f3f;
line-height: 1.15;
white-space: nowrap;
small {
font-size: 10px;
font-weight: 500;
opacity: 0.85;
}
em {
font-style: normal;
font-size: 16px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.is-paid {
font-size: 12px;
font-weight: 600;
color: #00b42a;
}
}
.primary-btn {
border: none;
height: 32px;
padding: 0 14px;
border-radius: 16px;
background: var(--app-primary-color);
color: #fff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
box-shadow: 0 4px 12px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
&--danger {
background: #f53f3f;
box-shadow: 0 4px 12px rgba(245, 63, 63, 0.28);
}
&--warn {
background: #ff7d00;
box-shadow: 0 4px 12px rgba(255, 125, 0, 0.28);
}
}
.h5-orders-more {
margin-top: 8px;
}
@media (prefers-reduced-motion: reduce) {
.h5-orders-todo__dot,
.h5-skel-card,
.h5-enter {
animation: none;
}
}
</style>