体验券活动优化

This commit is contained in:
zhanchao 2026-07-29 11:25:29 +08:00
parent 9b5ab9fa9c
commit f353daccaa
10 changed files with 2082 additions and 336 deletions

View File

@ -1,280 +1,321 @@
{
"pages": [
{
"path": "pages/home/home",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息",
"disableScroll": true
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/mine/mine",
"style": {
"navigationBarTitleText": "我的",
"disableScroll": true
}
},
{
"path": "pages/mine/contact",
"style": {
"navigationBarTitleText": "联系客服",
"disableScroll": true
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "健康柚",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/login/redirect-page",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/login/agreement",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/common/privacy",
"style": {
"navigationBarTitleText": "隐私政策",
"disableScroll": true
}
},
{
"path": "pages/common/agreement",
"style": {
"navigationBarTitleText": "用户协议",
"disableScroll": true
}
},
{
"path": "pages/web-view/web-view",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
}
],
"subPackages": [
{
"root": "pages/article",
"name": "article",
"pages": [
{
"path": "article-list",
"style": {
"navigationBarTitleText": "我的宣教",
"disableScroll": true
}
},
{
"path": "article-cate-list",
"style": {
"navigationBarTitleText": "健康宣教",
"disableScroll": true
}
},
{
"path": "article-detail",
"style": {
"navigationBarTitleText": "宣教文章",
"disableScroll": true
}
},
{
"path": "send-article",
"style": {
"navigationBarTitleText": "选择宣教文章",
"disableScroll": true
}
}
]
},
{
"root": "pages/survey",
"name": "survey",
"pages": [
{
"path": "survey-list",
"style": {
"navigationBarTitleText": "我的问卷",
"disableScroll": true
}
},
{
"path": "fill",
"style": {
"navigationBarTitleText": "问卷",
"disableScroll": true
}
}
]
},
{
"root": "pages/rate",
"name": "rate",
"pages": [
{
"path": "rate-list",
"style": {
"navigationBarTitleText": "服务评价",
"disableScroll": true
}
},
{
"path": "rate-detail",
"style": {
"navigationBarTitleText": "服务评价",
"disableScroll": true
}
}
]
},
{
"root": "pages/archive",
"name": "archive",
"pages": [
{
"path": "archive-manage",
"style": {
"navigationBarTitleText": "档案管理",
"disableScroll": true
}
},
{
"path": "edit-archive",
"style": {
"navigationBarTitleText": "新增档案",
"disableScroll": true
}
},
{
"path": "archive-result",
"style": {
"navigationBarTitleText": "团队服务",
"disableScroll": true
}
}
]
},
{
"root": "pages/health",
"name": "health",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
},
{
"path": "record",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
}
]
},
{
"root": "pages/library",
"name": "library",
"pages": [
{
"path": "diagnosis-list",
"style": {
"navigationBarTitleText": "选择诊断",
"disableScroll": true
}
}
]
},
{
"root": "pages/team",
"name": "team",
"pages": [
{
"path": "team-detail",
"style": {
"navigationBarTitleText": "团队介绍",
"disableScroll": true
}
},
{
"path": "homepage",
"style": {
"navigationBarTitleText": "个人主页",
"disableScroll": true
}
},
{
"path": "friend",
"style": {
"navigationBarTitleText": "添加好友",
"disableScroll": true
}
}
]
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#065bd6",
"backgroundColor": "#065bd6"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#007aff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/home/home",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_selected.png",
"text": "服务"
},
{
"pagePath": "pages/message/message",
"iconPath": "static/tabbar/consult.png",
"selectedIconPath": "static/tabbar/consult_selected.png",
"text": "咨询"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine_selected.png",
"text": "我的"
}
]
},
"uniIdRouter": {}
}
{
"pages": [
{
"path": "pages/home/home",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/message/message",
"style": {
"navigationBarTitleText": "消息",
"disableScroll": true
}
},
{
"path": "pages/message/index",
"style": {
"navigationBarTitleText": "聊天",
"enablePullDownRefresh": false,
"disableScroll": true
}
},
{
"path": "pages/mine/mine",
"style": {
"navigationBarTitleText": "我的",
"disableScroll": true
}
},
{
"path": "pages/mine/contact",
"style": {
"navigationBarTitleText": "联系客服",
"disableScroll": true
}
},
{
"path": "pages/login/login",
"style": {
"navigationBarTitleText": "健康柚",
"navigationStyle": "custom",
"disableScroll": true
}
},
{
"path": "pages/login/redirect-page",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/login/agreement",
"style": {
"navigationBarTitleText": "健康柚",
"disableScroll": true
}
},
{
"path": "pages/common/privacy",
"style": {
"navigationBarTitleText": "隐私政策",
"disableScroll": true
}
},
{
"path": "pages/common/agreement",
"style": {
"navigationBarTitleText": "用户协议",
"disableScroll": true
}
},
{
"path": "pages/web-view/web-view",
"style": {
"navigationBarTitleText": "",
"disableScroll": true
}
}
],
"subPackages": [
{
"root": "pages/article",
"name": "article",
"pages": [
{
"path": "article-list",
"style": {
"navigationBarTitleText": "我的宣教",
"disableScroll": true
}
},
{
"path": "article-cate-list",
"style": {
"navigationBarTitleText": "健康宣教",
"disableScroll": true
}
},
{
"path": "article-detail",
"style": {
"navigationBarTitleText": "宣教文章",
"disableScroll": true
}
},
{
"path": "send-article",
"style": {
"navigationBarTitleText": "选择宣教文章",
"disableScroll": true
}
}
]
},
{
"root": "pages/survey",
"name": "survey",
"pages": [
{
"path": "survey-list",
"style": {
"navigationBarTitleText": "我的问卷",
"disableScroll": true
}
},
{
"path": "fill",
"style": {
"navigationBarTitleText": "问卷",
"disableScroll": true
}
}
]
},
{
"root": "pages/rate",
"name": "rate",
"pages": [
{
"path": "rate-list",
"style": {
"navigationBarTitleText": "服务评价",
"disableScroll": true
}
},
{
"path": "rate-detail",
"style": {
"navigationBarTitleText": "服务评价",
"disableScroll": true
}
}
]
},
{
"root": "pages/experience-coupon",
"name": "experience-coupon",
"pages": [
{
"path": "claim",
"style": {
"navigationBarTitleText": "领取体验券",
"disableScroll": true
}
},
{
"path": "my-rights",
"style": {
"navigationBarTitleText": "我的权益",
"disableScroll": true
}
},
{
"path": "select-rights-archive",
"style": {
"navigationBarTitleText": "选择档案",
"disableScroll": true
}
},
{
"path": "my-coupons",
"style": {
"navigationBarTitleText": "我的体验券",
"disableScroll": true
}
},
{
"path": "wallet",
"style": {
"navigationBarTitleText": "我的卡包",
"disableScroll": true
}
}
]
},
{
"root": "pages/archive",
"name": "archive",
"pages": [
{
"path": "archive-manage",
"style": {
"navigationBarTitleText": "档案管理",
"disableScroll": true
}
},
{
"path": "edit-archive",
"style": {
"navigationBarTitleText": "新增档案",
"disableScroll": true
}
},
{
"path": "archive-result",
"style": {
"navigationBarTitleText": "团队服务",
"disableScroll": true
}
}
]
},
{
"root": "pages/health",
"name": "health",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
},
{
"path": "record",
"style": {
"navigationBarTitleText": "健康信息",
"disableScroll": true
}
}
]
},
{
"root": "pages/library",
"name": "library",
"pages": [
{
"path": "diagnosis-list",
"style": {
"navigationBarTitleText": "选择诊断",
"disableScroll": true
}
}
]
},
{
"root": "pages/team",
"name": "team",
"pages": [
{
"path": "team-detail",
"style": {
"navigationBarTitleText": "团队介绍",
"disableScroll": true
}
},
{
"path": "homepage",
"style": {
"navigationBarTitleText": "个人主页",
"disableScroll": true
}
},
{
"path": "friend",
"style": {
"navigationBarTitleText": "添加好友",
"disableScroll": true
}
}
]
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#065bd6",
"backgroundColor": "#065bd6"
},
"tabBar": {
"color": "#666666",
"selectedColor": "#007aff",
"backgroundColor": "#ffffff",
"borderStyle": "white",
"list": [
{
"pagePath": "pages/home/home",
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/home_selected.png",
"text": "服务"
},
{
"pagePath": "pages/message/message",
"iconPath": "static/tabbar/consult.png",
"selectedIconPath": "static/tabbar/consult_selected.png",
"text": "咨询"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mine_selected.png",
"text": "我的"
}
]
},
"uniIdRouter": {}
}

