2026-07-29 11:25:29 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<full-page pageClass="coupons-page" :customScroll="true">
|
2026-08-03 10:03:56 +08:00
|
|
|
|
<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>
|
2026-07-29 11:25:29 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
2026-08-03 10:03:56 +08:00
|
|
|
|
</scroll-view>
|
2026-07-29 11:25:29 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 海报领取弹窗 -->
|
|
|
|
|
|
<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);
|
2026-08-03 10:03:56 +08:00
|
|
|
|
const refreshing = ref(false);
|
2026-07-29 11:25:29 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 10:03:56 +08:00
|
|
|
|
async function loadCoupons(options = {}) {
|
|
|
|
|
|
const { silent = false } = options;
|
2026-07-29 11:25:29 +08:00
|
|
|
|
if (!corpId.value || !customerId.value) {
|
|
|
|
|
|
list.value = [];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-08-03 10:03:56 +08:00
|
|
|
|
if (!silent) loading.value = true;
|
2026-07-29 11:25:29 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-03 10:03:56 +08:00
|
|
|
|
async function refreshCoupons() {
|
|
|
|
|
|
if (refreshing.value) return;
|
|
|
|
|
|
refreshing.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await loadCoupons({ silent: true });
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
refreshing.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 11:25:29 +08:00
|
|
|
|
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>
|
2026-08-03 10:03:56 +08:00
|
|
|
|
.coupons-page :deep(.page-scroll) {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.coupon-scroll {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-29 11:25:29 +08:00
|
|
|
|
.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>
|