触发条件接入 AI 助手(选模型、发送 skill、一键填入);预报时机并入下单并支持公式验证;下单失败可继续修改或存草稿;补充订单附件、H5 下单与渠道买价相关优化。 Co-authored-by: Cursor <cursoragent@cursor.com>
1015 lines
25 KiB
Vue
1015 lines
25 KiB
Vue
<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>
|