View File

@ -0,0 +1,358 @@
<template>
<full-page>
<view class="claim-page">
<view v-if="tip" class="tip">{{ tip }}</view>
<view v-else class="tip">加载中...</view>
</view>
<!-- 海报领取弹窗 -->
<view v-if="posterVisible" class="poster-mask">
<view class="poster-dialog">
<image
class="poster-image"
:src="issue?.posterUrl || ''"
mode="widthFix"
/>
<view class="poster-info">
<view class="poster-title">{{ issue?.activityName || "体验券" }}</view>
<view class="poster-desc">{{ projectText }}</view>
<view class="poster-desc">{{ validText }}</view>
</view>
<view class="poster-btn" :class="{ disabled: claiming }" @click="acceptCoupon">
{{ claiming ? "领取中..." : "接受" }}
</view>
</view>
</view>
</full-page>
</template>
<script setup>
import { computed, ref } from "vue";
import { storeToRefs } from "pinia";
import { onLoad, onShow } from "@dcloudio/uni-app";
import useAccount from "@/store/account";
import api from "@/utils/api";
import { confirm, hideLoading, loading, toast } from "@/utils/widget";
import { normalizeCorpId } from "@/utils/api-base-config";
import FullPage from "@/components/full-page.vue";
const { account } = storeToRefs(useAccount());
const { login } = useAccount();
const corpId = ref("");
const issueId = ref("");
const memberId = ref("");
const issue = ref(null);
const tip = ref("");
const claiming = ref(false);
const autoPrompted = ref(false);
const posterVisible = ref(false);
const projectText = computed(() => {
const projects = Array.isArray(issue.value?.projectSnapshot) ? issue.value.projectSnapshot : [];
if (!projects.length) return "未配置项目";
return projects.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、");
});
const validText = computed(() => {
const rule = issue.value?.projectValidRuleSnapshot || {};
if (issue.value?.projectExpireTime) {
return `有效期至 ${formatDate(issue.value.projectExpireTime)}`;
}
if (rule.type === "fixedDate" && rule.fixedDate) {
return `有效期至 ${formatDate(rule.fixedDate)}`;
}
if (rule.type === "daysAfterClaim") {
return `领取后 ${rule.days || 0} 天有效`;
}
return "有效期以活动规则为准";
});
function formatDate(ts) {
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 buildClaimContent() {
const name = issue.value?.activityName || "体验券";
return `您收到一张【${name}】体验券,包含${projectText.value},是否存入您的权益卡包?`;
}
async function ensureLogin() {
if (!account.value) await login();
if (!account.value) {
tip.value = "请先登录后再领取体验券";
uni.navigateTo({ url: "/pages/login/login" });
return false;
}
return true;
}
async function loadIssueDetail() {
const res = await api(
"getExperienceCouponIssueDetail",
{ corpId: corpId.value, issueId: issueId.value },
false
);
if (!res?.success || !res.data) {
tip.value = res?.message || "体验券不存在或已失效";
return false;
}
issue.value = res.data;
if (!memberId.value && res.data.customerId) {
memberId.value = res.data.customerId;
}
return true;
}
async function hasBoundArchive() {
const openid = account.value?.openid || "";
if (!openid || !corpId.value) return false;
if (memberId.value) {
try {
const detail = await api(
"getCustomerByCustomerId",
{ corpId: corpId.value, customerId: memberId.value },
false
);
const customer = detail?.data || null;
if (customer && String(customer.miniAppId || "") === String(openid)) {
return true;
}
} catch (e) {
console.warn("getCustomerByCustomerId failed", e);
}
}
try {
const countRes = await api(
"getWxAppCustomerCount",
{ miniAppId: openid, corpId: corpId.value },
false
);
return Number(countRes?.data || 0) > 0;
} catch (e) {
console.warn("getWxAppCustomerCount failed", e);
return false;
}
}
function goBindArchive() {
uni.navigateTo({
url: `/pages/archive/archive-manage?corpId=${encodeURIComponent(corpId.value || "")}`,
});
}
function isExpiredIssue() {
const expireAt = Number(issue.value?.projectExpireTime || 0);
if (expireAt && Date.now() > expireAt) return true;
const rule = issue.value?.projectValidRuleSnapshot || {};
if (rule.type === "fixedDate" && rule.fixedDate && Date.now() > Number(rule.fixedDate)) {
return true;
}
return false;
}
async function acceptCoupon() {
if (claiming.value || !issue.value) return;
try {
await confirm(buildClaimContent(), {
title: "确认接受",
confirmText: "确认接受",
cancelText: "暂不领取",
});
} catch {
return;
}
claiming.value = true;
loading("领取中...");
try {
const res = await api("claimExperienceCoupon", {
corpId: corpId.value,
issueId: issueId.value,
claimUserId: account.value?.openid || "",
});
hideLoading();
if (!res?.success) {
tip.value = res?.message || "领取失败";
toast(tip.value);
return;
}
toast("领取成功");
posterVisible.value = false;
uni.redirectTo({
url: `/pages/experience-coupon/my-rights?corpId=${encodeURIComponent(corpId.value)}&customerId=${encodeURIComponent(memberId.value || "")}&name=${encodeURIComponent(issue.value.customerName || "")}`,
});
} catch (e) {
hideLoading();
tip.value = e?.message || "领取失败";
toast(tip.value);
} finally {
claiming.value = false;
}
}
async function promptClaim() {
if (autoPrompted.value) return;
autoPrompted.value = true;
if (!issue.value) return;
if (issue.value.status === "claimed") {
tip.value = "该体验券已领取";
uni.redirectTo({
url: `/pages/experience-coupon/my-rights?corpId=${encodeURIComponent(corpId.value)}&customerId=${encodeURIComponent(memberId.value || "")}`,
});
return;
}
if (issue.value.status === "void") {
tip.value = "该体验券已作废";
return;
}
if (issue.value.status && issue.value.status !== "issued") {
tip.value = "当前状态不可领取";
return;
}
if (isExpiredIssue()) {
tip.value = "体验券已过期,无法领取";
return;
}
const bound = await hasBoundArchive();
if (!bound) {
tip.value = "请先绑定档案后再领取体验券";
try {
await confirm("请先绑定档案后再领取体验券", {
title: "提示",
confirmText: "去绑定",
cancelText: "取消",
});
goBindArchive();
} catch {
// ignore
}
return;
}
if (!issue.value.posterUrl) {
tip.value = "该体验券暂无海报,无法领取";
return;
}
tip.value = "";
posterVisible.value = true;
}
async function bootstrap() {
loading("加载中...");
try {
const okLogin = await ensureLogin();
if (!okLogin) return;
const okDetail = await loadIssueDetail();
if (!okDetail) return;
await promptClaim();
} finally {
hideLoading();
}
}
onLoad((options = {}) => {
corpId.value = normalizeCorpId(options.corpId || "");
issueId.value = options.issueId || options.id || "";
memberId.value = options.memberId || options.customerId || "";
if (!corpId.value || !issueId.value) {
tip.value = "领取参数不完整";
toast(tip.value);
return;
}
bootstrap();
});
onShow(() => {
if (issue.value && issue.value.status === "issued" && tip.value.includes("绑定")) {
autoPrompted.value = false;
promptClaim();
}
});
</script>
<style scoped>
.claim-page {
min-height: 100%;
padding: 30rpx;
box-sizing: border-box;
background: #f6fafa;
}
.tip {
margin-top: 120rpx;
text-align: center;
font-size: 28rpx;
color: #999;
}
.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;
line-height: 1.4;
}
.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>

