feat: 补齐权益使用成功的弹窗

This commit is contained in:
zoujing
2026-09-18 15:00:00 +08:00
parent a3698add2a
commit 2f240246bc
6 changed files with 160 additions and 37 deletions

View File

@@ -77,7 +77,7 @@ const MATRIX = {
ticket: ["ink3", "brand", "accent", "white", "ink2", "accentDeep"],
user: ["ink3", "brand", "white", "ink2"],
check: ["white", "brand"],
close: ["ink3", "white"],
close: ["ink3", "white", "danger"],
doc: ["ink2", "brand"],
shield: ["ink2", "white", "brand"],
info: ["ink2", "brand", "ink3"],

View File

@@ -1,15 +1,24 @@
<script setup>
/**
* 使用确认弹窗(需求 §7.4
* 使用弹窗(需求 §7.4· 三态共用一个遮罩与卡片
* confirm 确认使用 → success 使用成功(回带权益信息 + 本次使用时间)→ fail 使用失败(仅一行说明)
* 点已领取的权益 → 弹窗确认 → 仅记录一次使用,权益保持可用(可多次使用,不失效)
*/
import { computed } from "vue";
import { BENEFIT_TYPE_LABEL } from "@/constant";
import { dateRangeText, priceText, rateText } from "@/utils";
import { dateRangeText, dotDateTime, priceText, rateText } from "@/utils";
const props = defineProps({
visible: { type: Boolean, default: false },
benefit: { type: Object, default: null },
/** confirm 确认使用 | success 使用成功 | fail 使用失败 */
state: { type: String, default: "confirm" },
/** 本次使用时间(接口 used_atRFC3339 */
usedAt: { type: String, default: "" },
/** 失败原因(接口 message缺省走统一兜底文案 */
failText: { type: String, default: "" },
/** 提交中:确认按钮防重复点击 */
submitting: { type: Boolean, default: false },
});
const emit = defineEmits(["close", "confirm"]);
@@ -42,13 +51,21 @@ const ruleText = computed(
"该权益不绑定具体商品,可多次使用。本次操作仅记录一次使用,不会使权益失效。"
);
/** 弹窗四行对照label + 值 */
/** 确认弹窗四行对照label + 值 */
const rows = computed(() => [
{ label: "优惠价格", value: priceLine.value, accent: true },
{ label: "适用规格", value: b.value.price?.spec || "—" },
{ label: "适用门店", value: b.value.merchant_name || b.value.scope_desc || "—" },
{ label: "有效期", value: validLine.value || "—" },
]);
/** 成功弹窗信息块:权益名 + 四行 */
const successRows = computed(() => [
{ label: "优惠价格", value: priceLine.value || "—" },
{ label: "适用规格", value: b.value.price?.spec || "—" },
{ label: "适用门店", value: b.value.merchant_name || b.value.scope_desc || "—" },
{ label: "本次使用", value: dotDateTime(props.usedAt) || "—" },
]);
</script>
<template>
@@ -57,37 +74,96 @@ const rows = computed(() => [
class="fixed inset-0 z-[200] flex items-center justify-center px-[34px] bg-[rgba(3,20,15,0.52)] backdrop-blur-[7px]"
@click="emit('close')"
>
<view class="w-full bg-white rounded-dialog pt-6 px-[22px] pb-[18px] text-center shadow-dialog" @click.stop>
<view class="w-[52px] h-[52px] rounded-full mx-auto flex items-center justify-center grad-brand shadow-glowbrand">
<AppIcon name="check" tone="white" :size="22" />
</view>
<view class="mt-[14px] text-title font-semibold text-ink">确认使用该权益</view>
<view class="mt-[14px] p-3 text-left rounded-[10px] border-[0.5px] border-line2 grad-brand-soft">
<view class="text-[13.5px] font-semibold text-ink">{{ typeLabel }}</view>
<view v-for="row in rows" :key="row.label" class="flex justify-between gap-3 mt-[5px] text-[11.5px] text-ink2">
<text>{{ row.label }}</text>
<text class="text-right" :class="row.accent ? 'font-semibold text-accent' : ''">{{ row.value }}</text>
<view
class="w-full bg-white rounded-dialog shadow-dialog text-center"
:class="state === 'fail' ? 'pt-6 px-5 pb-[18px]' : 'pt-6 px-[22px] pb-[18px]'"
@click.stop
>
<!-- · 确认使用 -->
<template v-if="state === 'confirm'">
<view class="w-[52px] h-[52px] rounded-full mx-auto flex items-center justify-center grad-brand shadow-glowbrand">
<AppIcon name="check" tone="white" :size="22" />
</view>
</view>
<view class="mt-3 text-[11px] leading-[1.7] text-ink3 text-left">{{ ruleText }}</view>
<view class="mt-[14px] text-title font-semibold text-ink">确认使用该权益</view>
<view class="flex gap-[10px] mt-[18px]">
<view class="mt-[14px] p-3 text-left rounded-[10px] border-[0.5px] border-line2 grad-brand-soft">
<view class="text-[13.5px] font-semibold text-ink">{{ typeLabel }}</view>
<view v-for="row in rows" :key="row.label" class="flex justify-between gap-3 mt-[5px] text-[11.5px] text-ink2">
<text>{{ row.label }}</text>
<text class="text-right" :class="row.accent ? 'font-semibold text-accent' : ''">{{ row.value }}</text>
</view>
</view>
<view class="mt-3 text-[11px] leading-[1.7] text-ink3 text-left">{{ ruleText }}</view>
<view class="flex gap-[10px] mt-[18px]">
<view
class="flex-1 h-11 rounded-[16px] flex items-center justify-center text-body bg-alt text-ink2 font-normal"
@click="emit('close')"
>
再想想
</view>
<view
class="flex-1 h-11 rounded-[16px] flex items-center justify-center text-body font-semibold grad-brand text-white shadow-glowbrand"
@click="emit('confirm')"
>
{{ submitting ? "使用中…" : "确认使用" }}
</view>
</view>
</template>
<!-- · 使用成功 -->
<template v-else-if="state === 'success'">
<view
class="flex-1 h-11 rounded-[16px] flex items-center justify-center text-body bg-alt text-ink2 font-normal"
@click="emit('close')"
class="w-[60px] h-[60px] rounded-full mx-auto flex items-center justify-center grad-brand shadow-[0_0_0_9px_rgba(0,181,120,0.075),0_12px_28px_rgba(0,181,120,0.3)]"
>
再想想
<AppIcon name="check" tone="white" :size="27" />
</view>
<view
class="flex-1 h-11 rounded-[16px] flex items-center justify-center text-body font-semibold grad-brand shadow-glowbrand"
@click="emit('confirm')"
>
确认使用
<view class="mt-4 text-[19px] font-semibold text-ink">使用成功</view>
<view class="mt-[7px] text-[12px] leading-[1.65] text-ink2">该权益仍可继续使用</view>
<view class="mt-4 p-[13px] pb-3 text-left rounded-[10px] border-[0.5px] border-line2 grad-brand-soft">
<view class="text-[13.5px] font-semibold text-ink">{{ typeLabel }}</view>
<view
v-for="row in successRows"
:key="row.label"
class="flex justify-between gap-3 mt-[7px] text-[11.5px] text-ink2"
>
<text>{{ row.label }}</text>
<text class="text-right font-semibold text-ink">{{ row.value }}</text>
</view>
</view>
</view>
<view class="flex mt-4">
<view
class="flex-1 h-[46px] rounded-[16px] flex items-center justify-center text-body font-semibold grad-brand text-white shadow-glowbrand"
@click="emit('close')"
>
知道了
</view>
</view>
</template>
<!-- · 使用失败 -->
<template v-else>
<view class="w-11 h-11 rounded-full mx-auto flex items-center justify-center bg-[rgba(229,72,77,0.09)]">
<AppIcon name="close" tone="danger" :size="20" />
</view>
<view class="mt-3 text-[15.5px] font-semibold text-ink">使用失败</view>
<view class="mt-[7px] text-[11.5px] leading-[1.7] text-ink3">{{ failText || "网络异常,请稍后重试" }}</view>
<view class="flex mt-4">
<view
class="flex-1 h-[42px] rounded-[16px] flex items-center justify-center text-[14px] font-medium bg-alt text-ink2"
@click="emit('close')"
>
知道了
</view>
</view>
</template>
</view>
</view>
</template>

View File

@@ -38,6 +38,14 @@ const store = useBenefitStore();
const seg = ref("center");
const dialogVisible = ref(false);
const activeBenefit = ref(null);
/** 使用弹窗三态confirm 确认使用 → success 成功(带权益信息)/ fail 失败(仅一行说明) */
const dialogState = ref("confirm");
/** 本次使用时间(接口 used_atRFC3339 */
const usedAt = ref("");
/** 失败说明(接口业务 message为空走统一兜底文案 */
const failText = ref("");
/** 提交中:确认按钮防重复点击 */
const submitting = ref(false);
const centerList = computed(() => store.filteredCenter);
const mineList = computed(() => store.filteredMine);
@@ -77,6 +85,24 @@ const onClaim = async (benefit) => {
}, 600);
};
/** 接口错误 → 失败说明:只有后端给了业务 message 才用它,其余走兜底文案 */
const failMessage = (e) => {
const d = (e && e.data) || {};
return d.message || d.msg || (d.error && d.error.message) || "";
};
const resetDialog = () => {
dialogState.value = "confirm";
usedAt.value = "";
failText.value = "";
submitting.value = false;
};
const closeDialog = () => {
dialogVisible.value = false;
resetDialog();
};
/** 我的权益:点击卡片 → 使用确认弹窗(能否使用以接口 can_use 为准) */
const onCardTap = (benefit) => {
if (benefit.can_use === false) {
@@ -84,6 +110,7 @@ const onCardTap = (benefit) => {
return;
}
if (!requireLogin()) return;
resetDialog();
activeBenefit.value = benefit;
dialogVisible.value = true;
};
@@ -91,10 +118,18 @@ const onCardTap = (benefit) => {
/** 确认使用:入参是 user_benefit_id已领取实例 id仅记录一次权益保持可用 */
const onConfirmUse = async () => {
const target = activeBenefit.value;
dialogVisible.value = false;
if (!target) return;
await store.use(target.user_benefit_id || target.id);
uni.showToast({ title: "已记录本次使用", icon: "none" });
if (!target || submitting.value) return;
submitting.value = true;
try {
const res = await store.use(target.user_benefit_id || target.id);
usedAt.value = (res && res.used_at) || new Date().toISOString();
dialogState.value = "success";
} catch (e) {
failText.value = failMessage(e);
dialogState.value = "fail";
} finally {
submitting.value = false;
}
};
</script>
@@ -202,8 +237,8 @@ const onConfirmUse = async () => {
</scroll-view>
<AppTabBar active="benefit" />
<!-- 使用确认弹窗 -->
<BenefitDialog :visible="dialogVisible" :benefit="activeBenefit" @close="dialogVisible = false"
@confirm="onConfirmUse" />
<!-- 使用弹窗确认 / 成功 / 失败 三态 -->
<BenefitDialog :visible="dialogVisible" :benefit="activeBenefit" :state="dialogState" :used-at="usedAt"
:fail-text="failText" :submitting="submitting" @close="closeDialog" @confirm="onConfirmUse" />
</view>
</template>

View File

@@ -104,10 +104,11 @@ export const useBenefitStore = defineStore("benefit", {
await this.loadAll(true);
},
/** 记录一次使用:入参 user_benefit_id已领取实例 id */
/** 记录一次使用:入参 user_benefit_id已领取实例 id,返回 UsageRecordused_at 供成功弹窗展示) */
async use(userBenefitId) {
await benefitApi.useBenefit(userBenefitId);
const res = await benefitApi.useBenefit(userBenefitId);
await this.loadAll(true);
return res;
},
reset() {

View File

@@ -39,6 +39,7 @@
.ai-check.t-brand{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDBCNTc4JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNNS41IDEyLjYgMTAgMTdsOC41LTkiLz48L3N2Zz4=")}
.ai-close.t-ink3{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjOUNBM0FGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNNi41IDYuNWwxMSAxMU0xNy41IDYuNWwtMTEgMTEiLz48L3N2Zz4=")}
.ai-close.t-white{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRkZGRkZGJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNNi41IDYuNWwxMSAxMU0xNy41IDYuNWwtMTEgMTEiLz48L3N2Zz4=")}
.ai-close.t-danger{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjRTU0ODREJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNNi41IDYuNWwxMSAxMU0xNy41IDYuNWwtMTEgMTEiLz48L3N2Zz4=")}
.ai-doc.t-ink2{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjNkI3MjgwJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNNi41IDMuNWg3LjVsNC41IDQuNXYxMi41aC0xMnoiLz48cGF0aCBkPSJNMTQgMy41VjhoNC41Ii8+PHBhdGggZD0iTTkgMTNoNk05IDE2LjVoNC41Ii8+PC9zdmc+")}
.ai-doc.t-brand{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjMDBCNTc4JyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNNi41IDMuNWg3LjVsNC41IDQuNXYxMi41aC0xMnoiLz48cGF0aCBkPSJNMTQgMy41VjhoNC41Ii8+PHBhdGggZD0iTTkgMTNoNk05IDE2LjVoNC41Ii8+PC9zdmc+")}
.ai-shield.t-ink2{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAyNCAyNCcgZmlsbD0nbm9uZScgc3Ryb2tlPScjNkI3MjgwJyBzdHJva2Utd2lkdGg9JzEuOScgc3Ryb2tlLWxpbmVjYXA9J3JvdW5kJyBzdHJva2UtbGluZWpvaW49J3JvdW5kJz48cGF0aCBkPSJNMTIgMy41IDE5LjUgNnY1LjZjMCA0LjYtMy42IDcuNi03LjUgOC45LTMuOS0xLjMtNy41LTQuMy03LjUtOC45VjZ6Ii8+PHBhdGggZD0iTTkuMiAxMi4ybDIuMiAyLjIgMy42LTMuNiIvPjwvc3ZnPg==")}

View File

@@ -72,6 +72,15 @@ export const uid = () =>
/** 2026-09-12 / 2026-09-12T02:00:00Z → 2026.09.12(接口时间为 RFC3339 UTC只取日期段 */
export const dotDate = (str) => String(str || "").slice(0, 10).replace(/-/g, ".");
/** 使用时间文案RFC3339 → 2026.09.18 14:32接口为 UTC按设备时区还原 */
export const dotDateTime = (str) => {
if (!str) return "";
const d = new Date(String(str));
if (Number.isNaN(d.getTime())) return dotDate(str);
const p = (n) => String(n).padStart(2, "0");
return `${d.getFullYear()}.${p(d.getMonth() + 1)}.${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
};
/** 有效期区间文案2026.09.12 - 09.18 */
export const dateRangeText = (from, to) => {
if (!from && !to) return "";
@@ -93,5 +102,6 @@ export default {
cnDate,
uid,
dotDate,
dotDateTime,
dateRangeText,
};