feat: 龙里小程序第一次上传

This commit is contained in:
zoujing committed 2026-09-16 16:52:50 +08:00
commit f23d598328
59 files changed
+11482

No files matched your search

+187
View File
@@ -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>