View File

@ -0,0 +1,387 @@
<template>
<full-page pageClass="coupons-page" :customScroll="true">
<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>
<!-- 海报领取弹窗 -->
<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 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() {
if (!corpId.value || !customerId.value) {
list.value = [];
return;
}
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;
}
}
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>
.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>

View File

@ -0,0 +1,412 @@
<template>
<full-page pageClass="rights-page" :customScroll="true">
<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="!groups.length" class="empty">暂无权益</view>
<view v-else class="group-list">
<view v-for="group in groups" :key="group.key" class="group-card">
<view class="group-head">
<view class="group-badge">{{ group.badge }}</view>
<view class="group-title">{{ group.title }}</view>
</view>
<view class="table-head">
<text class="col col-name">项目名称</text>
<text class="col col-num">总次数</text>
<text class="col col-num">剩余次数</text>
<text class="col col-date">有效期至</text>
</view>
<view v-for="row in group.rows" :key="row._id" class="table-row">
<text class="col col-name">{{ row.projectName }}</text>
<text class="col col-num">{{ row.usageCount }}</text>
<text class="col col-num col-rest">{{ row.restUsageCount }}</text>
<text class="col col-date">{{ row.validText }}</text>
</view>
</view>
</view>
</view>
</full-page>
</template>
<script setup>
import { computed, 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 teams = ref([]);
const records = ref([]);
const loading = 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 pickCorpName(team = {}) {
return team.licenseHospitalName || team.leaderCorp || team.corpName || "-";
}
const groups = computed(() => {
const list = Array.isArray(records.value) ? records.value : [];
const valid = list.filter((item) => {
const status = String(item.treatmentStatus || "");
return !["void", "canceled", "cancel"].includes(status);
});
const packageMap = {};
const singleGroups = [];
valid.forEach((item) => {
const row = {
_id: item._id,
projectName: item.projectName || "-",
usageCount: Number(item.usageCount || 0),
restUsageCount: Number(item.restUsageCount || 0),
validText: formatDate(item.validTime),
};
if (item.packageId) {
const key = String(item.packageId);
if (!packageMap[key]) {
packageMap[key] = {
key: `pkg_${key}`,
badge: "套",
title: `套餐(${item.packageName || "未命名套餐"}`,
rows: [],
};
}
packageMap[key].rows.push(row);
return;
}
singleGroups.push({
key: `single_${item._id}`,
badge: "项",
title: "单项目",
rows: [row],
});
});
return [...singleGroups, ...Object.values(packageMap)];
});
function applySelection(selection = {}) {
corpId.value = normalizeCorpId(selection.corpId || corpId.value || "");
teamId.value = selection.teamId || teamId.value || "";
customerId.value = selection.customerId || selection.memberId || customerId.value || "";
customerName.value = selection.name || customerName.value || "";
if (selection.corpName) {
corpName.value = selection.corpName;
return;
}
const matched = teams.value.find(
(team) =>
normalizeCorpId(team.corpId) === corpId.value &&
(!teamId.value || team.teamId === teamId.value)
);
corpName.value = pickCorpName(matched || {});
}
async function resolveContext(options = {}) {
const cached = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
try {
teams.value = (await getTeams()) || [];
} catch (e) {
console.warn(e);
teams.value = [];
}
const incoming = {
corpId: options.corpId || corpId.value || cached.corpId || "",
teamId: options.teamId || teamId.value || cached.teamId || "",
customerId: options.customerId || options.memberId || customerId.value || "",
name: options.name ? decodeURIComponent(options.name) : customerName.value,
};
applySelection(incoming);
const matched =
teams.value.find(
(team) =>
team.teamId === teamId.value &&
normalizeCorpId(team.corpId) === corpId.value
) ||
teams.value.find((team) => team.teamId === teamId.value) ||
teams.value.find((team) => normalizeCorpId(team.corpId) === corpId.value) ||
teams.value[0] ||
null;
if (!corpId.value) corpId.value = normalizeCorpId(matched?.corpId || "");
if (!teamId.value) teamId.value = matched?.teamId || "";
if (!corpName.value) corpName.value = pickCorpName(matched || {});
if (customerId.value) return true;
const miniAppId = account.value?.openid || uni.getStorageSync("openid") || "";
if (!miniAppId || !corpId.value) 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((item) => item.relationship === "本人") || customers[0];
customerId.value = preferred?._id || "";
customerName.value = preferred?.name || customerName.value || "";
return !!customerId.value;
}
async function loadRights() {
if (!corpId.value || !customerId.value) {
records.value = [];
return;
}
loading.value = true;
try {
const res = await api(
"getTreatmentRecord",
{
corpId: corpId.value,
customerId: customerId.value,
page: 1,
pageSize: 200,
},
false
);
if (!res?.success) {
toast(res?.message || "加载权益失败");
records.value = [];
return;
}
records.value = res.list || res.data?.list || [];
} finally {
loading.value = false;
}
}
async function syncSelectedProfile() {
const selected = get(RIGHTS_SELECTION_CACHE_KEY);
if (!selected) return false;
remove(RIGHTS_SELECTION_CACHE_KEY);
applySelection(selected);
await loadRights();
return true;
}
function toSelectPage() {
const params = [
`corpId=${encodeURIComponent(corpId.value || "")}`,
`teamId=${encodeURIComponent(teamId.value || "")}`,
`customerId=${encodeURIComponent(customerId.value || "")}`,
`name=${encodeURIComponent(customerName.value || "")}`,
].join("&");
uni.navigateTo({
url: `/pages/experience-coupon/select-rights-archive?${params}`,
});
}
onLoad(async (options = {}) => {
uni.setNavigationBarTitle({ title: "我的权益" });
const ok = await resolveContext(options);
if (!ok) {
toast("请先绑定档案");
return;
}
await loadRights();
});
onShow(async () => {
const changed = await syncSelectedProfile();
if (changed) return;
if (corpId.value && customerId.value) {
await loadRights();
}
});
</script>
<style scoped>
.page-body {
min-height: 100%;
padding: 0 0 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;
}
.group-list {
padding-top: 12rpx;
}
.group-card {
margin-bottom: 16rpx;
background: #fff;
border-top: 1px solid #eceff4;
border-bottom: 1px solid #eceff4;
}
.group-head {
display: flex;
align-items: center;
padding: 18rpx 24rpx;
border-bottom: 1px solid #eceff4;
}
.group-badge {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: #2f7df6;
color: #fff;
font-size: 22rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
flex-shrink: 0;
}
.group-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.table-head,
.table-row {
display: flex;
align-items: flex-start;
padding: 18rpx 24rpx;
}
.table-head {
color: #666;
font-size: 24rpx;
font-weight: 600;
background: #f5f9fc;
border-bottom: 1px solid #eceff4;
}
.table-row {
color: #333;
font-size: 26rpx;
border-bottom: 1px solid #f1f3f6;
}
.table-row:last-child {
border-bottom: none;
}
.col {
text-align: center;
word-break: break-all;
line-height: 1.5;
}
.col-name {
flex: 1.35;
text-align: left;
padding-right: 16rpx;
}
.col-num {
flex: 0.7;
}
.col-date {
flex: 1;
}
.col-rest {
color: #ff9800;
font-weight: 600;
}
.empty {
padding: 120rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
</style>

View File

@ -0,0 +1,285 @@
<template>
<full-page pageClass="rights-selector-page" :customScroll="true">
<view class="page-body">
<view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!options.length" class="empty">暂无可用档案</view>
<view v-else class="archive-list">
<view
v-for="item in options"
:key="item.key"
class="archive-card"
:class="{ active: item.isCurrent }"
@click="selectArchive(item)"
>
<view class="archive-head">
<view class="archive-name-row">
<view class="archive-name">{{ item.name || "未命名" }}</view>
<view v-if="item.relationship" class="archive-tag">{{ item.relationship }}</view>
</view>
<view v-if="item.isCurrent" class="archive-current">当前</view>
</view>
<view class="archive-meta">{{ item.metaText }}</view>
<view class="archive-id">证件号{{ item.idCardText }}</view>
<view class="archive-corp">{{ item.corpName }}</view>
</view>
</view>
</view>
</full-page>
</template>
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccount from "@/store/account";
import api from "@/utils/api";
import { set } from "@/utils/cache";
import { toast } from "@/utils/widget";
import { normalizeCorpId } from "@/utils/api-base-config";
import FullPage from "@/components/full-page.vue";
const RIGHTS_SELECTION_CACHE_KEY = "experience-coupon-rights-selection";
const { account } = storeToRefs(useAccount());
const { getTeams } = useAccount();
const loading = ref(false);
const options = ref([]);
const currentCorpId = ref("");
const currentTeamId = ref("");
const currentCustomerId = ref("");
const mode = ref("back");
const target = ref("");
function pickCorpName(team = {}) {
return team.licenseHospitalName || team.leaderCorp || team.corpName || "-";
}
function maskMobile(mobile = "") {
const value = String(mobile || "").trim();
if (!value) return "";
if (/^\d{11}$/.test(value)) {
return value.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
}
return value;
}
function maskIdCard(idCard = "") {
const value = String(idCard || "").trim();
if (!value) return "--";
if (value.length <= 4) return value;
return `${"*".repeat(Math.max(value.length - 4, 2))}${value.slice(-4)}`;
}
function buildMetaText(customer = {}) {
const parts = [];
if (customer.age) parts.push(`${customer.age}`);
if (customer.mobile) parts.push(maskMobile(customer.mobile));
return parts.length ? parts.join("") : "暂无档案信息";
}
function dedupeByCorp(list = []) {
const map = new Map();
list.forEach((team) => {
const corpId = normalizeCorpId(team.corpId);
if (!corpId || map.has(corpId)) return;
map.set(corpId, { ...team, corpId });
});
return Array.from(map.values());
}
function buildTargetUrl(item) {
const corp = encodeURIComponent(item.corpId || "");
const team = encodeURIComponent(item.teamId || "");
const customer = encodeURIComponent(item.customerId || "");
const name = encodeURIComponent(item.name || "");
if (target.value === "coupons") {
return `/pages/experience-coupon/my-coupons?corpId=${corp}&teamId=${team}&customerId=${customer}`;
}
if (target.value === "health") {
return `/pages/health/list?teamId=${team}&corpId=${corp}&id=${customer}&name=${name}`;
}
return `/pages/experience-coupon/my-rights?corpId=${corp}&teamId=${team}&customerId=${customer}&name=${name}`;
}
async function loadArchives() {
const miniAppId = account.value?.openid || uni.getStorageSync("openid") || "";
if (!miniAppId) {
options.value = [];
return;
}
loading.value = true;
try {
const teams = (await getTeams()) || [];
const corpTeams = dedupeByCorp(teams);
const responses = await Promise.all(
corpTeams.map(async (team) => {
try {
const res = await api("getMiniAppCustomers", { miniAppId, corpId: team.corpId }, false);
const customers = res?.success && Array.isArray(res.data) ? res.data : [];
return customers.map((customer) => ({ team, customer }));
} catch (e) {
console.warn(e);
return [];
}
})
);
const list = responses
.flat()
.map(({ team, customer }) => ({
key: `${team.corpId}_${customer._id}`,
corpId: team.corpId,
teamId: team.teamId || "",
customerId: customer._id || "",
name: customer.name || "",
relationship: customer.relationship || "",
metaText: buildMetaText(customer),
idCardText: maskIdCard(customer.idCard),
corpName: pickCorpName(team),
}))
.filter((item) => item.corpId && item.customerId)
.sort((a, b) => {
const aScore =
(a.corpId === currentCorpId.value ? 4 : 0) +
(a.teamId === currentTeamId.value ? 2 : 0) +
(a.customerId === currentCustomerId.value ? 8 : 0) +
(a.relationship === "本人" ? 1 : 0);
const bScore =
(b.corpId === currentCorpId.value ? 4 : 0) +
(b.teamId === currentTeamId.value ? 2 : 0) +
(b.customerId === currentCustomerId.value ? 8 : 0) +
(b.relationship === "本人" ? 1 : 0);
return bScore - aScore;
})
.map((item) => ({
...item,
isCurrent:
item.corpId === currentCorpId.value &&
item.customerId === currentCustomerId.value,
}));
options.value = list;
} finally {
loading.value = false;
}
}
function selectArchive(item) {
if (!item) return;
if (mode.value === "target") {
uni.navigateTo({
url: buildTargetUrl(item),
});
return;
}
set(RIGHTS_SELECTION_CACHE_KEY, {
corpId: item.corpId,
teamId: item.teamId,
customerId: item.customerId,
name: item.name,
corpName: item.corpName,
});
uni.navigateBack();
}
onLoad(async (optionsData = {}) => {
uni.setNavigationBarTitle({ title: "选择档案" });
currentCorpId.value = normalizeCorpId(optionsData.corpId || "");
currentTeamId.value = optionsData.teamId || "";
currentCustomerId.value = optionsData.customerId || optionsData.memberId || "";
mode.value = optionsData.mode === "target" ? "target" : "back";
target.value = optionsData.target || "rights";
await loadArchives();
if (!options.value.length) {
toast("暂无可用档案");
}
});
</script>
<style scoped>
.page-body {
min-height: 100%;
padding: 24rpx;
box-sizing: border-box;
background: #f5f6fa;
}
.archive-card {
background: #fff;
border-radius: 18rpx;
padding: 24rpx;
margin-bottom: 20rpx;
border: 2rpx solid transparent;
box-shadow: 0 8rpx 24rpx rgba(15, 23, 42, 0.06);
}
.archive-card.active {
border-color: #065bd6;
}
.archive-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16rpx;
margin-bottom: 16rpx;
}
.archive-name-row {
min-width: 0;
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;
}
.archive-name {
font-size: 34rpx;
font-weight: 600;
color: #1f2329;
}
.archive-tag,
.archive-current {
padding: 4rpx 12rpx;
border-radius: 999rpx;
font-size: 20rpx;
}
.archive-tag {
color: #0f766e;
background: rgba(15, 118, 110, 0.12);
}
.archive-current {
color: #065bd6;
background: rgba(6, 91, 214, 0.1);
}
.archive-meta,
.archive-id {
font-size: 28rpx;
color: #4b5563;
line-height: 1.6;
}
.archive-corp {
margin-top: 16rpx;
padding-top: 16rpx;
border-top: 1px solid #eef2f6;
font-size: 28rpx;
color: #065bd6;
font-weight: 500;
}
.empty {
padding: 120rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<view class="redirect-page">跳转中...</view>
</template>
<script setup>
import { onLoad } from "@dcloudio/uni-app";
onLoad((options = {}) => {
const type = options.type === "coupon" ? "coupon" : "rights";
const params = [
options.corpId ? `corpId=${encodeURIComponent(options.corpId)}` : "",
options.teamId ? `teamId=${encodeURIComponent(options.teamId)}` : "",
options.customerId ? `customerId=${encodeURIComponent(options.customerId)}` : "",
options.memberId ? `memberId=${encodeURIComponent(options.memberId)}` : "",
options.name ? `name=${encodeURIComponent(options.name)}` : "",
]
.filter(Boolean)
.join("&");
const target =
type === "coupon"
? `/pages/experience-coupon/my-coupons?${params}`
: `/pages/experience-coupon/my-rights?${params}`;
uni.redirectTo({ url: target });
});
</script>
<style scoped>
.redirect-page {
padding: 120rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
</style>

View File

@ -147,6 +147,18 @@ onLoad((options) => {
if (options.q) {
opts.value = JSON.stringify(options)
changeTeam(parseInviteOptions(options));
} else if (options.type === 'experienceCoupon' && options.issueId && options.corpId) {
const params = [
`corpId=${encodeURIComponent(options.corpId || "")}`,
`issueId=${encodeURIComponent(options.issueId || "")}`,
options.memberId ? `memberId=${encodeURIComponent(options.memberId)}` : "",
options.couponId ? `couponId=${encodeURIComponent(options.couponId)}` : "",
options.unionid ? `unionid=${encodeURIComponent(options.unionid)}` : "",
options.externalUserId ? `externalUserId=${encodeURIComponent(options.externalUserId)}` : "",
].filter(Boolean).join("&");
uni.redirectTo({
url: `/pages/experience-coupon/claim?${params}`,
});
} else if (options.type === 'archive' || (options.teamId && options.corpId)) {
changeTeam(options);
}

View File

@ -1,9 +1,7 @@
<template>
<view class="page-container">
<!-- Blue background area for header -->
<view class="header-bg"></view>
<!-- User Card -->
<view class="user-card">
<view class="avatar-container">
<uni-icons type="person-filled" size="60" color="#cccccc" class="default-avatar"></uni-icons>
@ -14,7 +12,27 @@
</view>
</view>
<!-- Menu List -->
<view class="shortcut-bar">
<view class="shortcut-item" @click="openRightsWallet">
<view class="shortcut-icon shortcut-icon--rights">
<uni-icons type="gift" size="28" color="#065bd6"></uni-icons>
</view>
<text class="shortcut-title">我的权益</text>
</view>
<view class="shortcut-item" @click="openCouponWallet">
<view class="shortcut-icon shortcut-icon--coupon">
<uni-icons type="wallet" size="28" color="#ff8a00"></uni-icons>
</view>
<text class="shortcut-title">我的体验券</text>
</view>
<view class="shortcut-item" @click="openHealthRecord">
<view class="shortcut-icon shortcut-icon--record">
<uni-icons type="compose" size="28" color="#19be6b"></uni-icons>
</view>
<text class="shortcut-title">治疗记录</text>
</view>
</view>
<view class="menu-container">
<view class="px-15 py-12 flex items-center border-b" @click="toPage('/pages/mine/contact')">
<view class="flex-shrink-0 item-icon">
@ -53,81 +71,203 @@
</view>
</view>
</view>
<!-- <view class="menu-container">
<uni-list>
<uni-list-item title="联系客服" link to="/pages/mine/contact" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="headphones" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="隐私保护政策" link to="/pages/common/privacy" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="locked" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="用户注册协议" link to="/pages/common/agreement" clickable>
<template v-slot:header>
<view class="item-icon">
<uni-icons type="paperclip" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
<uni-list-item title="退出登录" link @click="handleLogout">
<template v-slot:header>
<view class="item-icon">
<uni-icons type="undo" size="22" color="#000"></uni-icons>
</view>
</template>
</uni-list-item>
</uni-list>
</view> -->
</view>
</template>
<script setup>
import { computed } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { computed, ref } from "vue";
import { onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import accountStore from "@/store/account";
import api from "@/utils/api";
import { get } from "@/utils/cache";
import { normalizeCorpId } from "@/utils/api-base-config";
import { toast } from "@/utils/widget";
const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info";
const store = accountStore();
const { account } = storeToRefs(store);
const { getTeams, login } = store;
const context = ref({
corpId: "",
teamId: "",
customerId: "",
customerName: "",
customers: [],
});
const resolving = ref(false);
const maskedPhone = computed(() => {
const account = uni.getStorageSync('account');
const mobile = account?.mobile || store.account?.mobile || '';
if (!mobile) return '';
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
const localAccount = uni.getStorageSync("account");
const mobile = localAccount?.mobile || account.value?.mobile || "";
if (!mobile) return "";
return mobile.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
});
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
title: "提示",
content: "确定要退出登录吗?",
success: function (res) {
if (res.confirm) {
// Clear login info
uni.removeStorageSync('account');
uni.removeStorageSync('openid');
uni.removeStorageSync("account");
uni.removeStorageSync("openid");
store.account = null;
// Redirect to login or home
uni.reLaunch({
url: '/pages/login/login'
url: "/pages/login/login",
});
}
}
},
});
};
function toPage(url) {
function pickPreferredCustomer(list = []) {
if (!Array.isArray(list) || !list.length) return null;
return list.find((item) => item.relationship === "本人") || list[0] || null;
}
async function ensureLogin() {
if (account.value?.openid) return true;
const local = uni.getStorageSync("account");
if (local?.openid) {
store.account = local;
return true;
}
const res = await login();
return !!(res && res.openid);
}
async function resolveTeam() {
const cached = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
let teams = [];
try {
teams = (await getTeams()) || [];
} catch (e) {
console.warn("getTeams failed", e);
}
const cachedCorpId = normalizeCorpId(cached.corpId || "");
const matched =
teams.find(
(item) =>
item.teamId === cached.teamId &&
(!cachedCorpId || normalizeCorpId(item.corpId || "") === cachedCorpId)
) ||
teams.find((item) => item.teamId === cached.teamId) ||
teams[0] ||
null;
return {
corpId: normalizeCorpId(matched?.corpId || cached.corpId || ""),
teamId: matched?.teamId || cached.teamId || "",
};
}
async function resolveContext({ force = false } = {}) {
if (resolving.value) return context.value;
if (!force && context.value.corpId && context.value.customerId) {
return context.value;
}
resolving.value = true;
try {
const ok = await ensureLogin();
if (!ok) {
toast("请先登录");
return null;
}
const team = await resolveTeam();
if (!team.corpId) {
toast("请先加入服务团队");
return null;
}
const miniAppId = account.value?.openid || uni.getStorageSync("openid") || "";
const res = await api(
"getMiniAppCustomers",
{ miniAppId, corpId: team.corpId },
false
);
const customers = res?.success && Array.isArray(res.data) ? res.data : [];
const preferred = pickPreferredCustomer(customers);
context.value = {
corpId: team.corpId,
teamId: team.teamId,
customerId: preferred?._id || "",
customerName: preferred
? `${preferred.name || ""}${preferred.relationship ? ` ${preferred.relationship}` : ""}`.trim()
: "",
customers,
};
return context.value;
} finally {
resolving.value = false;
}
}
function goBindArchive(ctx) {
const corpId = ctx?.corpId || "";
const teamId = ctx?.teamId || "";
uni.navigateTo({
url: url
url: `/pages/archive/archive-manage?corpId=${encodeURIComponent(corpId)}&teamId=${encodeURIComponent(teamId)}`,
});
}
function openArchiveSelector(target, ctx) {
const params = [
`mode=target`,
`target=${encodeURIComponent(target)}`,
`corpId=${encodeURIComponent(ctx?.corpId || "")}`,
`teamId=${encodeURIComponent(ctx?.teamId || "")}`,
`customerId=${encodeURIComponent(ctx?.customerId || "")}`,
`name=${encodeURIComponent(ctx?.customerName || "")}`,
].join("&");
uni.navigateTo({
url: `/pages/experience-coupon/select-rights-archive?${params}`,
});
}
async function openWithArchive(target) {
const ctx = await resolveContext({ force: true });
if (!ctx) return;
if (!ctx.customerId) {
uni.showModal({
title: "提示",
content: "请先绑定档案后再查看",
confirmText: "去绑定",
success: (res) => {
if (res.confirm) goBindArchive(ctx);
},
});
return;
}
openArchiveSelector(target, ctx);
}
async function openRightsWallet() {
await openWithArchive("rights");
}
async function openCouponWallet() {
await openWithArchive("coupons");
}
async function openHealthRecord() {
await openWithArchive("health");
}
function toPage(url) {
uni.navigateTo({ url });
}
onShow(() => {
resolveContext({ force: true });
});
</script>
<style lang="scss">
@ -184,14 +324,63 @@ page {
}
}
.shortcut-bar {
margin: 0 15px 15px;
padding: 12px 0 10px;
background-color: #fff;
border-radius: 8px;
display: flex;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.shortcut-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.shortcut-icon {
width: 48px;
height: 48px;
margin-bottom: 6px;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.shortcut-icon--rights {
background: #eef5ff;
}
.shortcut-icon--coupon {
background: #fff6e8;
}
.shortcut-icon--record {
background: #ecfff5;
}
.shortcut-title {
font-size: 14px;
line-height: 20px;
color: #333;
}
.menu-container {
background-color: #fff;
margin-top: 10px;
margin: 0 15px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
.item-icon {
margin-right: 10px;
width: 24px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
}
</style>
</style>

View File

@ -12,6 +12,26 @@ export default [
path: 'pages/login/redirect-page',
meta: { title: '柚健康' },
},
{
path: 'pages/experience-coupon/claim',
meta: { title: '领取体验券', login: true }
},
{
path: 'pages/experience-coupon/my-rights',
meta: { title: '我的权益', login: true }
},
{
path: 'pages/experience-coupon/select-rights-archive',
meta: { title: '选择档案', login: true }
},
{
path: 'pages/experience-coupon/my-coupons',
meta: { title: '我的体验券', login: true }
},
{
path: 'pages/experience-coupon/wallet',
meta: { title: '我的卡包', login: true }
},
{
path: 'pages/archive/archive-manage',
meta: { title: '档案管理', login: true }

View File

@ -50,9 +50,15 @@ const urlsConfig = {
addMedicalRecord: 'addMedicalRecord',
authCustomerToTeam: 'authCustomerToTeam', // 授权客户到团队
bindMiniAppArchive: "bindMiniAppArchive",
claimExperienceCoupon: "claimExperienceCoupon",
voidExperienceCouponIssue: "voidExperienceCouponIssue",
getCustomerByCustomerId: 'getCustomerByCustomerId',
getCustomerExperienceCoupons: "getCustomerExperienceCoupons",
getExperienceCouponIssueDetail: "getExperienceCouponIssueDetail",
getExperienceCouponIssueList: "getExperienceCouponIssueList",
getMiniAppCustomers: 'getMiniAppCustomers',
getTeamCustomers: 'getTeamCustomers',
getTreatmentRecord: "getTreatmentRecord",
getUnbindMiniAppCustomers: 'getUnbindMiniAppCustomers',
getCustomerMedicalRecord: 'getCustomerMedicalRecord',
getMedicalRecordById: 'getMedicalRecordById',