front/src/views/h5/customer-center/orders/index.vue
zzh c6a4484448 feat: 查价价卡展示贸易条款与产品分组,并优化 H5/登录与顾问体验
销售产品可配置 DDP/DDU;查价返回分组与贸易条款并展示在价卡上。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-30 11:48:36 +08:00

966 lines
23 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="{ 'is-pay': isPendingPay(row) }"
:style="{ animationDelay: `${Math.min(index, 6) * 40}ms` }"
@click="goDetail(row)"
>
<!-- 顶栏:状态 + 单号(右上) -->
<div class="h5-order-top">
<span class="h5-order-status" :style="statusStyle(row)">{{ statusLabel(row) }}</span>
<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>
</div>
<!-- 主视觉:发货仓 → 目的国(顺丰/菜鸟式线路) -->
<div class="h5-order-route">
<div class="h5-order-route__end">
<strong>{{ warehouseLabel(row) }}</strong>
<em>{{ t('h5.price.origin') }}</em>
</div>
<div class="h5-order-route__mid" aria-hidden="true">
<span class="h5-order-route__line" />
<img :src="iconRoute" alt="" class="h5-order-route__icon" draggable="false" />
<span class="h5-order-route__line" />
</div>
<div class="h5-order-route__end h5-order-route__end--dest">
<strong>{{ countryLabel(row) || countryCode(row) || '-' }}</strong>
<em>
{{
countryLabel(row) && countryCode(row)
? countryCode(row)
: t('h5.orders.detail.country')
}}
</em>
</div>
</div>
<!-- 底栏:渠道 + 金额/主操作 -->
<div class="h5-order-foot">
<p v-if="channelLabel(row) || row.cargoAttributeLabel" class="h5-order-sub">
<span v-if="channelLabel(row)">{{ channelLabel(row) }}</span>
<template v-if="channelLabel(row) && row.cargoAttributeLabel"> · </template>
<span v-if="row.cargoAttributeLabel">{{ row.cargoAttributeLabel }}</span>
</p>
<span v-else class="h5-order-sub h5-order-sub--spacer" />
<div class="h5-order-foot__action" @click.stop>
<span v-if="isPendingPay(row)" class="h5-order-amount">
<small>{{ t('h5.orders.payable') }}</small>
<em>¥{{ formatMoney(row.receivableAmount) }}</em>
</span>
<span v-else-if="row.isPaid" class="h5-order-amount is-paid">
{{ t('h5.orders.paid') }}
</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="text-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 iconRoute from '@/assets/icons/h5/route-plane.svg?url';
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 warehouseLabel(row: any) {
return String(row.warehouse || '').trim() || t('h5.price.originAny');
}
function countryLabel(row: any) {
return String(row.consigneeCountryName || '').trim();
}
function countryCode(row: any) {
return String(row.consigneeCountry || '').trim().toUpperCase();
}
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 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 {
margin-bottom: 12px;
padding: 14px;
border-radius: 14px;
border: none;
background: #fff;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
cursor: pointer;
text-align: left;
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
&.is-pay {
box-shadow:
0 0 0 1px color-mix(in srgb, #f53f3f 18%, transparent),
0 1px 2px rgba(15, 23, 42, 0.04);
}
&:active {
transform: scale(0.992);
}
}
/* 顶栏:状态 + 单号(右上) */
.h5-order-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 14px;
}
.h5-order-status {
flex-shrink: 0;
font-size: 12px;
padding: 3px 8px;
border-radius: 6px;
font-weight: 600;
white-space: nowrap;
}
.h5-order-no {
min-width: 0;
max-width: 62%;
margin: 0;
border: none;
background: transparent;
padding: 0;
font-size: 12px;
font-weight: 500;
color: #86909c;
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 4px;
cursor: pointer;
text-align: right;
span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:deep(.arco-icon) {
flex-shrink: 0;
color: #c9cdd4;
font-size: 12px;
}
}
.h5-order-amount {
display: inline-flex;
align-items: baseline;
gap: 4px;
color: #f53f3f;
line-height: 1.15;
white-space: nowrap;
small {
font-size: 11px;
font-weight: 500;
opacity: 0.85;
}
em {
font-style: normal;
font-size: 15px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
&.is-paid {
font-size: 12px;
font-weight: 600;
color: #00b42a;
}
}
/* 主视觉:仓 → 国 */
.h5-order-route {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 6px;
margin-bottom: 14px;
padding: 12px 10px;
border-radius: 12px;
background: #f7f8fa;
}
.h5-order-route__end {
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
strong {
font-size: 16px;
font-weight: 700;
color: #1d2129;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
em {
font-style: normal;
font-size: 11px;
color: #86909c;
line-height: 1.2;
}
&--dest {
text-align: right;
strong {
color: var(--app-primary-color);
}
}
}
.h5-order-route__mid {
display: flex;
align-items: center;
gap: 4px;
padding: 0 2px;
flex-shrink: 0;
}
.h5-order-route__line {
width: 16px;
height: 1px;
background: color-mix(in srgb, var(--app-primary-color) 35%, #e5e6eb);
}
.h5-order-route__icon {
width: 22px;
height: 22px;
display: block;
object-fit: contain;
pointer-events: none;
-webkit-user-drag: none;
}
/* 底栏:渠道 + 金额/操作 */
.h5-order-foot {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.h5-order-sub {
flex: 1;
min-width: 0;
margin: 0;
font-size: 12px;
color: #86909c;
line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&--spacer {
visibility: hidden;
}
}
.h5-order-foot__action {
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 10px;
}
.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);
}
}
.text-btn {
border: none;
background: transparent;
padding: 0 2px;
height: 32px;
font-size: 13px;
font-weight: 600;
color: var(--app-primary-color);
cursor: pointer;
white-space: nowrap;
}
.h5-orders-more {
margin-top: 8px;
}
@media (prefers-reduced-motion: reduce) {
.h5-orders-todo__dot,
.h5-skel-card,
.h5-enter {
animation: none;
}
}
</style>