576 lines
14 KiB
Vue
Raw Normal View History

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>
2026-08-14 14:16:15 +08:00
<view v-else-if="!list.length" class="empty">暂无体验券</view>
2026-08-03 10:03:56 +08:00
<view v-else class="coupon-list">
<view
v-for="item in list"
:key="item._id"
class="coupon-card"
2026-08-14 14:16:15 +08:00
:class="item.cardClass"
2026-08-03 10:03:56 +08:00
@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">
2026-08-14 14:16:15 +08:00
<view class="coupon-title-row">
<view class="coupon-title">{{ item.activityName || "体验券" }}</view>
</view>
<view class="coupon-state-row">
<view class="coupon-status" :class="item.statusClass">
{{ item.displayStatusText }}
</view>
</view>
2026-08-05 15:43:23 +08:00
<view class="coupon-info-grid">
<view class="coupon-info-row coupon-info-row--projects">
<text class="coupon-info-label">项目</text>
<view class="coupon-project-list">
<view
v-for="project in item.projectSnapshot || []"
:key="project.projectId"
class="coupon-project-item"
>
<text>{{ project.projectName || "未命名项目" }}</text>
<text>×{{ project.usageCount || 1 }}</text>
</view>
<text v-if="!(item.projectSnapshot || []).length">未配置项目</text>
</view>
</view>
<view class="coupon-info-row">
<text class="coupon-info-label">项目有效期</text>
2026-08-14 14:16:15 +08:00
<text>{{ item.projectValidText }}</text>
</view>
<view class="coupon-info-row">
<text class="coupon-info-label">体验券有效期</text>
<text>{{ item.couponValidText }}</text>
2026-08-05 15:43:23 +08:00
</view>
<view class="coupon-info-row">
2026-08-14 14:16:15 +08:00
<text class="coupon-info-label">发送时间</text>
<text>{{ item.issueTimeText }}</text>
2026-08-05 15:43:23 +08:00
</view>
</view>
2026-08-14 14:16:15 +08:00
<view class="coupon-action" :class="{ disabled: item.displayStatus !== 'issued' }">
{{ item.actionText }}
</view>
2026-08-03 10:03:56 +08:00
</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);
2026-08-14 14:16:15 +08:00
function toTimestamp(value) {
if (!value) return 0;
const numeric = Number(value);
if (Number.isFinite(numeric)) return numeric;
const parsed = new Date(value).getTime();
return Number.isNaN(parsed) ? 0 : parsed;
}
2026-07-29 11:25:29 +08:00
function formatDate(ts) {
2026-08-14 14:16:15 +08:00
const time = toTimestamp(ts);
if (!time) return "";
const d = new Date(time);
2026-07-29 11:25:29 +08:00
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}`;
}
2026-08-14 14:16:15 +08:00
function formatDateTime(ts) {
const time = toTimestamp(ts);
if (!time) return "-";
const d = new Date(time);
if (Number.isNaN(d.getTime())) return "-";
const h = String(d.getHours()).padStart(2, "0");
const min = String(d.getMinutes()).padStart(2, "0");
return `${formatDate(time)} ${h}:${min}`;
}
2026-07-29 11:25:29 +08:00
function projectText(projects) {
const arr = Array.isArray(projects) ? projects : [];
if (!arr.length) return "未配置项目";
return arr.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、");
}
2026-08-14 14:16:15 +08:00
function buildProjectValidText(item) {
2026-07-29 11:25:29 +08:00
const rule = item.projectValidRuleSnapshot || {};
2026-08-14 14:16:15 +08:00
const projectExpireAt = toTimestamp(item.projectExpireTime);
if (projectExpireAt) return `有效期至 ${formatDate(projectExpireAt)}`;
2026-07-29 11:25:29 +08:00
if (rule.type === "fixedDate" && rule.fixedDate) {
return `有效期至 ${formatDate(rule.fixedDate)}`;
}
if (rule.type === "daysAfterClaim") {
return `领取后 ${rule.days || 0} 天有效`;
}
return "领取后按活动规则生效";
}
2026-08-14 14:16:15 +08:00
function getCouponExpireAt(item) {
return toTimestamp(item.activityEndTime || item.displayExpireTime);
2026-08-05 15:43:23 +08:00
}
2026-08-14 14:16:15 +08:00
function buildCouponValidText(item) {
const start = formatDate(item.activityStartTime) || "-";
const expireAt = getCouponExpireAt(item);
const end = formatDate(expireAt) || "-";
if (start === "-" && end === "-") return "未配置有效期";
return `${start}${end}`;
2026-07-29 11:25:29 +08:00
}
function isExpired(item) {
2026-08-14 14:16:15 +08:00
const expireAt = getCouponExpireAt(item);
2026-07-29 11:25:29 +08:00
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;
}
2026-08-14 14:16:15 +08:00
function resolveDisplayStatus(item) {
const status = item.displayStatus || item.status || "issued";
if (status === "claimed") {
return {
displayStatus: "claimed",
displayStatusText: "已领取",
actionText: "已领取",
cardClass: "coupon-card--claimed",
statusClass: "coupon-status--claimed",
};
2026-07-29 11:25:29 +08:00
}
2026-08-14 14:16:15 +08:00
if (status === "expired" || status === "void" || isExpired(item)) {
return {
displayStatus: "expired",
displayStatusText: "已过期",
actionText: "已过期",
cardClass: "coupon-card--expired",
statusClass: "coupon-status--expired",
};
}
return {
displayStatus: "issued",
displayStatusText: "待领取",
actionText: "点击查看海报领取",
cardClass: "coupon-card--issued",
statusClass: "coupon-status--issued",
};
2026-07-29 11:25:29 +08:00
}
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,
page: 1,
pageSize: 100,
},
false
);
if (!res?.success) {
toast(res?.message || "加载体验券失败");
list.value = [];
return;
}
const raw = res.list || res.data?.list || [];
2026-08-14 14:16:15 +08:00
list.value = raw
.map((item) => {
const status = resolveDisplayStatus(item);
return {
...item,
...status,
projectText: projectText(item.projectSnapshot),
projectValidText: buildProjectValidText(item),
couponValidText: buildCouponValidText(item),
issueTimeText: formatDateTime(item.issueTime || item.createTime),
};
})
.sort((a, b) => Number(b.issueTime || b.createTime || 0) - Number(a.issueTime || a.createTime || 0));
2026-07-29 11:25:29 +08:00
} 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) {
2026-08-14 14:16:15 +08:00
if (item?.displayStatus !== "issued") {
toast(item?.displayStatusText || "当前体验券不可领取");
return;
}
2026-07-29 11:25:29 +08:00
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);
}
2026-08-14 14:16:15 +08:00
.coupon-card--claimed,
.coupon-card--expired {
opacity: 0.78;
}
2026-07-29 11:25:29 +08:00
.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;
}
2026-08-14 14:16:15 +08:00
.coupon-title-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 8rpx;
}
.coupon-state-row {
display: flex;
margin-bottom: 8rpx;
}
2026-07-29 11:25:29 +08:00
.coupon-title {
2026-08-14 14:16:15 +08:00
min-width: 0;
flex: 1;
2026-07-29 11:25:29 +08:00
font-size: 30rpx;
font-weight: 600;
color: #222;
2026-08-14 14:16:15 +08:00
}
.coupon-status {
flex-shrink: 0;
padding: 4rpx 12rpx;
border-radius: 999rpx;
font-size: 22rpx;
line-height: 1.4;
}
.coupon-status--issued {
color: #ff8a00;
background: rgba(255, 138, 0, 0.12);
}
.coupon-status--claimed {
color: #0f766e;
background: rgba(15, 118, 110, 0.12);
}
.coupon-status--expired {
color: #909399;
background: #f0f2f5;
2026-07-29 11:25:29 +08:00
}
2026-08-05 15:43:23 +08:00
.coupon-info-grid {
display: flex;
flex-direction: column;
gap: 6rpx;
color: #606266;
font-size: 22rpx;
2026-07-29 11:25:29 +08:00
line-height: 1.4;
}
2026-08-05 15:43:23 +08:00
.coupon-info-row {
display: flex;
align-items: flex-start;
min-width: 0;
}
.coupon-info-label {
width: 132rpx;
flex-shrink: 0;
color: #909399;
}
.coupon-info-row > text:last-child,
.coupon-project-list {
min-width: 0;
overflow-wrap: anywhere;
}
.coupon-project-list {
flex: 1;
}
.coupon-project-item {
display: flex;
justify-content: space-between;
gap: 12rpx;
}
.coupon-project-item text:first-child {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
2026-07-29 11:25:29 +08:00
.coupon-action {
margin-top: 12rpx;
2026-08-05 15:43:23 +08:00
font-size: 22rpx;
2026-07-29 11:25:29 +08:00
color: #ff8a00;
}
2026-08-14 14:16:15 +08:00
.coupon-action.disabled {
color: #a8abb2;
}
2026-07-29 11:25:29 +08:00
.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>