2026-09-02 14:50:14 +08:00

664 lines
17 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 class="user-card" @click="toSelectPage">
<view class="avatar">
<uni-icons type="person-filled" size="36" color="#c0c4cc" />
</view>
<view class="user-main">
<view class="user-name">{{ customerName || "未选择档案" }}</view>
<view class="user-corp">所属机构{{ corpName || "-" }}</view>
</view>
<uni-icons type="right" size="16" color="#c0c4cc" />
</view>
<view class="summary">
以下为<text class="summary-name">{{ customerName || "-" }}</text>的所有剩余权益
</view>
<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" :class="item.cardClass"
@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-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>
<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>
<text>{{ item.projectValidText }}</text>
</view>
<view class="coupon-info-row">
<text class="coupon-info-label">体验券有效期</text>
<text>{{ item.couponValidText }}</text>
</view>
<view class="coupon-info-row">
<text class="coupon-info-label">发送时间</text>
<text>{{ item.issueTimeText }}</text>
</view>
</view>
<view class="coupon-action" :class="{ disabled: item.displayStatus !== 'issued' }">
{{ item.actionText }}
</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, remove } 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 RIGHTS_SELECTION_CACHE_KEY = "experience-coupon-rights-selection";
const { account } = storeToRefs(useAccount());
const { getTeams } = useAccount();
const corpId = ref("");
const teamId = ref("");
const customerId = ref("");
const customerName = ref("");
const corpName = ref("");
const list = ref([]);
const loading = ref(false);
const refreshing = ref(false);
const posterVisible = ref(false);
const current = ref(null);
const claiming = ref(false);
async function loadCorpName() {
if (!corpId.value) {
corpName.value = "";
return;
}
const res = await api("getCorpInfo", { corpId: corpId.value }, false);
const corp = Array.isArray(res?.data) ? res.data[0] : null;
corpName.value = corp?.corp_name || corp?.corpName || "";
}
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;
}
function formatDate(ts) {
const time = toTimestamp(ts);
if (!time) return "";
const d = new Date(time);
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 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}`;
}
function projectText(projects) {
const arr = Array.isArray(projects) ? projects : [];
if (!arr.length) return "未配置项目";
return arr.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、");
}
function buildProjectValidText(item) {
const rule = item.projectValidRuleSnapshot || {};
const projectExpireAt = toTimestamp(item.projectExpireTime);
if (projectExpireAt) return `有效期至 ${formatDate(projectExpireAt)}`;
if (rule.type === "fixedDate" && rule.fixedDate) {
return `有效期至 ${formatDate(rule.fixedDate)}`;
}
if (rule.type === "daysAfterClaim") {
return `领取后 ${rule.days || 0} 天有效`;
}
return "领取后按活动规则生效";
}
function getCouponExpireAt(item) {
return toTimestamp(item.activityEndTime || item.displayExpireTime);
}
function buildCouponValidText(item) {
const start = formatDate(item.activityStartTime) || "-";
const expireAt = getCouponExpireAt(item);
const end = formatDate(expireAt) || "-";
if (start === "-" && end === "-") return "未配置有效期";
return `${start}${end}`;
}
function isExpired(item) {
const expireAt = getCouponExpireAt(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 || "";
customerName.value = options.name ? decodeURIComponent(options.name) : customerName.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;
await loadCorpName();
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 || "";
customerName.value = preferred?.name || customerName.value || "";
return !!customerId.value;
}
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",
};
}
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",
};
}
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,
page: 1,
pageSize: 100,
},
false
);
if (!res?.success) {
toast(res?.message || "加载体验券失败");
list.value = [];
return;
}
const raw = res.list || res.data?.list || [];
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));
} finally {
loading.value = false;
}
}
async function refreshCoupons() {
if (refreshing.value) return;
refreshing.value = true;
try {
await loadCoupons({ silent: true });
} finally {
refreshing.value = false;
}
}
async function syncSelectedProfile() {
const selected = get(RIGHTS_SELECTION_CACHE_KEY);
if (!selected) return false;
remove(RIGHTS_SELECTION_CACHE_KEY);
corpId.value = normalizeCorpId(selected.corpId || corpId.value || "");
teamId.value = selected.teamId || teamId.value || "";
customerId.value = selected.customerId || selected.memberId || customerId.value || "";
customerName.value = selected.name || customerName.value || "";
await loadCorpName();
await loadCoupons();
return true;
}
function toSelectPage() {
const params = [
`corpId=${encodeURIComponent(corpId.value || "")}`,
`teamId=${encodeURIComponent(teamId.value || "")}`,
`customerId=${encodeURIComponent(customerId.value || "")}`,
`mode=back`,
`target=coupons`,
].join("&");
uni.navigateTo({
url: `/pages/experience-coupon/select-rights-archive?${params}`,
});
}
function openPoster(item) {
if (item?.displayStatus !== "issued") {
toast(item?.displayStatusText || "当前体验券不可领取");
return;
}
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 () => {
const changed = await syncSelectedProfile();
if (changed) return;
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;
}
.user-card {
display: flex;
align-items: center;
background: #fff;
border-radius: 0;
padding: 24rpx 30rpx;
border-bottom: 1px solid #eceff4;
}
.avatar {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #f0f2f5;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
flex-shrink: 0;
}
.user-main {
min-width: 0;
flex: 1;
}
.user-name {
font-size: 32rpx;
font-weight: 600;
color: #222;
margin-bottom: 8rpx;
}
.user-corp {
font-size: 24rpx;
color: #888;
}
.summary {
padding: 18rpx 30rpx;
font-size: 26rpx;
color: #666;
background: #f0f2f5;
border-top: 1px solid #eceff4;
border-bottom: 1px solid #eceff4;
}
.summary-name {
color: #065bd6;
font-weight: 600;
}
.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-card--claimed,
.coupon-card--expired {
opacity: 0.78;
}
.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-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 8rpx;
}
.coupon-state-row {
display: flex;
margin-bottom: 8rpx;
}
.coupon-title {
min-width: 0;
flex: 1;
font-size: 30rpx;
font-weight: 600;
color: #222;
}
.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;
}
.coupon-info-grid {
display: flex;
flex-direction: column;
gap: 6rpx;
color: #606266;
font-size: 22rpx;
line-height: 1.4;
}
.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;
}
.coupon-action {
margin-top: 12rpx;
font-size: 22rpx;
color: #ff8a00;
}
.coupon-action.disabled {
color: #a8abb2;
}
.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>