feat: 补齐权益使用成功的弹窗
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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_at,RFC3339) */
|
||||
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>
|
||||
|
||||
@@ -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_at,RFC3339) */
|
||||
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>
|
||||
|
||||
@@ -104,10 +104,11 @@ export const useBenefitStore = defineStore("benefit", {
|
||||
await this.loadAll(true);
|
||||
},
|
||||
|
||||
/** 记录一次使用:入参 user_benefit_id(已领取实例 id) */
|
||||
/** 记录一次使用:入参 user_benefit_id(已领取实例 id),返回 UsageRecord(used_at 供成功弹窗展示) */
|
||||
async use(userBenefitId) {
|
||||
await benefitApi.useBenefit(userBenefitId);
|
||||
const res = await benefitApi.useBenefit(userBenefitId);
|
||||
await this.loadAll(true);
|
||||
return res;
|
||||
},
|
||||
|
||||
reset() {
|
||||
|
||||
@@ -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==")}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user