2026-08-03 10:03:56 +08:00

417 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<full-page pageClass="coupons-page" :customScroll="true">
<scroll-view
class="coupon-scroll"
scroll-y="true"
refresher-enabled="true"
:refresher-triggered="refreshing"
@refresherrefresh="refreshCoupons"
>
<view class="page-body">
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无待领取体验券</view>
<view v-else class="coupon-list">
<view
v-for="item in list"
:key="item._id"
class="coupon-card"
@click="openPoster(item)"
>
<image
v-if="item.posterUrl"
class="coupon-poster"
:src="item.posterUrl"
mode="aspectFill"
/>
<view v-else class="coupon-poster coupon-poster--empty">体验券</view>
<view class="coupon-body">
<view class="coupon-title">{{ item.activityName || "体验券" }}</view>
<view class="coupon-meta">{{ item.projectText }}</view>
<view class="coupon-meta">{{ item.validText }}</view>
<view class="coupon-action">点击查看海报领取</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 海报领取弹窗 -->
<view v-if="posterVisible" class="poster-mask" @click="closePoster">
<view class="poster-dialog" @click.stop>
<image
class="poster-image"
:src="current?.posterUrl || ''"
mode="widthFix"
/>
<view class="poster-info">
<view class="poster-title">{{ current?.activityName || "体验券" }}</view>
<view class="poster-desc">{{ current?.projectText }}</view>
</view>
<view class="poster-btn" :class="{ disabled: claiming }" @click="acceptCoupon">
{{ claiming ? "领取中..." : "接受" }}
</view>
</view>
</view>
</full-page>
</template>
<script setup>
import { ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccount from "@/store/account";
import api from "@/utils/api";
import { get } from "@/utils/cache";
import { toast } from "@/utils/widget";
import { normalizeCorpId } from "@/utils/api-base-config";
import FullPage from "@/components/full-page.vue";
const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info";
const { account } = storeToRefs(useAccount());
const { getTeams } = useAccount();
const corpId = ref("");
const teamId = ref("");
const customerId = ref("");
const list = ref([]);
const loading = ref(false);
const refreshing = ref(false);
const posterVisible = ref(false);
const current = ref(null);
const claiming = ref(false);
function formatDate(ts) {
if (!ts) return "";
const d = new Date(Number(ts));
if (Number.isNaN(d.getTime())) return "";
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function projectText(projects) {
const arr = Array.isArray(projects) ? projects : [];
if (!arr.length) return "未配置项目";
return arr.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、");
}
function buildValidText(item) {
const rule = item.projectValidRuleSnapshot || {};
if (item.projectExpireTime) return `有效期至 ${formatDate(item.projectExpireTime)}`;
if (rule.type === "fixedDate" && rule.fixedDate) {
return `有效期至 ${formatDate(rule.fixedDate)}`;
}
if (rule.type === "daysAfterClaim") {
return `领取后 ${rule.days || 0} 天有效`;
}
return "领取后按活动规则生效";
}
function getExpireAt(item) {
if (item.projectExpireTime) return Number(item.projectExpireTime);
const rule = item.projectValidRuleSnapshot || {};
if (rule.type === "fixedDate" && rule.fixedDate) {
return Number(rule.fixedDate);
}
return 0;
}
function isExpired(item) {
const expireAt = getExpireAt(item);
if (!expireAt) return false;
return Date.now() > expireAt;
}
async function resolveContext(options = {}) {
corpId.value = normalizeCorpId(options.corpId || corpId.value || "");
teamId.value = options.teamId || teamId.value || "";
customerId.value = options.customerId || options.memberId || customerId.value || "";
const cached = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
let teams = [];
try {
teams = (await getTeams()) || [];
} catch (e) {
console.warn(e);
}
const matched =
teams.find((t) => t.teamId === (teamId.value || cached.teamId)) ||
teams.find((t) => normalizeCorpId(t.corpId) === corpId.value) ||
teams[0] ||
null;
if (!corpId.value) corpId.value = normalizeCorpId(matched?.corpId || cached.corpId || "");
if (!teamId.value) teamId.value = matched?.teamId || cached.teamId || "";
if (!corpId.value) return false;
if (customerId.value) return true;
const miniAppId = account.value?.openid || uni.getStorageSync("openid") || "";
if (!miniAppId) return false;
const res = await api("getMiniAppCustomers", { miniAppId, corpId: corpId.value }, false);
const customers = res?.success && Array.isArray(res.data) ? res.data : [];
const preferred = customers.find((c) => c.relationship === "本人") || customers[0];
customerId.value = preferred?._id || "";
return !!customerId.value;
}
async function voidExpired(item) {
try {
await api(
"voidExperienceCouponIssue",
{
corpId: corpId.value,
issueId: item._id,
voidReason: "有效期内未领取,自动失效",
},
false
);
} catch (e) {
console.warn("auto void failed", e);
}
}
async function loadCoupons(options = {}) {
const { silent = false } = options;
if (!corpId.value || !customerId.value) {
list.value = [];
return;
}
if (!silent) loading.value = true;
try {
const res = await api(
"getCustomerExperienceCoupons",
{
corpId: corpId.value,
customerId: customerId.value,
status: "issued",
page: 1,
pageSize: 100,
},
false
);
if (!res?.success) {
toast(res?.message || "加载体验券失败");
list.value = [];
return;
}
const raw = res.list || res.data?.list || [];
const valid = [];
for (const item of raw) {
if (isExpired(item)) {
voidExpired(item);
continue;
}
valid.push({
...item,
projectText: projectText(item.projectSnapshot),
validText: buildValidText(item),
});
}
list.value = valid;
} finally {
loading.value = false;
}
}
async function refreshCoupons() {
if (refreshing.value) return;
refreshing.value = true;
try {
await loadCoupons({ silent: true });
} finally {
refreshing.value = false;
}
}
function openPoster(item) {
if (!item?.posterUrl) {
toast("该体验券暂无海报");
return;
}
current.value = item;
posterVisible.value = true;
}
function closePoster() {
if (claiming.value) return;
posterVisible.value = false;
current.value = null;
}
async function acceptCoupon() {
if (!current.value || claiming.value) return;
claiming.value = true;
try {
const res = await api("claimExperienceCoupon", {
corpId: corpId.value,
issueId: current.value._id,
claimUserId: account.value?.openid || "",
});
if (!res?.success) {
toast(res?.message || "领取失败");
return;
}
toast("领取成功");
posterVisible.value = false;
uni.redirectTo({
url: `/pages/experience-coupon/my-rights?corpId=${encodeURIComponent(corpId.value)}&teamId=${encodeURIComponent(teamId.value)}&customerId=${encodeURIComponent(customerId.value)}&name=${encodeURIComponent(current.value.customerName || "")}`,
});
} catch (e) {
toast(e?.message || "领取失败");
} finally {
claiming.value = false;
}
}
onLoad(async (options = {}) => {
uni.setNavigationBarTitle({ title: "我的体验券" });
const ok = await resolveContext(options);
if (!ok) {
toast("请先绑定档案");
return;
}
await loadCoupons();
});
onShow(async () => {
if (corpId.value && customerId.value) {
await loadCoupons();
}
});
</script>
<style scoped>
.coupons-page :deep(.page-scroll) {
overflow: hidden;
}
.coupon-scroll {
height: 100%;
background: #f5f5f5;
}
.page-body {
min-height: 100%;
padding: 24rpx 30rpx 40rpx;
box-sizing: border-box;
background: #f5f5f5;
}
.coupon-card {
display: flex;
gap: 20rpx;
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06);
}
.coupon-poster {
width: 160rpx;
height: 160rpx;
border-radius: 12rpx;
background: #f5f7fa;
flex-shrink: 0;
}
.coupon-poster--empty {
display: flex;
align-items: center;
justify-content: center;
color: #c0c4cc;
font-size: 24rpx;
}
.coupon-body {
min-width: 0;
flex: 1;
}
.coupon-title {
font-size: 30rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
}
.coupon-meta {
font-size: 24rpx;
color: #888;
line-height: 1.4;
}
.coupon-action {
margin-top: 12rpx;
font-size: 24rpx;
color: #ff8a00;
}
.empty {
padding: 120rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
.poster-mask {
position: fixed;
inset: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.65);
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
box-sizing: border-box;
}
.poster-dialog {
width: 100%;
max-width: 620rpx;
background: #fff;
border-radius: 20rpx;
overflow: hidden;
}
.poster-image {
width: 100%;
display: block;
background: #f5f7fa;
}
.poster-info {
padding: 24rpx 28rpx 8rpx;
}
.poster-title {
font-size: 30rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
}
.poster-desc {
font-size: 24rpx;
color: #888;
}
.poster-btn {
margin: 24rpx 28rpx 32rpx;
height: 80rpx;
border-radius: 40rpx;
background: #065bd6;
color: #fff;
font-size: 30rpx;
display: flex;
align-items: center;
justify-content: center;
}
.poster-btn.disabled {
opacity: 0.6;
}
</style>