All checks were successful
Build and Deploy Front / deploy (push) Successful in 6m2s
修复运单批量弹窗 v-if 打开时无数据,并优化充值页操作区、官网登录入口、H5 揽收列表与优惠券无门槛文案;订单类型字典改走 Dictionary/GetList。 Co-authored-by: Cursor <cursoragent@cursor.com>
730 lines
16 KiB
Vue
730 lines
16 KiB
Vue
<template>
|
|
<a-drawer
|
|
:visible="visible"
|
|
placement="bottom"
|
|
height="auto"
|
|
:footer="false"
|
|
:header="false"
|
|
unmount-on-close
|
|
:drawer-style="drawerStyle"
|
|
popup-container="body"
|
|
class="h5-coupons-drawer"
|
|
@cancel="close"
|
|
>
|
|
<div class="h5-cp">
|
|
<div class="h5-cp__handle" aria-hidden="true" />
|
|
|
|
<header class="h5-cp__head">
|
|
<h3>{{ t('h5.coupons.title') }}</h3>
|
|
<button type="button" class="h5-cp__close" @click="close">
|
|
<icon-close />
|
|
</button>
|
|
</header>
|
|
|
|
<div class="h5-cp__tabs">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab"
|
|
type="button"
|
|
class="h5-cp__tab"
|
|
:class="{ 'is-active': activeTab === tab }"
|
|
@click="switchTab(tab)"
|
|
>
|
|
{{ t(`h5.coupons.tab.${tab}`) }}
|
|
</button>
|
|
</div>
|
|
|
|
<a-spin :loading="loading" class="h5-cp__body">
|
|
<H5EmptyState
|
|
v-if="!rows.length && !loading"
|
|
:icon="IconGift"
|
|
:text="t('h5.coupons.empty')"
|
|
>
|
|
<button type="button" class="h5-cp__empty-cta" @click="goOrder">
|
|
{{ t('h5.coupons.use') }}
|
|
</button>
|
|
</H5EmptyState>
|
|
|
|
<article
|
|
v-for="item in rows"
|
|
:key="item.id"
|
|
class="coupon-ticket"
|
|
:class="{
|
|
'is-disabled': item.status !== 1,
|
|
'is-newcomer': item.isNewcomer,
|
|
'is-expiring': item.isExpiringSoon,
|
|
'is-used': item.status === 3,
|
|
'is-expired': item.status === 4,
|
|
'is-locked': item.status === 5,
|
|
}"
|
|
@click="item.status === 1 && goOrder()"
|
|
>
|
|
<span
|
|
v-if="item.status === 3 || item.status === 4 || item.status === 5"
|
|
class="coupon-stamp"
|
|
:class="item.status === 3 ? 'is-used' : item.status === 5 ? 'is-locked' : 'is-expired'"
|
|
aria-hidden="true"
|
|
>
|
|
{{ statusLabel(item.status) }}
|
|
</span>
|
|
|
|
<div class="coupon-ticket__face">
|
|
<span v-if="item.isNewcomer" class="coupon-ticket__ribbon">{{
|
|
t('h5.coupons.badge.newcomer')
|
|
}}</span>
|
|
<div class="face-amount">
|
|
<template v-if="item.faceValue != null">
|
|
<span class="currency">¥</span>
|
|
<span class="num">{{ formatAmount(item.faceValue) }}</span>
|
|
</template>
|
|
<template v-else>
|
|
<span class="num rate">{{ formatDiscount(item.discountRate) }}</span>
|
|
<span class="unit">{{ t('h5.coupons.discountUnit') }}</span>
|
|
</template>
|
|
</div>
|
|
<p class="face-rule">
|
|
{{
|
|
Number(item.minOrderAmount) > 0
|
|
? t('h5.coupons.minOrder', { amount: formatAmount(item.minOrderAmount) })
|
|
: t('h5.coupons.noThreshold')
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="coupon-ticket__notch" aria-hidden="true">
|
|
<i /><i />
|
|
</div>
|
|
|
|
<div class="coupon-ticket__body">
|
|
<div class="body-top">
|
|
<h3>{{ item.templateName || t('h5.coupons.unnamed') }}</h3>
|
|
<span v-if="item.couponTypeName" class="type-chip">{{ item.couponTypeName }}</span>
|
|
</div>
|
|
|
|
<div class="meta-row meta-row--validity">
|
|
<span class="meta-label">{{ t('h5.coupons.validLabel') }}</span>
|
|
<strong class="meta-value">
|
|
<template v-if="item.status === 5">{{ t('h5.coupons.how.unlock') }}</template>
|
|
<template v-else>
|
|
{{ formatDate(item.validStartTime) }} ~ {{ formatDate(item.validEndTime) }}
|
|
</template>
|
|
</strong>
|
|
</div>
|
|
|
|
<div class="meta-row meta-row--how">
|
|
<span class="meta-label">{{ t('h5.coupons.howLabel') }}</span>
|
|
<p class="meta-how">{{ usageTip(item) }}</p>
|
|
</div>
|
|
|
|
<div v-if="item.status === 1" class="body-foot">
|
|
<span v-if="item.isExpiringSoon" class="expire-tag">
|
|
{{ t('h5.coupons.expiringSoon') }}
|
|
</span>
|
|
<span class="use-btn">{{ t('h5.coupons.use') }}</span>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</a-spin>
|
|
</div>
|
|
</a-drawer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { IconClose, IconGift } from '@arco-design/web-vue/es/icon';
|
|
import { getMyCoupons } from '@/api/marketing/portal';
|
|
import H5EmptyState from './H5EmptyState.vue';
|
|
|
|
defineOptions({ name: 'H5CouponsSheet' });
|
|
|
|
const props = defineProps<{
|
|
visible: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'update:visible': [value: boolean];
|
|
use: [];
|
|
}>();
|
|
|
|
const { t } = useI18n();
|
|
const tabs = ['available', 'locked', 'used', 'expired'] as const;
|
|
const activeTab = ref<(typeof tabs)[number]>('available');
|
|
const loading = ref(false);
|
|
const rows = ref<any[]>([]);
|
|
|
|
const statusMap: Record<string, number> = { available: 1, locked: 5, used: 3, expired: 4 };
|
|
|
|
const drawerStyle = {
|
|
borderRadius: '20px 20px 0 0',
|
|
overflow: 'hidden',
|
|
maxHeight: 'min(92vh, 780px)',
|
|
};
|
|
|
|
function formatAmount(v: unknown) {
|
|
const n = Number(v);
|
|
if (!Number.isFinite(n)) return '0';
|
|
return Number.isInteger(n) ? String(n) : n.toFixed(2).replace(/\.?0+$/, '');
|
|
}
|
|
|
|
function formatDiscount(rate: unknown) {
|
|
const n = Number(rate);
|
|
if (!Number.isFinite(n) || n <= 0) return '--';
|
|
return `${(n * 10).toFixed((n * 10) % 1 === 0 ? 0 : 1)}`;
|
|
}
|
|
|
|
function formatDate(value: unknown) {
|
|
if (!value) return '--';
|
|
return String(value).replace('T', ' ').slice(0, 10);
|
|
}
|
|
|
|
function statusLabel(status: number) {
|
|
if (status === 3) return t('h5.coupons.tab.used');
|
|
if (status === 4) return t('h5.coupons.tab.expired');
|
|
if (status === 5) return t('h5.coupons.tab.locked');
|
|
return '';
|
|
}
|
|
|
|
function usageTip(item: {
|
|
couponType?: string;
|
|
minOrderAmount?: number;
|
|
faceValue?: number | null;
|
|
status?: number;
|
|
unlockHint?: string;
|
|
}) {
|
|
if (item.status === 5) {
|
|
return item.unlockHint || t('h5.coupons.how.unlock');
|
|
}
|
|
const type = String(item.couponType || '');
|
|
const min = formatAmount(item.minOrderAmount ?? 0);
|
|
if (type === 'percent_off') {
|
|
return t('h5.coupons.how.percent', { amount: min });
|
|
}
|
|
if (type === 'shipping_off') {
|
|
return t('h5.coupons.how.shipping', { amount: min });
|
|
}
|
|
if (type === 'direct_off') {
|
|
return t('h5.coupons.how.direct');
|
|
}
|
|
return t('h5.coupons.how.fixed', { amount: min });
|
|
}
|
|
|
|
function isExpiringSoon(end: unknown, status: number) {
|
|
if (status !== 1 || !end) return false;
|
|
const endTime = new Date(String(end)).getTime();
|
|
if (!Number.isFinite(endTime)) return false;
|
|
const days = (endTime - Date.now()) / (24 * 60 * 60 * 1000);
|
|
return days >= 0 && days <= 3;
|
|
}
|
|
|
|
function close() {
|
|
emit('update:visible', false);
|
|
}
|
|
|
|
function goOrder() {
|
|
emit('use');
|
|
close();
|
|
}
|
|
|
|
function switchTab(tab: (typeof tabs)[number]) {
|
|
activeTab.value = tab;
|
|
void load();
|
|
}
|
|
|
|
async function load() {
|
|
loading.value = true;
|
|
try {
|
|
const res = await getMyCoupons({
|
|
page: 1,
|
|
pageSize: 50,
|
|
status: statusMap[activeTab.value],
|
|
});
|
|
rows.value = (res.items || []).map((raw: any) => {
|
|
const status = Number(raw.status ?? raw.Status ?? 0);
|
|
const validEndTime = raw.validEndTime ?? raw.ValidEndTime ?? '';
|
|
const grantChannel = String(raw.grantChannel ?? raw.GrantChannel ?? '');
|
|
return {
|
|
id: raw.id ?? raw.Id,
|
|
templateName: raw.templateName ?? raw.TemplateName,
|
|
faceValue:
|
|
raw.faceValue != null || raw.FaceValue != null
|
|
? Number(raw.faceValue ?? raw.FaceValue)
|
|
: null,
|
|
discountRate:
|
|
raw.discountRate != null || raw.DiscountRate != null
|
|
? Number(raw.discountRate ?? raw.DiscountRate)
|
|
: null,
|
|
minOrderAmount: Number(raw.minOrderAmount ?? raw.MinOrderAmount ?? 0),
|
|
validStartTime: raw.validStartTime ?? raw.ValidStartTime ?? '',
|
|
validEndTime,
|
|
status,
|
|
couponType: String(raw.couponType ?? raw.CouponType ?? ''),
|
|
couponTypeName: raw.couponTypeName ?? raw.CouponTypeName ?? '',
|
|
grantChannel,
|
|
isNewcomer: grantChannel === 'newcomer',
|
|
isExpiringSoon: isExpiringSoon(validEndTime, status),
|
|
unlockHint: raw.unlockHint ?? raw.UnlockHint ?? '',
|
|
};
|
|
});
|
|
} catch {
|
|
rows.value = [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
watch(
|
|
() => props.visible,
|
|
(visible) => {
|
|
if (visible) {
|
|
activeTab.value = 'available';
|
|
void load();
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
</script>
|
|
|
|
<style lang="less">
|
|
.h5-coupons-drawer {
|
|
.arco-drawer-content {
|
|
border-radius: 20px 20px 0 0;
|
|
overflow: hidden;
|
|
max-height: min(92vh, 780px);
|
|
}
|
|
|
|
.arco-drawer-body {
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: min(92vh, 780px);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="less" scoped>
|
|
@coupon-orange: #ff7a18;
|
|
@coupon-orange-deep: #e85d04;
|
|
@coupon-orange-soft: #fff4e8;
|
|
|
|
.h5-cp {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-height: min(92vh, 780px);
|
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
background: #f5f7f8;
|
|
}
|
|
|
|
.h5-cp__handle {
|
|
width: 36px;
|
|
height: 4px;
|
|
margin: 10px auto 0;
|
|
border-radius: 999px;
|
|
background: #c9cdd4;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.h5-cp__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
position: relative;
|
|
height: 48px;
|
|
flex-shrink: 0;
|
|
|
|
h3 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #1d2129;
|
|
}
|
|
}
|
|
|
|
.h5-cp__close {
|
|
position: absolute;
|
|
right: 12px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 36px;
|
|
height: 36px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: transparent;
|
|
color: #86909c;
|
|
font-size: 18px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.h5-cp__tabs {
|
|
display: flex;
|
|
gap: 4px;
|
|
margin: 0 16px 12px;
|
|
padding: 3px;
|
|
border-radius: 12px;
|
|
background: #eef0f3;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.h5-cp__tab {
|
|
flex: 1;
|
|
height: 32px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
background: transparent;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: #86909c;
|
|
cursor: pointer;
|
|
|
|
&.is-active {
|
|
background: #fff;
|
|
color: #1d2129;
|
|
font-weight: 600;
|
|
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
}
|
|
}
|
|
|
|
.h5-cp__body {
|
|
flex: 1;
|
|
min-height: 220px;
|
|
overflow: auto;
|
|
display: block;
|
|
width: 100%;
|
|
padding: 0 16px 16px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.h5-cp__empty-cta {
|
|
margin-top: 16px;
|
|
height: 40px;
|
|
padding: 0 20px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
background: linear-gradient(135deg, @coupon-orange, @coupon-orange-deep);
|
|
color: #fff;
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
box-shadow: 0 8px 18px rgba(232, 93, 4, 0.28);
|
|
}
|
|
|
|
.coupon-ticket {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: stretch;
|
|
min-height: 128px;
|
|
margin-bottom: 14px;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
background: #fff;
|
|
box-shadow: 0 4px 16px rgba(15, 23, 42, 0.06);
|
|
|
|
&:active:not(.is-disabled) {
|
|
transform: scale(0.985);
|
|
}
|
|
|
|
&.is-disabled {
|
|
cursor: default;
|
|
filter: grayscale(0.25);
|
|
opacity: 0.88;
|
|
|
|
.use-btn {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
&.is-newcomer .coupon-ticket__face {
|
|
background: linear-gradient(150deg, #ff9f1a 0%, @coupon-orange 45%, @coupon-orange-deep 100%);
|
|
}
|
|
|
|
&.is-expiring:not(.is-disabled) {
|
|
box-shadow:
|
|
0 4px 16px rgba(232, 93, 4, 0.14),
|
|
0 0 0 1.5px rgba(232, 93, 4, 0.28);
|
|
}
|
|
}
|
|
|
|
.coupon-stamp {
|
|
position: absolute;
|
|
top: 18px;
|
|
right: 10px;
|
|
z-index: 3;
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
letter-spacing: 0.08em;
|
|
transform: rotate(18deg);
|
|
pointer-events: none;
|
|
box-sizing: border-box;
|
|
border: 2.5px solid currentColor;
|
|
background:
|
|
radial-gradient(circle at 30% 28%, rgba(255, 255, 255, 0.55), transparent 42%),
|
|
rgba(255, 255, 255, 0.72);
|
|
box-shadow:
|
|
inset 0 0 0 1px rgba(255, 255, 255, 0.5),
|
|
0 2px 8px rgba(15, 23, 42, 0.08);
|
|
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 4px;
|
|
border-radius: 50%;
|
|
border: 1px dashed currentColor;
|
|
opacity: 0.75;
|
|
}
|
|
|
|
&.is-used {
|
|
color: #86909c;
|
|
}
|
|
|
|
&.is-expired {
|
|
color: #c9cdd4;
|
|
}
|
|
|
|
&.is-locked {
|
|
color: var(--app-primary-color, #1d7281);
|
|
}
|
|
}
|
|
|
|
.coupon-ticket__face {
|
|
position: relative;
|
|
width: 128px;
|
|
flex-shrink: 0;
|
|
padding: 20px 10px 16px;
|
|
color: #fff;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
background: linear-gradient(150deg, #ffb347 0%, @coupon-orange 48%, @coupon-orange-deep 100%);
|
|
overflow: hidden;
|
|
|
|
&::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background:
|
|
repeating-linear-gradient(
|
|
-32deg,
|
|
transparent,
|
|
transparent 8px,
|
|
rgba(255, 255, 255, 0.08) 8px,
|
|
rgba(255, 255, 255, 0.08) 16px
|
|
);
|
|
pointer-events: none;
|
|
}
|
|
}
|
|
|
|
.coupon-ticket__ribbon {
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 8px;
|
|
z-index: 1;
|
|
height: 18px;
|
|
padding: 0 6px;
|
|
border-radius: 6px;
|
|
background: #fff;
|
|
color: @coupon-orange-deep;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.face-amount {
|
|
position: relative;
|
|
z-index: 1;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
gap: 1px;
|
|
line-height: 1;
|
|
|
|
.currency {
|
|
font-size: 16px;
|
|
font-weight: 800;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.num {
|
|
font-size: 40px;
|
|
font-weight: 900;
|
|
letter-spacing: -0.04em;
|
|
font-variant-numeric: tabular-nums;
|
|
text-shadow: 0 3px 10px rgba(120, 40, 0, 0.22);
|
|
|
|
&.rate {
|
|
font-size: 34px;
|
|
}
|
|
}
|
|
|
|
.unit {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
margin-bottom: 5px;
|
|
}
|
|
}
|
|
|
|
.face-rule {
|
|
position: relative;
|
|
z-index: 1;
|
|
margin: 10px 0 0;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
opacity: 0.95;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.coupon-ticket__notch {
|
|
position: relative;
|
|
width: 14px;
|
|
flex-shrink: 0;
|
|
background: linear-gradient(#e5e6eb 50%, transparent 50%) top / 1.5px 8px repeat-y;
|
|
background-position: center;
|
|
|
|
i {
|
|
position: absolute;
|
|
left: 50%;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: #f5f7f8;
|
|
transform: translateX(-50%);
|
|
|
|
&:first-child {
|
|
top: -7px;
|
|
}
|
|
|
|
&:last-child {
|
|
bottom: -7px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.coupon-ticket__body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 14px 14px 12px 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
}
|
|
|
|
.body-top {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
|
|
h3 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin: 0;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
color: #1d2129;
|
|
line-height: 1.35;
|
|
}
|
|
}
|
|
|
|
.type-chip {
|
|
flex-shrink: 0;
|
|
height: 20px;
|
|
padding: 0 7px;
|
|
border-radius: 999px;
|
|
background: @coupon-orange-soft;
|
|
color: @coupon-orange-deep;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
line-height: 20px;
|
|
}
|
|
|
|
.meta-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 6px;
|
|
margin-top: 6px;
|
|
|
|
&--validity .meta-value {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: @coupon-orange-deep;
|
|
letter-spacing: 0.01em;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
&--how .meta-how {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
color: #4e5969;
|
|
line-height: 1.45;
|
|
font-weight: 500;
|
|
}
|
|
}
|
|
|
|
.meta-label {
|
|
flex-shrink: 0;
|
|
height: 18px;
|
|
padding: 0 5px;
|
|
border-radius: 4px;
|
|
background: rgba(232, 93, 4, 0.1);
|
|
color: @coupon-orange-deep;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 18px;
|
|
}
|
|
|
|
.body-foot {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.expire-tag {
|
|
height: 22px;
|
|
padding: 0 8px;
|
|
border-radius: 999px;
|
|
background: rgba(232, 93, 4, 0.14);
|
|
color: @coupon-orange-deep;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
line-height: 22px;
|
|
animation: coupon-pulse 1.6s ease-in-out infinite;
|
|
}
|
|
|
|
.use-btn {
|
|
margin-left: auto;
|
|
height: 30px;
|
|
padding: 0 16px;
|
|
border-radius: 999px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: #fff;
|
|
background: linear-gradient(135deg, @coupon-orange, @coupon-orange-deep);
|
|
box-shadow: 0 6px 14px rgba(232, 93, 4, 0.32);
|
|
}
|
|
|
|
@keyframes coupon-pulse {
|
|
0%,
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.62;
|
|
}
|
|
}
|
|
</style>
|