Files
longli-mp/src/components/BenefitCard/index.vue
T

174 lines
7.6 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.
<script setup>
/**
* 权益卡(全站唯一组件 · 4 处复用)
* 使用位置:权益中心 / 我的权益 / 详情页「到店权益」
* 四行固定顺序:类型 / 价格 / 规格·适用 / 有效期·名额(v11 价格表达)
* 所有权益元素一律票根橙,不按类型分色;灰化态与 cta 态走「互斥类串」,不复制结构。
*/
import { computed } from "vue";
import { BENEFIT_STATUS, BENEFIT_TYPE_ICON, BENEFIT_TYPE_LABEL } from "@/constant";
import { discountLabel, dotDate, priceText } from "@/utils";
const props = defineProps({
benefit: { type: Object, required: true },
/** center 权益中心 | mine 我的权益 | detail 详情页到店权益 */
mode: { type: String, default: "center" },
});
const emit = defineEmits(["action", "tap"]);
const b = computed(() => props.benefit || {});
const status = computed(() => b.value.status);
const isOff = computed(
() => status.value === BENEFIT_STATUS.EXPIRED || status.value === BENEFIT_STATUS.SOLD_OUT
);
const isClaimed = computed(() => status.value === BENEFIT_STATUS.CLAIMED);
const typeLabel = computed(() => BENEFIT_TYPE_LABEL[b.value.type] || "权益");
const typeIcon = computed(() => BENEFIT_TYPE_ICON[b.value.type] || "gift");
const deal = computed(() => priceText(b.value.price?.deal));
const original = computed(() => priceText(b.value.price?.original));
const pct = computed(() => discountLabel(b.value.price?.deal, b.value.price?.original));
const spec = computed(() => b.value.price?.spec || "");
const hasPrice = computed(() => !!deal.value);
const slogan = computed(() =>
Array.isArray(b.value.slogan) ? b.value.slogan : b.value.slogan ? [b.value.slogan] : []
);
const validText = computed(() => {
const { valid_to: to, valid_days: days } = b.value;
if ((isClaimed.value || status.value === BENEFIT_STATUS.EXPIRED) && to) {
return `有效期至 ${dotDate(to)}`;
}
if (days) return `有效期:${days} 天`;
if (to) return `有效期至 ${dotDate(to)}`;
return "";
});
/** meta 行第二项:随状态变化 */
const secondMeta = computed(() => {
const left = Number(b.value.stock_left);
switch (status.value) {
case BENEFIT_STATUS.SOLD_OUT:
return { text: "今日名额已发完", icon: "people", ok: false };
case BENEFIT_STATUS.EXPIRED:
return { text: "已失效", icon: "clock", ok: false };
case BENEFIT_STATUS.CLAIMED:
return { text: "可多次使用", icon: "check", ok: true };
default:
if (!Number.isNaN(left) && left > 0 && left <= 20) {
return { text: `仅剩 ${left} 个名额`, icon: "people", ok: false };
}
return { text: "剩余名额充足", icon: "people", ok: true };
}
});
const ctaText = computed(() => {
if (status.value === BENEFIT_STATUS.SOLD_OUT) return "已抢光";
if (status.value === BENEFIT_STATUS.EXPIRED) return "已失效";
if (isClaimed.value) return "去使用";
return "立即领取";
});
/* ---- 灰化态互斥类串(同属性只出现其一,不依赖 CSS 顺序) ---- */
const rootStateCls = computed(() =>
isOff.value ? "border-line2 shadow-elev1" : "border-[rgba(255,138,61,0.3)] shadow-ccard"
);
const sideStateCls = computed(() => (isOff.value ? "bg-[#F8FAFB]" : "grad-cardside"));
const kindStateCls = computed(() => (isOff.value ? "text-ink3" : "text-accent"));
const kindIconCls = computed(() =>
isOff.value ? "bg-[#BFC7CE]" : "grad-accent shadow-[0_4px_10px_rgba(255,138,61,0.34)]"
);
const priceNumCls = computed(() => (isOff.value ? "text-[#A7B0B8]" : "gradtext-accent"));
const wasCls = computed(() => (isOff.value ? "text-[#B6BEC5]" : "text-ink3"));
const pctCls = computed(() =>
isOff.value ? "bg-[#EDF1F4] text-ink3" : "bg-[rgba(255,138,61,0.14)] text-accent-deep"
);
const specCls = computed(() =>
isOff.value
? "bg-[#EDF1F4] text-ink3 border-transparent"
: "bg-[rgba(255,138,61,0.13)] text-accent-deep border-[rgba(255,138,61,0.3)]"
);
const scopeCls = computed(() => (isOff.value ? "text-ink3" : "text-ink2"));
const coverImgCls = computed(() => (isOff.value ? "grayscale-[0.6] brightness-[0.96]" : ""));
const ctaStateCls = computed(() =>
isOff.value ? "bg-[#C7CED4]" : "grad-accent shadow-[0_8px_18px_rgba(255,138,61,0.4)]"
);
const onTap = () => {
emit("tap", b.value);
emit("action", b.value);
};
</script>
<template>
<view class="relative flex h-[130px] bg-white border-[0.5px] rounded-card mt-[11px]" :class="rootStateCls"
hover-class="opacity-90" @click="onTap">
<!-- 左:信息区(四行固定顺序) -->
<view class="flex-1 min-w-0 rounded-l-card px-[14px] pt-[13px] pb-3 flex flex-col" :class="sideStateCls">
<!-- ① 类型 -->
<view class="flex items-center gap-[6px] text-[13px] font-semibold whitespace-nowrap" :class="kindStateCls">
<view class="w-[21px] h-[21px] rounded-full shrink-0 flex items-center justify-center" :class="kindIconCls">
<AppIcon :name="typeIcon" tone="white" :size="12.5" />
</view>
<text>{{ typeLabel }}</text>
</view>
<!-- ② 价格:优惠价 + 划线原价 + 折扣标(右端) -->
<view v-if="hasPrice" class="flex items-baseline gap-[6px] mt-2 leading-none whitespace-nowrap">
<text class="text-[15px] font-bold" :class="priceNumCls">¥</text>
<text class="text-display font-extrabold tracking-[-1px]" :class="priceNumCls">{{ deal }}</text>
<text v-if="original" class="text-[11.5px] font-medium line-through" :class="wasCls">¥{{ original }}</text>
<text v-if="pct" class="ml-auto text-[10px] font-bold tracking-[0.2px] rounded-[6px] px-[6px] py-[3px]"
:class="pctCls">
{{ pct }}
</text>
</view>
<!-- ③ 规格胶囊 + 适用范围 -->
<view class="flex items-center gap-[6px] mt-[7px] text-[11px] whitespace-nowrap" :class="scopeCls">
<text v-if="spec" class="shrink-0 text-hint font-semibold rounded-[6px] px-[6px] py-[2px] border-[0.5px]"
:class="specCls">
{{ spec }}
</text>
<view class="min-w-0 flex-1 line-clamp-1">{{ b.scope_desc || b.merchant_name || "" }}</view>
</view>
<!-- ④ 有效期 + 第二项随态 -->
<view class="flex items-center gap-2 mt-auto text-[10px] text-ink3">
<view v-if="validText" class="flex items-center gap-[3.5px] shrink-0 whitespace-nowrap">
<AppIcon name="clock" tone="ink3" :size="12" />
<text>{{ validText }}</text>
</view>
<view class="flex items-center gap-[3.5px] shrink-0 whitespace-nowrap"
:class="secondMeta.ok ? 'text-[#00A46C]' : ''">
<AppIcon :name="secondMeta.icon" :tone="secondMeta.ok ? 'brand' : 'ink3'" :size="12" />
<text>{{ secondMeta.text }}</text>
</view>
</view>
</view>
<!-- 右:配图区(62%/38% 撕口) -->
<view
class="relative w-[38%] min-w-[112px] shrink-0 overflow-hidden rounded-r-card border-l border-dashed border-[rgba(255,138,61,0.45)]">
<image class="absolute inset-0 w-full h-full" :class="coverImgCls" :src="b.image_url" mode="aspectFill" />
<view class="absolute inset-0 scrim-cover" />
<view
class="absolute top-[11px] right-[11px] z-[2] text-right text-[11.5px] font-bold leading-[1.5] text-white tracking-[0.6px] [rotate:-8deg] [text-shadow:0_1px_7px_rgba(0,0,0,0.5)]">
<text v-for="(line, i) in slogan" :key="i" class="block">{{ line }}</text>
</view>
<view class="absolute left-0 right-0 bottom-3 z-[2] flex justify-center">
<view class="h-[30px] px-4 rounded-full text-white text-cap font-semibold flex items-center whitespace-nowrap"
:class="ctaStateCls">
{{ ctaText }}
</view>
</view>
</view>
</view>
</template>