diff --git a/scripts/build-icons.mjs b/scripts/build-icons.mjs index f1fb80f..60f85d6 100644 --- a/scripts/build-icons.mjs +++ b/scripts/build-icons.mjs @@ -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"], diff --git a/src/components/BenefitDialog/index.vue b/src/components/BenefitDialog/index.vue index 597d709..a6731a0 100644 --- a/src/components/BenefitDialog/index.vue +++ b/src/components/BenefitDialog/index.vue @@ -1,15 +1,24 @@ diff --git a/src/pages/benefit/index.vue b/src/pages/benefit/index.vue index e06be14..d4d04d8 100644 --- a/src/pages/benefit/index.vue +++ b/src/pages/benefit/index.vue @@ -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; + } }; @@ -202,8 +237,8 @@ const onConfirmUse = async () => { - - + + diff --git a/src/store/modules/benefit.js b/src/store/modules/benefit.js index 11b36ec..75bba59 100644 --- a/src/store/modules/benefit.js +++ b/src/store/modules/benefit.js @@ -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() { diff --git a/src/styles/icons.scss b/src/styles/icons.scss index dfc2d6b..538bc64 100644 --- a/src/styles/icons.scss +++ b/src/styles/icons.scss @@ -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==")} diff --git a/src/utils/index.js b/src/utils/index.js index d70c0a4..2bd9350 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -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, };