feat: 龙里小程序第一次上传
This commit is contained in:
commit
f23d598328
59 files changed
+11482
No files matched your search
@@ -0,0 +1,187 @@
|
||||
<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>
|
||||
<text class="min-w-0 truncate">{{ b.scope_desc || b.merchant_name || "" }}</text>
|
||||
</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>
|
||||
Reference in new issue
Block a user