Compare commits

..

No commits in common. "dev-ticket" and "main" have entirely different histories.

21 changed files with 305 additions and 1312 deletions

View File

@ -1,47 +0,0 @@
<template>
<common-cell :name="name" :required="required">
<view class="joint-row">
<picker mode="selector" :range="jointOptions" :disabled="disableChange" @change="changeJoint">
<view class="joint-picker" :class="jointType ? '' : 'form__placeholder'">{{ jointType || '请选择关节类型' }}</view>
</picker>
<picker mode="selector" :range="jointSideOptions" :disabled="disableChange" @change="changeSide">
<view class="joint-picker" :class="jointSide ? '' : 'form__placeholder'">{{ jointSide || '请选择侧别' }}</view>
</picker>
</view>
</common-cell>
</template>
<script setup>
import { computed } from 'vue';
import commonCell from '../common-cell.vue';
const emits = defineEmits(['change']);
const props = defineProps({
form: { type: Object, default: () => ({}) },
name: { default: '' },
title: { default: '' },
range: { type: Array, default: () => [] },
required: { type: Boolean, default: false },
disableChange: { type: Boolean, default: false },
});
const jointOptions = computed(() => props.range.length ? props.range : ['膝关节', '髋关节']);
const jointSideOptions = ['左侧', '右侧', '双侧'];
const jointType = computed(() => props.form?.[props.title] || '');
const jointSide = computed(() => props.form?.jointSide || '');
function changeJoint(event) {
emits('change', { title: props.title, value: jointOptions.value[event.detail.value] });
}
function changeSide(event) {
emits('change', { title: 'jointSide', value: jointSideOptions[event.detail.value] });
}
</script>
<style>
@import '../cell-style.css';
.joint-row { display: flex; gap: 16rpx; width: 100%; }
.joint-row picker { flex: 1; min-width: 0; }
.joint-picker { padding: 12rpx 16rpx; border: 1rpx solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; }
</style>

View File

@ -1,48 +0,0 @@
<template>
<common-cell :name="name" :required="required">
<view class="exercise-row">
<input class="exercise-input" type="number" :disabled="disableChange" :value="frequency" placeholder="次数" @input="changeFrequency" />
<text class="exercise-unit">次/周</text>
<input class="exercise-input" type="number" :disabled="disableChange" :value="duration" placeholder="时长" @input="changeDuration" />
<text class="exercise-unit">min/次</text>
</view>
</common-cell>
</template>
<script setup>
import { computed } from 'vue';
import commonCell from '../common-cell.vue';
const emits = defineEmits(['change']);
const props = defineProps({
form: { type: Object, default: () => ({}) },
name: { default: '' },
title: { default: '' },
required: { type: Boolean, default: false },
disableChange: { type: Boolean, default: false },
});
const values = computed(() => {
const matched = String(props.form?.[props.title] || '').match(/^\s*(\d*)\s*次\/周\s*[;;]\s*(\d*)\s*min\/次\s*$/);
return matched ? { frequency: matched[1], duration: matched[2] } : { frequency: '', duration: '' };
});
const frequency = computed(() => values.value.frequency);
const duration = computed(() => values.value.duration);
function emitValue(nextFrequency, nextDuration) {
emits('change', {
title: props.title,
value: !nextFrequency && !nextDuration ? '' : `${nextFrequency || ''}次/周;${nextDuration || ''}min/次`,
});
}
function changeFrequency(event) { emitValue(event.detail.value, duration.value); }
function changeDuration(event) { emitValue(frequency.value, event.detail.value); }
</script>
<style>
@import '../cell-style.css';
.exercise-row { display: flex; align-items: center; gap: 10rpx; white-space: nowrap; }
.exercise-input { width: 96rpx; min-width: 96rpx; padding: 8rpx 10rpx; border: 1rpx solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; }
.exercise-unit { flex-shrink: 0; font-size: 26rpx; color: #303133; }
</style>

View File

@ -1,58 +0,0 @@
<template>
<common-cell :name="name" :required="required">
<view class="select-other">
<picker mode="selector" :range="range" :disabled="disableChange" @change="changeSelect">
<view class="select-other__picker" :class="selectedValue ? '' : 'form__placeholder'">{{ selectedValue || '请选择' }}</view>
</picker>
<input v-if="showOther" class="select-other__input" :disabled="disableChange" :value="otherValue" placeholder="请填写其他原因" @input="changeOther" />
</view>
</common-cell>
</template>
<script setup>
import { computed, ref, watch } from 'vue';
import commonCell from '../common-cell.vue';
const emits = defineEmits(['change']);
const props = defineProps({
form: { type: Object, default: () => ({}) },
name: { default: '' },
title: { default: '' },
range: { type: Array, default: () => [] },
otherFiled: { default: '' },
otherField: { default: '' },
required: { type: Boolean, default: false },
disableChange: { type: Boolean, default: false },
});
const value = computed(() => props.form?.[props.title] || '');
const otherOption = computed(() => props.otherFiled || props.otherField || props.range.find((item) => /^(其它|其他)$/.test(item)) || '其它');
const isPreset = computed(() => props.range.includes(value.value));
const selectingOther = ref(false);
const showOther = computed(() => selectingOther.value || (Boolean(value.value) && !isPreset.value));
const selectedValue = computed(() => showOther.value ? otherOption.value : value.value);
const otherValue = computed(() => showOther.value ? value.value : '');
watch(value, (nextValue) => {
if (nextValue && props.range.includes(nextValue)) selectingOther.value = false;
});
function changeSelect(event) {
const selected = props.range[event.detail.value];
if (selected === otherOption.value) {
selectingOther.value = true;
return;
}
selectingOther.value = false;
emits('change', { title: props.title, value: selected });
}
function changeOther(event) { emits('change', { title: props.title, value: event.detail.value }); }
</script>
<style>
@import '../cell-style.css';
.select-other { width: 100%; }
.select-other__picker, .select-other__input { box-sizing: border-box; width: 100%; padding: 12rpx 16rpx; border: 1rpx solid #dcdfe6; border-radius: 8rpx; font-size: 28rpx; }
.select-other__input { margin-top: 14rpx; }
</style>

View File

@ -10,12 +10,6 @@
@change="change" /> @change="change" />
<form-select v-else-if="attrs.type === 'select'" v-bind="attrs" :form="form" :disableChange="disableChange" <form-select v-else-if="attrs.type === 'select'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" /> @change="change" />
<form-evaluated-joint v-else-if="attrs.type === 'evaluatedJoint'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" />
<form-exercise-level v-else-if="attrs.type === 'exerciseLevel'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" />
<form-select-and-other v-else-if="attrs.type === 'selectAndOther'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" />
<form-textarea v-else-if="attrs.type === 'textarea'" v-bind="attrs" :form="form" :disableChange="disableChange" <form-textarea v-else-if="attrs.type === 'textarea'" v-bind="attrs" :form="form" :disableChange="disableChange"
@change="change" /> @change="change" />
<form-mult-disease v-else-if="attrs.type === 'selfMultipleDiseases'" v-bind="attrs" :form="form" @change="change" <form-mult-disease v-else-if="attrs.type === 'selfMultipleDiseases'" v-bind="attrs" :form="form" @change="change"
@ -39,9 +33,6 @@ import { useAttrs } from 'vue';
import formInput from './form-input.vue'; import formInput from './form-input.vue';
import formSelect from './form-select.vue'; import formSelect from './form-select.vue';
import formEvaluatedJoint from './form-evaluated-joint.vue';
import formExerciseLevel from './form-exercise-level.vue';
import formSelectAndOther from './form-select-and-other.vue';
import formRadio from './form-radio.vue'; import formRadio from './form-radio.vue';
import formDatepicker from './form-datepicker.vue'; import formDatepicker from './form-datepicker.vue';
import formRegion from './form-region.vue'; import formRegion from './form-region.vue';
@ -117,4 +108,4 @@ export default {
} }
</script> --> </script> -->
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -33,7 +33,7 @@ const props = defineProps({
} }
}) })
const formCellType = ['input', 'select', 'date', 'radio', 'region', 'textarea', 'multiSelectAndOther', 'selectAndOther', 'selfMultipleDiseases', 'files', 'diagnosis', 'positiveFind', 'evaluatedJoint', 'exerciseLevel']; const formCellType = ['input', 'select', 'date', 'radio', 'region', 'textarea', 'multiSelectAndOther', 'selfMultipleDiseases', 'files','diagnosis','positiveFind'];
const formCellTitle = ['surgicalHistory']; const formCellTitle = ['surgicalHistory'];
const customCellType = ['BMI', 'bloodPressure']; const customCellType = ['BMI', 'bloodPressure'];
const disabledMap = computed(() => props.disableTitles.reduce((m, i) => { const disabledMap = computed(() => props.disableTitles.reduce((m, i) => {

View File

@ -1,91 +0,0 @@
import api from "@/utils/api";
import { set } from "@/utils/cache";
import { normalizeCorpId } from "@/utils/api-base-config";
function buildTeamLoginUrl(redirectUrl) {
const query = [
"source=teamInvite",
`redirectUrl=${encodeURIComponent(redirectUrl || "")}`,
].join("&");
return `/pages/login/login?${query}`;
}
export function useTeamAccess() {
async function getTeamInfo({ corpId, teamId, fallbackCorpName = "" }) {
if (!corpId || !teamId) return null;
try {
const res = await api(
"getTeamData",
{ corpId, teamId, withCorpName: true },
false
);
const team = res?.data || null;
if (!team) return null;
return {
...team,
corpName: team.corpName || team.corp_name || fallbackCorpName,
};
} catch (error) {
console.warn("getTeamData failed", error);
return null;
}
}
async function openTeamLogin({
corpId,
teamId,
redirectUrl,
fallbackCorpName = "",
corpUserId = "",
externalUserId = "",
}) {
if (!corpId || !teamId) {
return { success: false, message: "团队信息缺失,暂无法授权" };
}
const team = await getTeamInfo({ corpId, teamId, fallbackCorpName });
set("invite-team-info", {
corpId,
teamId,
corpUserId,
externalUserId,
corpName: team?.corpName || fallbackCorpName,
teamName: team?.name || team?.teamName || "",
avatars: Array.isArray(team?.memberList)
? team.memberList.map((member) => member?.avatar || "").filter(Boolean)
: [],
});
uni.navigateTo({ url: buildTeamLoginUrl(redirectUrl) });
return { success: true, team };
}
async function ensureTeamAdded({ appid, account, getTeams, corpId, teamId }) {
if (!appid || !account?.openid || !corpId || !teamId) {
return { success: false, message: "团队信息缺失,暂无法绑定" };
}
const teams = await getTeams();
const linked = (teams || []).some(
(team) =>
normalizeCorpId(team.corpId) === normalizeCorpId(corpId) &&
String(team.teamId) === String(teamId)
);
if (linked) return { success: true, alreadyLinked: true };
const res = await api(
"bindWxappWithTeam",
{ appid, corpId, teamId, openid: account.openid },
false
);
if (!res?.success) {
return { success: false, message: res?.message || "添加团队失败" };
}
await getTeams();
return { success: true, alreadyLinked: false };
}
return {
getTeamInfo,
openTeamLogin,
ensureTeamAdded,
};
}

View File

@ -10,12 +10,12 @@
</view> </view>
</view> </view>
<template #footer> <template #footer>
<button-footer :showCancel="customerId ? true : false" cancelText="删除" <button-footer :showCancel="customerId ? true : false" cancelText="删除" :confirmText="healthTypes.length?'下一步':'保存'" @cancel="unBindArchive()"
:confirmText="healthTypes.length ? '下一步' : '保存'" @cancel="unBindArchive()" @confirm="confirm()" /> @confirm="confirm()" />
</template> </template>
</full-page> </full-page>
<bind-popup :customers="customers" :corpName="corpName" :enableHis="enableHis" :visible="visible" <bind-popup :customers="customers" :corpName="corpName" :enableHis="enableHis" :visible="visible" @close="visible = false"
@close="visible = false" @confirm="bindArchive($event)" /> @confirm="bindArchive($event)" />
<verify-popup :visible="verifyVisible" @close="verifyVisible = false" /> <verify-popup :visible="verifyVisible" @close="verifyVisible = false" />
</template> </template>
<script setup> <script setup>
@ -44,7 +44,6 @@ const { getExternalUserId } = useAccount()
const corpId = ref(''); const corpId = ref('');
const corpName = ref(''); const corpName = ref('');
const corpUserId = ref(''); const corpUserId = ref('');
const bindCustomerId = ref('');
const referenceCustomerId = ref(''); const referenceCustomerId = ref('');
const customer = ref({}); const customer = ref({});
const customerId = ref(''); const customerId = ref('');
@ -60,7 +59,6 @@ const visible = ref(false);
const referenceCustomer = ref(null) const referenceCustomer = ref(null)
const healthTypes = ref([]); const healthTypes = ref([]);
const enableHis = ref(false); const enableHis = ref(false);
const source = ref('');
const formData = computed(() => { const formData = computed(() => {
return { ...customer.value, ...form.value, mobile: account.value?.mobile } return { ...customer.value, ...form.value, mobile: account.value?.mobile }
@ -168,11 +166,6 @@ async function addArchive() {
set('home-invite-team-info', { teamId: teamId.value }) set('home-invite-team-info', { teamId: teamId.value })
if (res && res.success) { if (res && res.success) {
uni.$emit('reloadTeamCustomers') uni.$emit('reloadTeamCustomers')
if (source.value === 'experienceCoupon') {
await toast('档案创建成功');
uni.navigateBack();
return;
}
// getTeam(corpId.value, teamId.value, res.data.id); // getTeam(corpId.value, teamId.value, res.data.id);
if (healthTypes.value.length) { if (healthTypes.value.length) {
const nextType = healthTypes.value[0]; const nextType = healthTypes.value[0];
@ -194,20 +187,6 @@ async function getResponsiblePerson() {
return res && res.data ? res.data : '' return res && res.data ? res.data : ''
} }
function shouldBackAfterArchiveBound() {
return ['experienceCoupon', 'appointmentRegistration'].includes(source.value)
}
function backAfterArchiveBound() {
if (shouldBackAfterArchiveBound()) {
uni.navigateBack()
return;
}
uni.switchTab({
url: '/pages/home/home'
})
}
async function bindArchive(customerId) { async function bindArchive(customerId) {
let responsiblePerson = ''; let responsiblePerson = '';
if (externalUserId.value) { if (externalUserId.value) {
@ -218,7 +197,9 @@ async function bindArchive(customerId) {
if (res && res.success) { if (res && res.success) {
await toast('绑定成功'); await toast('绑定成功');
uni.$emit('reloadTeamCustomers') uni.$emit('reloadTeamCustomers')
backAfterArchiveBound() uni.switchTab({
url: '/pages/home/home'
})
} else { } else {
toast(res?.message || '绑定失败'); toast(res?.message || '绑定失败');
} }
@ -228,15 +209,11 @@ async function init() {
if (customerId.value) { if (customerId.value) {
await getCustomer(); await getCustomer();
} else { } else {
await getExternalUserId(corpId.value); const res = await getArchives();
const res = bindCustomerId.value ? await getExperienceCouponBindArchive() : await getArchives();
if (res.length > 0) { if (res.length > 0) {
visible.value = true; visible.value = true;
} else if (bindCustomerId.value || shouldBackAfterArchiveBound()) {
await toast('指定绑定档案不存在或已绑定');
uni.navigateBack();
return;
} }
getExternalUserId(corpId.value);
getTeam(corpId.value, teamId.value) getTeam(corpId.value, teamId.value)
} }
await getBaseForm(); await getBaseForm();
@ -246,17 +223,8 @@ async function init() {
} }
async function getExperienceCouponBindArchive() {
const res = await api('getCustomerByCustomerId', { corpId: corpId.value, customerId: bindCustomerId.value });
const data = res && res.success && res.data ? res.data : null;
customers.value = data ? [data] : [];
corpName.value = res && res.corpName ? res.corpName : corpName.value;
enableHis.value = false;
return customers.value;
}
async function getArchives() { async function getArchives() {
const res = await api('getUnbindMiniAppCustomers', { corpId: corpId.value, mobile: account.value?.mobile || '', externalUserId: externalUserId.value }); const res = await api('getUnbindMiniAppCustomers', { corpId: corpId.value, mobile: account.value?.mobile || '' });
customers.value = res && Array.isArray(res.data) ? res.data : []; customers.value = res && Array.isArray(res.data) ? res.data : [];
corpName.value = res && res.corpName ? res.corpName : ''; corpName.value = res && res.corpName ? res.corpName : '';
enableHis.value = res && res.enableHis ? res.enableHis : false; enableHis.value = res && res.enableHis ? res.enableHis : false;
@ -337,10 +305,8 @@ onLoad(options => {
teamId.value = options.teamId; teamId.value = options.teamId;
corpId.value = options.corpId; corpId.value = options.corpId;
customerId.value = options.id || ''; customerId.value = options.id || '';
bindCustomerId.value = options.bindCustomerId || '';
corpUserId.value = options.corpUserId || ''; corpUserId.value = options.corpUserId || '';
referenceCustomerId.value = options.referenceCustomerId || ''; referenceCustomerId.value = options.referenceCustomerId || '';
source.value = options.source || '';
if (referenceCustomerId.value) { if (referenceCustomerId.value) {
getReferenceCustomer(); getReferenceCustomer();
} }

View File

@ -1,29 +1,20 @@
<template> <template>
<view> <full-page>
<full-page> <view class="claim-page">
<view class="claim-page"> <view v-if="tip" class="tip">{{ tip }}</view>
<view v-if="tip" class="tip">{{ tip }}</view> <view v-else class="tip">加载中...</view>
<view v-else class="tip">加载中...</view> </view>
</view>
</full-page>
<!-- Keep the fixed overlay outside full-page's scroll-view for iOS compatibility. -->
<view v-if="posterVisible" class="poster-mask"> <view v-if="posterVisible" class="poster-mask">
<view class="poster-dialog"> <view class="poster-dialog">
<image <image class="poster-image" :src="issue?.posterUrl || ''" mode="widthFix" />
v-if="issue?.posterUrl"
class="poster-image"
:src="issue.posterUrl"
mode="aspectFit"
/>
<view v-else class="poster-image poster-image--placeholder">体验券</view>
<view class="poster-content">{{ buildClaimContent() }}</view> <view class="poster-content">{{ buildClaimContent() }}</view>
<view class="poster-btn" :class="{ disabled: claiming }" @click="acceptCoupon"> <view class="poster-btn" :class="{ disabled: claiming }" @click="acceptCoupon">
{{ claiming ? "领取中..." : "接受" }} {{ claiming ? "领取中..." : "接受" }}
</view> </view>
</view> </view>
</view> </view>
</view> </full-page>
</template> </template>
<script setup> <script setup>
@ -32,29 +23,21 @@ import { storeToRefs } from "pinia";
import { onLoad, onShow } from "@dcloudio/uni-app"; import { onLoad, onShow } from "@dcloudio/uni-app";
import useAccount from "@/store/account"; import useAccount from "@/store/account";
import api from "@/utils/api"; import api from "@/utils/api";
import { hideLoading, loading, toast } from "@/utils/widget"; import { confirm, hideLoading, loading, toast } from "@/utils/widget";
import { normalizeCorpId } from "@/utils/api-base-config"; import { normalizeCorpId } from "@/utils/api-base-config";
import { useTeamAccess } from "@/hooks/use-team-access";
import FullPage from "@/components/full-page.vue"; import FullPage from "@/components/full-page.vue";
const env = __VITE_ENV__;
const appid = env.MP_WX_APP_ID;
const { account } = storeToRefs(useAccount()); const { account } = storeToRefs(useAccount());
const { getTeams, login } = useAccount(); const { login } = useAccount();
const { openTeamLogin: openTeamInviteLogin, ensureTeamAdded: ensureJoinedTeam } = useTeamAccess();
const corpId = ref(""); const corpId = ref("");
const issueId = ref(""); const issueId = ref("");
const memberId = ref(""); const memberId = ref("");
const issue = ref(null); const issue = ref(null);
const issueCustomer = ref(null);
const tip = ref(""); const tip = ref("");
const claiming = ref(false); const claiming = ref(false);
const autoPrompted = ref(false); const autoPrompted = ref(false);
const posterVisible = ref(false); const posterVisible = ref(false);
const archiveBindPending = ref(false);
const bindingTeamId = ref("");
const bindCorpName = ref("");
const projectNameText = computed(() => { const projectNameText = computed(() => {
const projects = Array.isArray(issue.value?.projectSnapshot) ? issue.value.projectSnapshot : []; const projects = Array.isArray(issue.value?.projectSnapshot) ? issue.value.projectSnapshot : [];
@ -71,7 +54,7 @@ async function ensureLogin() {
if (!account.value) await login(); if (!account.value) await login();
if (!account.value) { if (!account.value) {
tip.value = "请先登录后再领取体验券"; tip.value = "请先登录后再领取体验券";
await openTeamLogin(); uni.navigateTo({ url: "/pages/login/login" });
return false; return false;
} }
return true; return true;
@ -94,132 +77,53 @@ async function loadIssueDetail() {
return true; return true;
} }
async function getIssueCustomer({ force = false } = {}) {
if (!corpId.value || !memberId.value) return null;
if (issueCustomer.value && !force) return issueCustomer.value;
try {
const detail = await api(
"getCustomerByCustomerId",
{ corpId: corpId.value, customerId: memberId.value },
false
);
const customer = detail?.data || null;
issueCustomer.value = customer;
bindCorpName.value = detail?.corpName || issue.value?.corpName || "";
const teamIds = Array.isArray(customer?.teamId) ? customer.teamId : [customer?.teamId];
bindingTeamId.value = issue.value?.teamId || teamIds.find(Boolean) || "";
return customer;
} catch (e) {
console.warn("getCustomerByCustomerId failed", e);
return null;
}
}
async function hasBoundArchive() { async function hasBoundArchive() {
const openid = account.value?.openid || ""; const openid = account.value?.openid || "";
if (!openid) return false; if (!openid || !corpId.value) return false;
const customer = await getIssueCustomer({ force: true }); if (memberId.value) {
return Boolean(customer && String(customer.miniAppId || "") === String(openid)); try {
} const detail = await api(
"getCustomerByCustomerId",
function buildClaimUrl() { { corpId: corpId.value, customerId: memberId.value },
const params = [ false
`corpId=${encodeURIComponent(corpId.value || "")}`, );
`issueId=${encodeURIComponent(issueId.value || "")}`, const customer = detail?.data || null;
memberId.value ? `memberId=${encodeURIComponent(memberId.value)}` : "", if (customer && String(customer.miniAppId || "") === String(openid)) {
].filter(Boolean).join("&"); return true;
return `/pages/experience-coupon/claim?${params}`; }
} } catch (e) {
console.warn("getCustomerByCustomerId failed", e);
async function openTeamLogin() {
const customer = await getIssueCustomer();
if (!customer || !bindingTeamId.value) {
tip.value = "体验券发放档案缺少所属团队,暂无法领取";
return false;
}
const result = await openTeamInviteLogin({
corpId: corpId.value,
teamId: bindingTeamId.value,
redirectUrl: buildClaimUrl(),
fallbackCorpName: bindCorpName.value || issue.value?.corpName || "",
});
if (!result.success) {
tip.value = result.message;
return false;
}
return false;
}
async function ensurePhoneAuthorized() {
if (account.value?.mobile) return true;
tip.value = "请先授权手机号后再领取体验券";
return openTeamLogin();
}
async function ensureTeamAdded() {
const customer = await getIssueCustomer();
if (!customer || !bindingTeamId.value) {
tip.value = "体验券发放档案缺少所属团队,暂无法领取";
return false;
}
loading("添加团队中...");
try {
const res = await ensureJoinedTeam({
appid,
account: account.value,
getTeams,
corpId: corpId.value,
teamId: bindingTeamId.value,
});
if (!res.success) {
tip.value = res.message;
toast(tip.value);
return false;
} }
return true; }
} finally {
hideLoading(); 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 buildArchiveBindUrl() { function goBindArchive() {
const params = [ uni.navigateTo({
`corpId=${encodeURIComponent(corpId.value || "")}`, url: `/pages/archive/archive-manage?corpId=${encodeURIComponent(corpId.value || "")}`,
`teamId=${encodeURIComponent(bindingTeamId.value || "")}`, });
`bindCustomerId=${encodeURIComponent(memberId.value || "")}`,
`source=experienceCoupon`,
].join("&");
return `/pages/archive/edit-archive?${params}`;
}
function openArchiveBindPage() {
if (!issueCustomer.value || !bindingTeamId.value) {
toast("体验券发放档案不存在");
return;
}
archiveBindPending.value = true;
uni.navigateTo({ url: buildArchiveBindUrl() });
}
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 getIssueExpireAt() {
return toTimestamp(issue.value?.activityEndTime || issue.value?.displayExpireTime);
} }
function isExpiredIssue() { function isExpiredIssue() {
const expireAt = getIssueExpireAt(); const expireAt = Number(issue.value?.projectExpireTime || 0);
return Boolean(expireAt && Date.now() > expireAt); 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() { async function acceptCoupon() {
@ -237,10 +141,6 @@ async function acceptCoupon() {
if (!res?.success) { if (!res?.success) {
tip.value = res?.message || "领取失败"; tip.value = res?.message || "领取失败";
toast(tip.value); toast(tip.value);
if (tip.value.includes("绑定本体验券发放的档案")) {
posterVisible.value = false;
openArchiveBindPage();
}
return; return;
} }
toast("领取成功"); toast("领取成功");
@ -284,7 +184,17 @@ async function promptClaim() {
const bound = await hasBoundArchive(); const bound = await hasBoundArchive();
if (!bound) { if (!bound) {
openArchiveBindPage(); tip.value = "请先绑定档案后再领取体验券";
try {
await confirm("请先绑定档案后再领取体验券", {
title: "提示",
confirmText: "去绑定",
cancelText: "取消",
});
goBindArchive();
} catch {
// ignore
}
return; return;
} }
@ -295,39 +205,17 @@ async function promptClaim() {
async function bootstrap() { async function bootstrap() {
loading("加载中..."); loading("加载中...");
try { try {
const okDetail = await loadIssueDetail();
if (!okDetail) return;
const okLogin = await ensureLogin(); const okLogin = await ensureLogin();
if (!okLogin) return; if (!okLogin) return;
const phoneAuthorized = await ensurePhoneAuthorized(); const okDetail = await loadIssueDetail();
if (!phoneAuthorized) return; if (!okDetail) return;
const teamAdded = await ensureTeamAdded();
if (!teamAdded) return;
await promptClaim(); await promptClaim();
} finally { } finally {
hideLoading(); hideLoading();
} }
} }
function parseOptions(options = {}) { onLoad((options = {}) => {
const href = typeof options.q === "string" ? decodeURIComponent(options.q) : "";
const [, url = ""] = href.split("?");
return url.split("&").reduce((acc, cur) => {
if (!cur) return acc;
const [key, val = ''] = cur.split("=");
if (!key) return acc;
acc[key] = val
return acc;
}, {});
}
onLoad((opts = {}) => {
let options = {};
if (opts.q) {
options = { ...parseOptions(opts) }
} else {
options = { ...opts }
}
corpId.value = normalizeCorpId(options.corpId || ""); corpId.value = normalizeCorpId(options.corpId || "");
issueId.value = options.issueId || options.id || ""; issueId.value = options.issueId || options.id || "";
memberId.value = options.memberId || options.customerId || ""; memberId.value = options.memberId || options.customerId || "";
@ -340,11 +228,9 @@ onLoad((opts = {}) => {
}); });
onShow(() => { onShow(() => {
if (archiveBindPending.value && issue.value && issue.value.status === "issued") { if (issue.value && issue.value.status === "issued" && tip.value.includes("绑定")) {
archiveBindPending.value = false;
autoPrompted.value = false; autoPrompted.value = false;
tip.value = ""; promptClaim();
posterVisible.value = true;
} }
}); });
</script> </script>
@ -363,13 +249,9 @@ onShow(() => {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
.poster-mask { .poster-mask {
position: fixed; position: fixed;
top: 0; inset: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000; z-index: 1000;
display: flex; display: flex;
align-items: center; align-items: center;
@ -382,7 +264,6 @@ onShow(() => {
.poster-dialog { .poster-dialog {
width: 100%; width: 100%;
max-width: 620rpx; max-width: 620rpx;
max-height: calc(100vh - 80rpx);
overflow: hidden; overflow: hidden;
border-radius: 20rpx; border-radius: 20rpx;
background: #fff; background: #fff;
@ -391,26 +272,15 @@ onShow(() => {
.poster-image { .poster-image {
display: block; display: block;
width: 100%; width: 100%;
height: 58vh;
max-height: 700rpx; max-height: 700rpx;
background: #f5f7fa; background: #f5f7fa;
} }
.poster-image--placeholder {
display: flex;
align-items: center;
justify-content: center;
color: #9aa5b1;
font-size: 32rpx;
}
.poster-content { .poster-content {
padding: 28rpx 30rpx 8rpx; padding: 28rpx 30rpx 8rpx;
color: #333; color: #333;
font-size: 28rpx; font-size: 28rpx;
line-height: 1.6; line-height: 1.6;
max-height: 160rpx;
overflow: hidden;
} }
.poster-btn { .poster-btn {
@ -428,4 +298,4 @@ onShow(() => {
.poster-btn.disabled { .poster-btn.disabled {
opacity: 0.6; opacity: 0.6;
} }
</style> </style>

View File

@ -9,13 +9,12 @@
> >
<view class="page-body"> <view class="page-body">
<view v-if="loading" class="empty">加载中...</view> <view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!list.length" class="empty">暂无体验券</view> <view v-else-if="!list.length" class="empty">暂无待领取体验券</view>
<view v-else class="coupon-list"> <view v-else class="coupon-list">
<view <view
v-for="item in list" v-for="item in list"
:key="item._id" :key="item._id"
class="coupon-card" class="coupon-card"
:class="item.cardClass"
@click="openPoster(item)" @click="openPoster(item)"
> >
<image <image
@ -26,14 +25,7 @@
/> />
<view v-else class="coupon-poster coupon-poster--empty">体验券</view> <view v-else class="coupon-poster coupon-poster--empty">体验券</view>
<view class="coupon-body"> <view class="coupon-body">
<view class="coupon-title-row"> <view class="coupon-title">{{ item.activityName || "体验券" }}</view>
<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-grid">
<view class="coupon-info-row coupon-info-row--projects"> <view class="coupon-info-row coupon-info-row--projects">
<text class="coupon-info-label">项目</text> <text class="coupon-info-label">项目</text>
@ -51,20 +43,14 @@
</view> </view>
<view class="coupon-info-row"> <view class="coupon-info-row">
<text class="coupon-info-label">项目有效期</text> <text class="coupon-info-label">项目有效期</text>
<text>{{ item.projectValidText }}</text> <text>{{ item.validText }}</text>
</view> </view>
<view class="coupon-info-row"> <view class="coupon-info-row">
<text class="coupon-info-label">体验券有效期</text> <text class="coupon-info-label">活动有效期</text>
<text>{{ item.couponValidText }}</text> <text>{{ item.activityValidText }}</text>
</view>
<view class="coupon-info-row">
<text class="coupon-info-label">发送时间</text>
<text>{{ item.issueTimeText }}</text>
</view> </view>
</view> </view>
<view class="coupon-action" :class="{ disabled: item.displayStatus !== 'issued' }"> <view class="coupon-action">点击查看海报领取</view>
{{ item.actionText }}
</view>
</view> </view>
</view> </view>
</view> </view>
@ -116,18 +102,9 @@ const posterVisible = ref(false);
const current = ref(null); const current = ref(null);
const claiming = ref(false); const claiming = ref(false);
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) { function formatDate(ts) {
const time = toTimestamp(ts); if (!ts) return "";
if (!time) return ""; const d = new Date(Number(ts));
const d = new Date(time);
if (Number.isNaN(d.getTime())) return ""; if (Number.isNaN(d.getTime())) return "";
const y = d.getFullYear(); const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0"); const m = String(d.getMonth() + 1).padStart(2, "0");
@ -135,26 +112,15 @@ function formatDate(ts) {
return `${y}-${m}-${day}`; 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) { function projectText(projects) {
const arr = Array.isArray(projects) ? projects : []; const arr = Array.isArray(projects) ? projects : [];
if (!arr.length) return "未配置项目"; if (!arr.length) return "未配置项目";
return arr.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、"); return arr.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、");
} }
function buildProjectValidText(item) { function buildValidText(item) {
const rule = item.projectValidRuleSnapshot || {}; const rule = item.projectValidRuleSnapshot || {};
const projectExpireAt = toTimestamp(item.projectExpireTime); if (item.projectExpireTime) return `有效期至 ${formatDate(item.projectExpireTime)}`;
if (projectExpireAt) return `有效期至 ${formatDate(projectExpireAt)}`;
if (rule.type === "fixedDate" && rule.fixedDate) { if (rule.type === "fixedDate" && rule.fixedDate) {
return `有效期至 ${formatDate(rule.fixedDate)}`; return `有效期至 ${formatDate(rule.fixedDate)}`;
} }
@ -164,20 +130,23 @@ function buildProjectValidText(item) {
return "领取后按活动规则生效"; return "领取后按活动规则生效";
} }
function getCouponExpireAt(item) { function buildActivityValidText(item) {
return toTimestamp(item.activityEndTime || item.displayExpireTime);
}
function buildCouponValidText(item) {
const start = formatDate(item.activityStartTime) || "-"; const start = formatDate(item.activityStartTime) || "-";
const expireAt = getCouponExpireAt(item); const end = formatDate(item.activityEndTime) || "-";
const end = formatDate(expireAt) || "-";
if (start === "-" && end === "-") return "未配置有效期";
return `${start} 至 ${end}`; return `${start} 至 ${end}`;
} }
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) { function isExpired(item) {
const expireAt = getCouponExpireAt(item); const expireAt = getExpireAt(item);
if (!expireAt) return false; if (!expireAt) return false;
return Date.now() > expireAt; return Date.now() > expireAt;
} }
@ -215,33 +184,20 @@ async function resolveContext(options = {}) {
return !!customerId.value; return !!customerId.value;
} }
function resolveDisplayStatus(item) { async function voidExpired(item) {
const status = item.displayStatus || item.status || "issued"; try {
if (status === "claimed") { await api(
return { "voidExperienceCouponIssue",
displayStatus: "claimed", {
displayStatusText: "已领取", corpId: corpId.value,
actionText: "已领取", issueId: item._id,
cardClass: "coupon-card--claimed", voidReason: "有效期内未领取,自动失效",
statusClass: "coupon-status--claimed", },
}; false
);
} catch (e) {
console.warn("auto void failed", e);
} }
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 = {}) { async function loadCoupons(options = {}) {
@ -257,6 +213,7 @@ async function loadCoupons(options = {}) {
{ {
corpId: corpId.value, corpId: corpId.value,
customerId: customerId.value, customerId: customerId.value,
status: "issued",
page: 1, page: 1,
pageSize: 100, pageSize: 100,
}, },
@ -268,19 +225,20 @@ async function loadCoupons(options = {}) {
return; return;
} }
const raw = res.list || res.data?.list || []; const raw = res.list || res.data?.list || [];
list.value = raw const valid = [];
.map((item) => { for (const item of raw) {
const status = resolveDisplayStatus(item); if (isExpired(item)) {
return { voidExpired(item);
...item, continue;
...status, }
projectText: projectText(item.projectSnapshot), valid.push({
projectValidText: buildProjectValidText(item), ...item,
couponValidText: buildCouponValidText(item), projectText: projectText(item.projectSnapshot),
issueTimeText: formatDateTime(item.issueTime || item.createTime), validText: buildValidText(item),
}; activityValidText: buildActivityValidText(item),
}) });
.sort((a, b) => Number(b.issueTime || b.createTime || 0) - Number(a.issueTime || a.createTime || 0)); }
list.value = valid;
} finally { } finally {
loading.value = false; loading.value = false;
} }
@ -297,10 +255,6 @@ async function refreshCoupons() {
} }
function openPoster(item) { function openPoster(item) {
if (item?.displayStatus !== "issued") {
toast(item?.displayStatusText || "当前体验券不可领取");
return;
}
if (!item?.posterUrl) { if (!item?.posterUrl) {
toast("该体验券暂无海报"); toast("该体验券暂无海报");
return; return;
@ -384,11 +338,6 @@ onShow(async () => {
box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06); box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06);
} }
.coupon-card--claimed,
.coupon-card--expired {
opacity: 0.78;
}
.coupon-poster { .coupon-poster {
width: 160rpx; width: 160rpx;
height: 160rpx; height: 160rpx;
@ -410,48 +359,11 @@ onShow(async () => {
flex: 1; 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 { .coupon-title {
min-width: 0;
flex: 1;
font-size: 30rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: #222; color: #222;
} margin-bottom: 8rpx;
.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 { .coupon-info-grid {
@ -504,10 +416,6 @@ onShow(async () => {
color: #ff8a00; color: #ff8a00;
} }
.coupon-action.disabled {
color: #a8abb2;
}
.empty { .empty {
padding: 120rpx 0; padding: 120rpx 0;
text-align: center; text-align: center;

View File

@ -86,14 +86,8 @@ function formatDate(ts) {
return `${y}-${m}-${day}`; return `${y}-${m}-${day}`;
} }
async function loadCorpName() { function pickCorpName(team = {}) {
if (!corpId.value) { return team.licenseHospitalName || team.leaderCorp || team.corpName || "-";
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 || "";
} }
const groups = computed(() => { const groups = computed(() => {
@ -145,6 +139,16 @@ function applySelection(selection = {}) {
teamId.value = selection.teamId || teamId.value || ""; teamId.value = selection.teamId || teamId.value || "";
customerId.value = selection.customerId || selection.memberId || customerId.value || ""; customerId.value = selection.customerId || selection.memberId || customerId.value || "";
customerName.value = selection.name || customerName.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 = {}) { async function resolveContext(options = {}) {
@ -177,7 +181,7 @@ async function resolveContext(options = {}) {
if (!corpId.value) corpId.value = normalizeCorpId(matched?.corpId || ""); if (!corpId.value) corpId.value = normalizeCorpId(matched?.corpId || "");
if (!teamId.value) teamId.value = matched?.teamId || ""; if (!teamId.value) teamId.value = matched?.teamId || "";
await loadCorpName(); if (!corpName.value) corpName.value = pickCorpName(matched || {});
if (customerId.value) return true; if (customerId.value) return true;
@ -236,7 +240,6 @@ async function syncSelectedProfile() {
if (!selected) return false; if (!selected) return false;
remove(RIGHTS_SELECTION_CACHE_KEY); remove(RIGHTS_SELECTION_CACHE_KEY);
applySelection(selected); applySelection(selected);
await loadCorpName();
await loadRights(); await loadRights();
return true; return true;
} }

View File

@ -3,7 +3,7 @@
<view class="page-body"> <view class="page-body">
<view v-if="loading" class="empty">加载中...</view> <view v-if="loading" class="empty">加载中...</view>
<view v-else-if="!options.length" class="empty">暂无可用档案</view> <view v-else-if="!options.length" class="empty">暂无可用档案</view>
<scroll-view v-else scroll-y class="archive-list"> <view v-else class="archive-list">
<view <view
v-for="item in options" v-for="item in options"
:key="item.key" :key="item.key"
@ -22,7 +22,7 @@
<view class="archive-id">证件号:{{ item.idCardText }}</view> <view class="archive-id">证件号:{{ item.idCardText }}</view>
<view class="archive-corp">{{ item.corpName }}</view> <view class="archive-corp">{{ item.corpName }}</view>
</view> </view>
</scroll-view> </view>
</view> </view>
</full-page> </full-page>
</template> </template>
@ -33,14 +33,14 @@ import { onLoad } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import useAccount from "@/store/account"; import useAccount from "@/store/account";
import api from "@/utils/api"; import api from "@/utils/api";
import { get, set } from "@/utils/cache"; import { set } from "@/utils/cache";
import { toast } from "@/utils/widget"; import { toast } from "@/utils/widget";
import { normalizeCorpId } from "@/utils/api-base-config"; import { normalizeCorpId } from "@/utils/api-base-config";
import FullPage from "@/components/full-page.vue"; 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 RIGHTS_SELECTION_CACHE_KEY = "experience-coupon-rights-selection";
const { account } = storeToRefs(useAccount()); const { account } = storeToRefs(useAccount());
const { getTeams } = useAccount();
const loading = ref(false); const loading = ref(false);
const options = ref([]); const options = ref([]);
@ -50,6 +50,10 @@ const currentCustomerId = ref("");
const mode = ref("back"); const mode = ref("back");
const target = ref(""); const target = ref("");
function pickCorpName(team = {}) {
return team.licenseHospitalName || team.leaderCorp || team.corpName || "-";
}
function maskMobile(mobile = "") { function maskMobile(mobile = "") {
const value = String(mobile || "").trim(); const value = String(mobile || "").trim();
if (!value) return ""; if (!value) return "";
@ -73,6 +77,16 @@ function buildMetaText(customer = {}) {
return parts.length ? parts.join(",") : "暂无档案信息"; 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) { function buildTargetUrl(item) {
const corp = encodeURIComponent(item.corpId || ""); const corp = encodeURIComponent(item.corpId || "");
const team = encodeURIComponent(item.teamId || ""); const team = encodeURIComponent(item.teamId || "");
@ -105,34 +119,35 @@ async function loadArchives() {
loading.value = true; loading.value = true;
try { try {
const currentTeam = get(HOME_CURRENT_TEAM_CACHE_KEY) || {}; const teams = (await getTeams()) || [];
const selectedCorpId = normalizeCorpId(currentTeam.corpId || currentCorpId.value); 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 [];
}
})
);
if (!selectedCorpId) { const list = responses
options.value = []; .flat()
return; .map(({ team, customer }) => ({
} key: `${team.corpId}_${customer._id}`,
corpId: team.corpId,
const [corpRes, customerRes] = await Promise.all([ teamId: team.teamId || "",
api("getCorpInfo", { corpId: selectedCorpId }, false),
api("getCorpMiniAppCustomers", { miniAppId, corpId: selectedCorpId }, false),
]);
const corp = Array.isArray(corpRes?.data) ? corpRes.data[0] : null;
const archiveCorpName = corp?.corp_name || corp?.corpName || "-";
const customers = customerRes?.success && Array.isArray(customerRes.data) ? customerRes.data : [];
const list = customers
.map((customer) => ({
key: `${selectedCorpId}_${customer._id}`,
corpId: selectedCorpId,
teamId: currentTeam.teamId || currentTeamId.value || "",
customerId: customer._id || "", customerId: customer._id || "",
name: customer.name || "", name: customer.name || "",
relationship: customer.relationship || "", relationship: customer.relationship || "",
metaText: buildMetaText(customer), metaText: buildMetaText(customer),
idCardText: maskIdCard(customer.idCard), idCardText: maskIdCard(customer.idCard),
corpName: archiveCorpName, corpName: pickCorpName(team),
})) }))
.filter((item) => item.customerId) .filter((item) => item.corpId && item.customerId)
.sort((a, b) => { .sort((a, b) => {
const aScore = const aScore =
(a.corpId === currentCorpId.value ? 4 : 0) + (a.corpId === currentCorpId.value ? 4 : 0) +
@ -195,16 +210,12 @@ onLoad(async (optionsData = {}) => {
<style scoped> <style scoped>
.page-body { .page-body {
height: 100vh; min-height: 100%;
padding: 24rpx; padding: 24rpx;
box-sizing: border-box; box-sizing: border-box;
background: #f5f6fa; background: #f5f6fa;
} }
.archive-list {
height: 100%;
}
.archive-card { .archive-card {
background: #fff; background: #fff;
border-radius: 18rpx; border-radius: 18rpx;

View File

@ -48,7 +48,6 @@ import pageLoading from "./loading.vue";
const { account } = storeToRefs(useAccount()); const { account } = storeToRefs(useAccount());
const { login, getTeams } = useAccount(); const { login, getTeams } = useAccount();
const env = __VITE_ENV__; const env = __VITE_ENV__;
const appid = env.MP_WX_APP_ID;
const shareAppVersion = env.MP_SHARE_WX_APP_VERSION; const shareAppVersion = env.MP_SHARE_WX_APP_VERSION;
const team = ref(null); const team = ref(null);
@ -59,8 +58,6 @@ const consultRef = ref(null);
const archiveRef = ref(null); const archiveRef = ref(null);
const corpUserIds = ref({}); const corpUserIds = ref({});
const referenceCustomerIds = ref({}); const referenceCustomerIds = ref({});
const pendingAction = ref(null);
const awaitingArchiveBinding = ref(false);
const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info"; const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info";
const corpId = computed(() => team.value?.corpId); const corpId = computed(() => team.value?.corpId);
@ -87,7 +84,7 @@ function cacheCurrentTeam(currentTeam) {
function isSameTeam(candidate, target = {}) { function isSameTeam(candidate, target = {}) {
const targetTeamId = target.teamId || ""; const targetTeamId = target.teamId || "";
if (!candidate || !candidate.teamId || String(candidate.teamId) !== String(targetTeamId)) return false; if (!candidate || !candidate.teamId || candidate.teamId !== targetTeamId) return false;
const targetCorpId = normalizeCorpId(target.corpId || ""); const targetCorpId = normalizeCorpId(target.corpId || "");
if (!targetCorpId) return true; if (!targetCorpId) return true;
@ -98,183 +95,37 @@ function isSameTeam(candidate, target = {}) {
async function changeTeam({ teamId, corpId, corpName }) { async function changeTeam({ teamId, corpId, corpName }) {
loading.value = true; loading.value = true;
try { const res = await api("getTeamData", { teamId, corpId });
const res = await api("getTeamData", { teamId, corpId, withCorpName: true }); loading.value = false;
if (res && res.data) { if (res && res.data) {
team.value = { team.value = {
...res.data, ...res.data,
corpId: normalizeCorpId(res.data.corpId || corpId), corpId: normalizeCorpId(res.data.corpId || corpId),
}; };
team.value.corpName = corpName || res.data.corpName || res.data.corp_name || ""; team.value.corpName = corpName;
cacheCurrentTeam(team.value); cacheCurrentTeam(team.value);
return team.value; } else {
}
toast(res?.message || "获取团队信息失败"); toast(res?.message || "获取团队信息失败");
return null;
} finally {
loading.value = false;
} }
} }
function parsePendingAction(options = {}) {
const type = options.type || "";
if (type === "experienceCoupon" && options.issueId && options.corpId) {
return {
type,
corpId: normalizeCorpId(options.corpId),
teamId: options.teamId || "",
issueId: options.issueId,
memberId: options.memberId || options.customerId || "",
couponId: options.couponId || "",
unionid: options.unionid || "",
externalUserId: options.externalUserId || "",
};
}
if (type === "appointmentRegistration" && options.customerId && options.corpId) {
return {
type,
corpId: normalizeCorpId(options.corpId),
teamId: options.teamId || "",
customerId: options.customerId,
name: options.name || "",
corpName: options.corpName || "",
appointmentId: options.appointmentId || "",
month: options.month || "",
externalUserId: options.externalUserId || "",
corpUserId: options.corpUserId || "",
};
}
return null;
}
function buildActionUrl(action) {
const params = Object.entries(action)
.filter(([key, value]) => key !== "type" && value)
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
.join("&");
const page = action.type === "experienceCoupon"
? "/pages/experience-coupon/claim"
: "/pages/record/appointment-record";
return `${page}?${params}`;
}
function buildArchiveBindUrl(action) {
const customerId = action.memberId || action.customerId;
return [
`/pages/archive/edit-archive?corpId=${encodeURIComponent(action.corpId)}`,
`teamId=${encodeURIComponent(team.value?.teamId || action.teamId)}`,
`bindCustomerId=${encodeURIComponent(customerId)}`,
`source=${encodeURIComponent(action.type)}`,
].join("&");
}
async function syncPendingExternalUser(action) {
if (!action?.externalUserId || !account.value?.openid || !action.corpId) return;
try {
await api("syncWxappExternalUserRelation", {
corpId: action.corpId,
externalUserId: action.externalUserId,
unionid: action.unionid || account.value.unionid || "",
openid: account.value.openid,
corpUserId: action.corpUserId || "",
}, false);
} catch (error) {
console.warn("同步外部联系人关系失败", error);
}
}
async function continuePendingAction() {
const action = pendingAction.value;
if (!action || !account.value?.openid || !team.value?.teamId) return;
if (action.teamId && String(action.teamId) !== String(team.value.teamId)) {
toast("未找到目标服务团队");
pendingAction.value = null;
return;
}
await syncPendingExternalUser(action);
const customerId = action.memberId || action.customerId;
const customerRes = await api(
"getCustomerByCustomerId",
{ corpId: action.corpId, customerId },
false
);
const customer = customerRes?.data || null;
if (!customer) {
toast(customerRes?.message || "目标档案不存在");
pendingAction.value = null;
return;
}
const archiveBound = String(customer.miniAppId || "") === String(account.value.openid);
if (!archiveBound) {
if (awaitingArchiveBinding.value) {
awaitingArchiveBinding.value = false;
pendingAction.value = null;
return;
}
awaitingArchiveBinding.value = true;
uni.navigateTo({ url: buildArchiveBindUrl(action) });
return;
}
pendingAction.value = null;
awaitingArchiveBinding.value = false;
uni.navigateTo({ url: buildActionUrl(action) });
}
async function getMatchTeams(inviteTeam = null) { async function getMatchTeams(inviteTeam = null) {
loading.value = true; loading.value = true;
try { teams.value = await getTeams();
teams.value = await getTeams(); const inviteIdentity = typeof inviteTeam === "string" ? { teamId: inviteTeam } : (inviteTeam || {});
const inviteIdentity = typeof inviteTeam === "string" ? { teamId: inviteTeam } : (inviteTeam || {}); const cachedIdentity = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
const cachedIdentity = get(HOME_CURRENT_TEAM_CACHE_KEY) || {}; const targetIdentity = getTeamIdentity({
const targetIdentity = getTeamIdentity({ teamId: inviteIdentity.teamId || team.value?.teamId || cachedIdentity.teamId,
teamId: inviteIdentity.teamId || team.value?.teamId || cachedIdentity.teamId, corpId: inviteIdentity.corpId || team.value?.corpId || cachedIdentity.corpId,
corpId: inviteIdentity.corpId || team.value?.corpId || cachedIdentity.corpId, });
}); const validTeam = teams.value.find(i => isSameTeam(i, targetIdentity));
let validTeam = teams.value.find(i => isSameTeam(i, targetIdentity)); const firstTeam = teams.value[0]
if (validTeam || firstTeam) {
if (pendingAction.value && targetIdentity.teamId && account.value?.openid) { changeTeam(validTeam || firstTeam);
if (!validTeam) { } else {
const bindRes = await api("bindWxappWithTeam", {
appid,
corpId: targetIdentity.corpId,
teamId: targetIdentity.teamId,
openid: account.value.openid,
});
if (!bindRes?.success) {
toast(bindRes?.message || "关联医生团队失败");
team.value = null;
return null;
}
teams.value = await getTeams();
validTeam = teams.value.find(i => isSameTeam(i, targetIdentity));
}
// 链接中的团队是本次动作的作用域,即使用户已绑定多个团队,也必须切换到该团队。
const targetTeam = await changeTeam({
...targetIdentity,
corpName: validTeam?.corpName || "",
});
if (targetTeam) {
teams.value = teams.value.length ? teams.value : [targetTeam];
return targetTeam;
}
return null;
}
const firstTeam = teams.value[0];
if (validTeam || firstTeam) {
return await changeTeam(validTeam || firstTeam);
}
team.value = null; team.value = null;
return null;
} finally {
loading.value = false;
} }
loading.value = false;
} }
// useLoad((opts) => { // useLoad((opts) => {
@ -283,8 +134,8 @@ async function getMatchTeams(inviteTeam = null) {
// } // }
// }); // });
onLoad((options = {}) => { onLoad(() => {
pendingAction.value = parsePendingAction(options); if (!account.value) login();
}) })
onShow(async () => { onShow(async () => {
@ -298,11 +149,7 @@ onShow(async () => {
referenceCustomerIds.value[inviteTeam.teamId] = inviteTeam.referenceCustomerId; referenceCustomerIds.value[inviteTeam.teamId] = inviteTeam.referenceCustomerId;
} }
if (account.value && account.value.openid) { if (account.value && account.value.openid) {
const targetTeam = pendingAction.value?.teamId getMatchTeams(inviteTeam && inviteTeam.teamId ? inviteTeam : null);
? { teamId: pendingAction.value.teamId, corpId: pendingAction.value.corpId }
: null;
await getMatchTeams(targetTeam || (inviteTeam && inviteTeam.teamId ? inviteTeam : null));
await continuePendingAction();
} else { } else {
teams.value = []; teams.value = [];
} }
@ -314,7 +161,7 @@ onShow(async () => {
} }
}); });
onShareAppMessage(() => { onShareAppMessage((res) => {
if (team.value && team.value.supportPatientForward === 'YES') { if (team.value && team.value.supportPatientForward === 'YES') {
const customer = customers.value[0]; const customer = customers.value[0];
const referenceCustomerId = customer ? customer._id : ''; const referenceCustomerId = customer ? customer._id : '';
@ -326,9 +173,9 @@ onShareAppMessage(() => {
} }
}) })
watch(account, async (n, o) => { watch(account, (n, o) => {
if (n && !o) { if (n && !o) {
if (!pendingAction.value) await getMatchTeams(); getMatchTeams();
} else if (!n && o) { } else if (!n && o) {
teams.value = []; teams.value = [];
} }

View File

@ -133,12 +133,10 @@ async function bindTeam() {
} }
await syncExternalUserRelation(); await syncExternalUserRelation();
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId }); const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
if (redirectUrl.value) { if (res1 && res1.data > 0) {
await attempToPage(redirectUrl.value)
} else if (res1 && res1.data > 0) {
toHome(); toHome();
} else { } else {
attempToPage(`/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`) attempToPage(redirectUrl.value)
} }
} }
@ -149,7 +147,7 @@ async function getPhoneNumber(e) {
if (!res) return; if (!res) return;
} }
if (team.value) { if (team.value) {
await bindTeam() bindTeam()
} else if (redirectUrl.value) { } else if (redirectUrl.value) {
await attempToPage(redirectUrl.value); await attempToPage(redirectUrl.value);
} else { } else {
@ -158,33 +156,20 @@ async function getPhoneNumber(e) {
} }
async function attempToPage(url) { async function attempToPage(url) {
if (!url) { const res1 = attempRedirect(url);
toHome(); if (res1) return;
return; const res2 = attempSwitchTab(url);
} if (res2) return;
try { toHome();
await attempRedirect(url);
return;
} catch {
// redirectTo 不支持 tabBar 时再尝试 switchTab。
}
try {
await attempSwitchTab(url);
return;
} catch {
toHome();
}
} }
onLoad((opts) => { onLoad((opts) => {
redirectUrl.value = opts.redirectUrl || "";
if (opts.source === "teamInvite") { if (opts.source === "teamInvite") {
team.value = get("invite-team-info"); team.value = get("invite-team-info");
if (!redirectUrl.value && team.value) { redirectUrl.value = `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
redirectUrl.value = `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
}
return; return;
} }
redirectUrl.value = opts.redirectUrl || "";
}); });
</script> </script>
<style scoped> <style scoped>

View File

@ -143,47 +143,24 @@ async function bindTeam({ corpUserId, externalUserId }) {
} }
} }
function normalizePlainOptions(options = {}) {
return Object.keys(options || {}).reduce((acc, key) => {
acc[key] = typeof options[key] === 'string' ? safeDecode(options[key]) : options[key];
return acc;
}, {})
}
onLoad((options) => { onLoad((options) => {
const normalizedOptions = normalizePlainOptions(options); if (options.q) {
if (normalizedOptions.q) { opts.value = JSON.stringify(options)
opts.value = JSON.stringify(normalizedOptions) changeTeam(parseInviteOptions(options));
changeTeam(parseInviteOptions(normalizedOptions)); } else if (options.type === 'experienceCoupon' && options.issueId && options.corpId) {
} else if (normalizedOptions.type === 'experienceCoupon' && normalizedOptions.issueId && normalizedOptions.corpId) {
const params = [ const params = [
`corpId=${encodeURIComponent(normalizedOptions.corpId || "")}`, `corpId=${encodeURIComponent(options.corpId || "")}`,
`issueId=${encodeURIComponent(normalizedOptions.issueId || "")}`, `issueId=${encodeURIComponent(options.issueId || "")}`,
normalizedOptions.memberId ? `memberId=${encodeURIComponent(normalizedOptions.memberId)}` : "", options.memberId ? `memberId=${encodeURIComponent(options.memberId)}` : "",
normalizedOptions.couponId ? `couponId=${encodeURIComponent(normalizedOptions.couponId)}` : "", options.couponId ? `couponId=${encodeURIComponent(options.couponId)}` : "",
normalizedOptions.unionid ? `unionid=${encodeURIComponent(normalizedOptions.unionid)}` : "", options.unionid ? `unionid=${encodeURIComponent(options.unionid)}` : "",
normalizedOptions.externalUserId ? `externalUserId=${encodeURIComponent(normalizedOptions.externalUserId)}` : "", options.externalUserId ? `externalUserId=${encodeURIComponent(options.externalUserId)}` : "",
].filter(Boolean).join("&"); ].filter(Boolean).join("&");
uni.redirectTo({ uni.redirectTo({
url: `/pages/experience-coupon/claim?${params}`, url: `/pages/experience-coupon/claim?${params}`,
}); });
} else if (normalizedOptions.type === 'appointmentRegistration' && normalizedOptions.customerId && normalizedOptions.corpId) { } else if (options.type === 'archive' || (options.teamId && options.corpId)) {
const params = [ changeTeam(options);
`corpId=${encodeURIComponent(normalizedOptions.corpId || "")}`,
normalizedOptions.teamId ? `teamId=${encodeURIComponent(normalizedOptions.teamId)}` : "",
`customerId=${encodeURIComponent(normalizedOptions.customerId || "")}`,
normalizedOptions.name ? `name=${encodeURIComponent(normalizedOptions.name)}` : "",
normalizedOptions.corpName ? `corpName=${encodeURIComponent(normalizedOptions.corpName)}` : "",
normalizedOptions.appointmentId ? `appointmentId=${encodeURIComponent(normalizedOptions.appointmentId)}` : "",
normalizedOptions.month ? `month=${encodeURIComponent(normalizedOptions.month)}` : "",
normalizedOptions.externalUserId ? `externalUserId=${encodeURIComponent(normalizedOptions.externalUserId)}` : "",
normalizedOptions.corpUserId ? `corpUserId=${encodeURIComponent(normalizedOptions.corpUserId)}` : "",
].filter(Boolean).join("&");
uni.redirectTo({
url: `/pages/record/appointment-record?${params}`,
});
} else if (normalizedOptions.type === 'archive' || (normalizedOptions.teamId && normalizedOptions.corpId)) {
changeTeam(normalizedOptions);
} }
}); });

View File

@ -1231,25 +1231,6 @@ $primary-color: #0877F1;
display: none !important; display: none !important;
} }
.custom-bubble,
.benefit-bubble {
padding: 0 !important;
border-radius: 0 !important;
background: transparent !important;
overflow: visible;
}
.custom-bubble::before,
.custom-bubble::after,
.benefit-bubble::before,
.benefit-bubble::after {
display: none !important;
}
.benefit-bubble {
max-width: 100%;
}
.message-right .message-card { .message-right .message-card {
margin-right: 8rpx; margin-right: 8rpx;
} }

View File

@ -59,28 +59,6 @@
mode="aspectFill" /> mode="aspectFill" />
</view> </view>
<!-- 权益消息 -->
<view v-else-if="getCustomMessageType(message) === 'benefit'" class="benefit-card">
<view class="benefit-title">{{ benefitData.title }}</view>
<view class="benefit-content">
<view v-for="(item, index) in benefitData.items" :key="`${item.projectId}-${index}`" class="benefit-item">
<view class="benefit-project-row">
<text v-if="index === 0" class="benefit-label">项目:</text>
<text v-else class="benefit-label-placeholder"></text>
<text class="benefit-project-name">{{ item.projectName }}</text>
<text class="benefit-count">X{{ item.usageCount }}</text>
</view>
<view class="benefit-valid-row">
<text class="benefit-valid-label">有效期:</text>
<text class="benefit-valid-value">{{ item.validTimeText || '无限期' }}</text>
</view>
</view>
<view class="benefit-detail-button" @click.stop="handleBenefitClick">
<text class="benefit-detail-text">查看详情</text>
</view>
</view>
</view>
<!-- 其他自定义消息 --> <!-- 其他自定义消息 -->
<!-- <view <!-- <view
v-else v-else
@ -134,32 +112,8 @@ const payloadData = computed(() => {
} }
}) })
const benefitData = computed(() => ({
title: payloadData.value.title || "医生为您录入专属权益",
patientId: payloadData.value.patientId || "",
patientName: payloadData.value.patientName || "",
corpId: payloadData.value.corpId || props.corpId || "",
teamId: payloadData.value.teamId || "",
items: Array.isArray(payloadData.value.items) ? payloadData.value.items : [],
}))
const handleBenefitClick = () => { // 计算图片样式
const customerId = benefitData.value.patientId;
const corpId = benefitData.value.corpId;
if (!customerId || !corpId) {
toast("权益信息不完整");
return;
}
const params = [
`corpId=${encodeURIComponent(corpId)}`,
`teamId=${encodeURIComponent(benefitData.value.teamId || "")}`,
`customerId=${encodeURIComponent(customerId)}`,
`name=${encodeURIComponent(benefitData.value.patientName || "")}`,
].join("&");
uni.navigateTo({
url: `/pages/experience-coupon/my-rights?${params}`,
});
};
const getImageStyle = (imageInfo) => { const getImageStyle = (imageInfo) => {
// 如果没有尺寸信息,使用默认样式 // 如果没有尺寸信息,使用默认样式
imageInfo.width = imageInfo.width || imageInfo.Width; imageInfo.width = imageInfo.width || imageInfo.Width;
@ -345,95 +299,4 @@ async function markArticleRead(sendId, articleId) {
<style scoped lang="scss"> <style scoped lang="scss">
@import "../chat.scss"; @import "../chat.scss";
.benefit-card {
width: 520rpx;
max-width: 100%;
box-sizing: border-box;
border-radius: 10rpx;
padding: 22rpx 18rpx 18rpx;
background: linear-gradient(180deg, #0b75f2 0%, #0067df 100%);
}
.benefit-title {
color: #fff;
font-size: 32rpx;
font-weight: 600;
line-height: 1.3;
margin-bottom: 18rpx;
}
.benefit-content {
border: 4rpx solid rgba(255, 255, 255, 0.28);
border-radius: 8rpx;
padding: 24rpx 24rpx 28rpx;
background: #fff;
}
.benefit-item + .benefit-item {
margin-top: 18rpx;
}
.benefit-project-row {
display: flex;
align-items: center;
min-width: 0;
}
.benefit-label,
.benefit-label-placeholder {
flex-shrink: 0;
width: 104rpx;
color: #8b8f96;
font-size: 28rpx;
line-height: 1.4;
}
.benefit-project-name {
flex: 1;
min-width: 0;
color: #111827;
font-size: 30rpx;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.benefit-count {
flex-shrink: 0;
margin-left: 16rpx;
color: #111827;
font-size: 30rpx;
line-height: 1.4;
}
.benefit-valid-row {
display: flex;
margin-left: 104rpx;
margin-top: 8rpx;
}
.benefit-valid-label,
.benefit-valid-value {
color: #8b8f96;
font-size: 26rpx;
line-height: 1.4;
}
.benefit-detail-button {
height: 82rpx;
margin-top: 28rpx;
border-radius: 8rpx;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #0b75f2 0%, #0067df 100%);
}
.benefit-detail-text {
color: #fff;
font-size: 30rpx;
font-weight: 500;
}
</style> </style>

View File

@ -20,10 +20,10 @@
</view> </view>
<!-- 聊天消息区域 --> <!-- 聊天消息区域 -->
<scroll-view class="chat-content" :style="{ <scroll-view class="chat-content" :style="{
bottom: (keyboardHeight > 0 ? keyboardHeight + 60 : 60) + 'px', bottom: (keyboardHeight > 0 ? keyboardHeight + 60 : 60) + 'px',
}" scroll-y="true" enhanced="true" bounces="false" :scroll-into-view="scrollIntoView" @scroll="onScroll" }" scroll-y="true" enhanced="true" bounces="false" :scroll-into-view="scrollIntoView" @scroll="onScroll"
@scrolltoupper="handleScrollToUpper" @click="closeMorePanel" ref="chatScrollView"> @scrolltoupper="handleScrollToUpper" @click="closeMorePanel" ref="chatScrollView">
<!-- 加载更多提示 --> <!-- 加载更多提示 -->
<view class="load-more-tip" v-if="messageList.length >= 15"> <view class="load-more-tip" v-if="messageList.length >= 15">
<view class="loading" v-if="isLoadingMore"> <view class="loading" v-if="isLoadingMore">
@ -390,19 +390,6 @@ function handleSystemMessageReceived(message) {
} }
} }
// 获取自定义消息类型
function getCustomMessageType(message) {
try {
const data =
typeof message.payload?.data === "string"
? JSON.parse(message.payload.data)
: message.payload?.data || {};
return data?.type || "";
} catch (error) {
return "";
}
}
// 获取消息气泡样式类 // 获取消息气泡样式类
function getBubbleClass(message) { function getBubbleClass(message) {
// 图片消息不需要气泡背景 // 图片消息不需要气泡背景
@ -410,10 +397,7 @@ function getBubbleClass(message) {
return "image-bubble"; return "image-bubble";
} }
if (message.type === "TIMCustomElem") { if (message.type === "TIMCustomElem") {
if (getCustomMessageType(message) === "benefit") { return message.flow === "out" ? "" : "";
return "benefit-bubble";
}
return "custom-bubble";
} }
return message.flow === "out" ? "user-bubble" : "doctor-bubble"; return message.flow === "out" ? "user-bubble" : "doctor-bubble";
} }
@ -588,44 +572,44 @@ const initTIMCallbacks = async () => {
) { ) {
seenIds.add(message.ID); seenIds.add(message.ID);
uniqueMessages.push(message); uniqueMessages.push(message);
} }
}); });
const mergedMessages = []; const mergedMessages = [];
const mergedSeenIds = new Set(); const mergedSeenIds = new Set();
const existingMessages = Array.isArray(messageList.value) const existingMessages = Array.isArray(messageList.value)
? messageList.value ? messageList.value
: []; : [];
existingMessages.forEach((message) => { existingMessages.forEach((message) => {
if (!message?.ID) return; if (!message?.ID) return;
if (message.conversationID !== chatInfo.value.conversationID) return; if (message.conversationID !== chatInfo.value.conversationID) return;
if (mergedSeenIds.has(message.ID)) return; if (mergedSeenIds.has(message.ID)) return;
mergedSeenIds.add(message.ID); mergedSeenIds.add(message.ID);
mergedMessages.push(message); mergedMessages.push(message);
}); });
uniqueMessages.forEach((message) => { uniqueMessages.forEach((message) => {
if (!message?.ID) return; if (!message?.ID) return;
if (mergedSeenIds.has(message.ID)) return; if (mergedSeenIds.has(message.ID)) return;
mergedSeenIds.add(message.ID); mergedSeenIds.add(message.ID);
mergedMessages.push(message); mergedMessages.push(message);
}); });
mergedMessages.sort((a, b) => { mergedMessages.sort((a, b) => {
const ta = Number(a?.lastTime || a?.time || 0) || 0; const ta = Number(a?.lastTime || a?.time || 0) || 0;
const tb = Number(b?.lastTime || b?.time || 0) || 0; const tb = Number(b?.lastTime || b?.time || 0) || 0;
return ta - tb; return ta - tb;
}); });
messageList.value = mergedMessages; messageList.value = mergedMessages;
console.log( console.log(
"消息列表已更新,原始", "消息列表已更新,原始",
messages.length, messages.length,
"条,过滤后", "条,过滤后",
messageList.value.length, messageList.value.length,
"条消息" "条消息"
); );
isCompleted.value = data.isCompleted || false; isCompleted.value = data.isCompleted || false;
isLoadingMore.value = false; isLoadingMore.value = false;
@ -793,11 +777,11 @@ const scrollToBottom = (immediate = false) => {
}; };
// 关闭功能栏 // 关闭功能栏
const closeMorePanel = () => { const closeMorePanel = () => {
uni.$emit("closeMorePanel"); uni.$emit("closeMorePanel");
chatInputRef.value?.blurInput?.(); chatInputRef.value?.blurInput?.();
uni.hideKeyboard(); uni.hideKeyboard();
}; };
// 滚动事件 // 滚动事件
const onScroll = throttle((e) => { const onScroll = throttle((e) => {

View File

@ -61,20 +61,11 @@
<script setup> <script setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { onLoad, onShow } from "@dcloudio/uni-app"; import { onLoad, onShow } from "@dcloudio/uni-app";
import { storeToRefs } from "pinia";
import useAccount from "@/store/account";
import api from "@/utils/api"; import api from "@/utils/api";
import { normalizeCorpId } from "@/utils/api-base-config"; import { normalizeCorpId } from "@/utils/api-base-config";
import { useTeamAccess } from "@/hooks/use-team-access"; import { toast } from "@/utils/widget";
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
import FullPage from "@/components/full-page.vue"; import FullPage from "@/components/full-page.vue";
const env = __VITE_ENV__;
const appid = env.MP_WX_APP_ID;
const { account, externalUserId } = storeToRefs(useAccount());
const { getTeams, getExternalUserId, login } = useAccount();
const { openTeamLogin: openTeamInviteLogin, ensureTeamAdded: ensureJoinedTeam } = useTeamAccess();
const corpId = ref(""); const corpId = ref("");
const teamId = ref(""); const teamId = ref("");
const customerId = ref(""); const customerId = ref("");
@ -85,10 +76,6 @@ const records = ref([]);
const staffNameMap = ref({}); const staffNameMap = ref({});
const loading = ref(false); const loading = ref(false);
const refreshing = ref(false); const refreshing = ref(false);
const corpUserId = ref("");
const routeExternalUserId = ref("");
const authPending = ref(false);
const initialized = ref(false);
const statusMap = { const statusMap = {
pending: { label: "未到院" }, pending: { label: "未到院" },
@ -99,126 +86,6 @@ const statusMap = {
const avatarText = computed(() => (customerName.value || "档案").slice(0, 1)); const avatarText = computed(() => (customerName.value || "档案").slice(0, 1));
const monthText = computed(() => selectedMonth.value.replace("-", "年") + "月"); const monthText = computed(() => selectedMonth.value.replace("-", "年") + "月");
function buildCurrentUrl() {
const params = [
`corpId=${encodeURIComponent(corpId.value || "")}`,
`teamId=${encodeURIComponent(teamId.value || "")}`,
customerId.value ? `customerId=${encodeURIComponent(customerId.value)}` : "",
customerName.value ? `name=${encodeURIComponent(customerName.value)}` : "",
corpName.value ? `corpName=${encodeURIComponent(corpName.value)}` : "",
corpUserId.value ? `corpUserId=${encodeURIComponent(corpUserId.value)}` : "",
routeExternalUserId.value ? `externalUserId=${encodeURIComponent(routeExternalUserId.value)}` : "",
selectedMonth.value ? `month=${encodeURIComponent(selectedMonth.value)}` : "",
].filter(Boolean).join("&");
return `/pages/record/appointment-record?${params}`;
}
async function ensureLogin() {
if (!account.value) await login();
if (!account.value) {
await ensureCustomerContext();
await openTeamLogin();
return false;
}
return true;
}
async function ensurePhoneAuthorized() {
if (account.value?.mobile) return true;
await ensureCustomerContext();
return openTeamLogin();
}
async function openTeamLogin() {
if (!corpId.value || !teamId.value) {
toast("预约团队信息缺失,暂无法查看预约记录");
return false;
}
const res = await openTeamInviteLogin({
corpId: corpId.value,
teamId: teamId.value,
corpUserId: corpUserId.value || "",
externalUserId: routeExternalUserId.value || externalUserId.value || "",
fallbackCorpName: corpName.value || "",
redirectUrl: buildCurrentUrl(),
});
if (!res.success) toast(res.message);
return false;
}
async function ensureCustomerContext() {
if (!corpId.value || !customerId.value) return null;
const res = await api("getCustomerByCustomerId", { corpId: corpId.value, customerId: customerId.value }, false);
const customer = res?.success && res.data ? res.data : null;
if (!customer) return null;
if (!customerName.value && customer.name) customerName.value = customer.name;
const teamIds = Array.isArray(customer.teamId) ? customer.teamId : [customer.teamId].filter(Boolean);
if (!teamId.value) teamId.value = teamIds.find(Boolean) || "";
return customer;
}
async function ensureTeamAdded() {
await ensureCustomerContext();
if (!corpId.value || !teamId.value) {
toast("预约团队信息缺失,暂无法查看预约记录");
return false;
}
showLoading("添加团队中...");
try {
const res = await ensureJoinedTeam({
appid,
account: account.value,
getTeams,
corpId: corpId.value,
teamId: teamId.value,
});
if (!res.success) {
toast(res.message);
return false;
}
return true;
} finally {
hideLoading();
}
}
async function ensureArchiveBound() {
const miniAppId = account.value?.openid || "";
if (!miniAppId || !corpId.value || !customerId.value) return false;
const customer = await ensureCustomerContext();
if (customer && String(customer.miniAppId || "") === String(miniAppId)) return true;
authPending.value = true;
uni.navigateTo({
url: `/pages/archive/edit-archive?corpId=${encodeURIComponent(corpId.value)}&teamId=${encodeURIComponent(teamId.value)}&bindCustomerId=${encodeURIComponent(customerId.value)}&source=appointmentRegistration`,
});
return false;
}
async function ensureAccessReady() {
await ensureCustomerContext();
const okLogin = await ensureLogin();
if (!okLogin) return false;
const okPhone = await ensurePhoneAuthorized();
if (!okPhone) return false;
await ensureCustomerContext();
if (corpId.value) await getExternalUserId(corpId.value);
const okTeam = await ensureTeamAdded();
if (!okTeam) return false;
const okArchive = await ensureArchiveBound();
if (!okArchive) return false;
return true;
}
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 pad(value) { function pad(value) {
return String(value).padStart(2, "0"); return String(value).padStart(2, "0");
} }
@ -259,13 +126,13 @@ function statusText(item) {
} }
function appointmentTypeText(item) { function appointmentTypeText(item) {
if (item?.appointmentType === "followup") return "复诊预约"; if (item?.appointmentType === "schedule") return item?.scheduleTtile || "排班";
if (item?.appointmentType === "treatment") return "治疗预约"; if (item?.appointmentType === "medical") return "复诊预约";
return "其他预约"; return "治疗预约";
} }
function practitionerLabel(item) { function practitionerLabel(item) {
return item?.appointmentType === "treatment" ? "治疗师" : "医生"; return item?.appointmentType === "medical" ? "医生" : "治疗师";
} }
function practitionerText(item) { function practitionerText(item) {
@ -376,28 +243,13 @@ onLoad(async (options = {}) => {
customerId.value = options.customerId || options.id || ""; customerId.value = options.customerId || options.id || "";
customerName.value = options.name ? decodeURIComponent(options.name) : ""; customerName.value = options.name ? decodeURIComponent(options.name) : "";
corpName.value = options.corpName ? decodeURIComponent(options.corpName) : ""; corpName.value = options.corpName ? decodeURIComponent(options.corpName) : "";
corpUserId.value = options.corpUserId || "";
routeExternalUserId.value = options.externalUserId || "";
if (options.month) selectedMonth.value = options.month; if (options.month) selectedMonth.value = options.month;
await loadCorpName();
const ready = await ensureAccessReady();
if (!ready) return;
initialized.value = true;
await loadStaffNameMap(); await loadStaffNameMap();
await loadRecords(); await loadRecords();
}); });
onShow(async () => { onShow(() => {
if (authPending.value) { if (corpId.value && customerId.value) loadRecords();
authPending.value = false;
const ready = await ensureAccessReady();
if (!ready) return;
initialized.value = true;
await loadStaffNameMap();
await loadRecords();
return;
}
if (initialized.value && corpId.value && customerId.value) loadRecords();
}); });
</script> </script>
@ -535,4 +387,4 @@ onShow(async () => {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #999;
} }
</style> </style>

View File

@ -47,6 +47,18 @@
<text class="row-value strong">{{ item.treatmentDoctorName || item.doctorName || <text class="row-value strong">{{ item.treatmentDoctorName || item.doctorName ||
staffText(item.treatmentDoctorUserId) }}</text> staffText(item.treatmentDoctorUserId) }}</text>
</view> </view>
<view v-if="assistantText(item)" class="record-row">
<text class="row-label">【配台】</text>
<text class="row-value">{{ assistantText(item) }}</text>
</view>
<view v-if="item.treatmentArea" class="record-row">
<text class="row-label">【治疗部位】</text>
<text class="row-value">{{ item.treatmentArea }}</text>
</view>
<view v-if="item.treatmentRemark" class="record-row">
<text class="row-label">【治疗备注】</text>
<text class="row-value">{{ item.treatmentRemark }}</text>
</view>
</view> </view>
</view> </view>
</view> </view>
@ -76,16 +88,6 @@ const refreshing = ref(false);
const avatarText = computed(() => (customerName.value || "档案").slice(0, 1)); const avatarText = computed(() => (customerName.value || "档案").slice(0, 1));
const monthText = computed(() => selectedMonth.value.replace("-", "年") + "月"); const monthText = computed(() => selectedMonth.value.replace("-", "年") + "月");
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 pad(value) { function pad(value) {
return String(value).padStart(2, "0"); return String(value).padStart(2, "0");
} }
@ -209,8 +211,8 @@ onLoad(async (options = {}) => {
teamId.value = options.teamId || ""; teamId.value = options.teamId || "";
customerId.value = options.customerId || options.id || ""; customerId.value = options.customerId || options.id || "";
customerName.value = options.name ? decodeURIComponent(options.name) : ""; customerName.value = options.name ? decodeURIComponent(options.name) : "";
corpName.value = options.corpName ? decodeURIComponent(options.corpName) : "";
if (options.month) selectedMonth.value = options.month; if (options.month) selectedMonth.value = options.month;
await loadCorpName();
await loadStaffNameMap(); await loadStaffNameMap();
await loadRecords(); await loadRecords();
}); });

View File

@ -159,12 +159,11 @@ export default defineStore("accountStore", () => {
const normalizedCorpId = normalizeCorpId(corpId); const normalizedCorpId = normalizeCorpId(corpId);
const unionid = account.value?.unionid; const unionid = account.value?.unionid;
const openid = account.value?.openid; const openid = account.value?.openid;
const miniAppId = account.value?.openid;
if (!(normalizedCorpId && unionid && openid)) { if (!(normalizedCorpId && unionid && openid)) {
externalUserId.value = ''; externalUserId.value = '';
return return
}; };
const res = await api('getUnionidToExternalUserid', { unionid, openid, corpId: normalizedCorpId, miniAppId }, false); const res = await api('getUnionidToExternalUserid', { unionid, openid, corpId: normalizedCorpId }, false);
const id = res && res.success && typeof res.data === 'string' && res.data.trim() ? res.data.trim() : ''; const id = res && res.success && typeof res.data === 'string' && res.data.trim() ? res.data.trim() : '';
externalUserId.value = id; externalUserId.value = id;
return id; return id;

View File

@ -21,7 +21,6 @@ const urlsConfig = {
recordBusinessCardBehavior: 'recordBusinessCardBehavior', recordBusinessCardBehavior: 'recordBusinessCardBehavior',
getJoinedTeams: 'getJoinedTeams', getJoinedTeams: 'getJoinedTeams',
getCorpBusinessCardStatus: 'getCorpBusinessCardStatus', getCorpBusinessCardStatus: 'getCorpBusinessCardStatus',
getCorpInfo: 'getCorpInfo',
}, },
knowledgeBase: { knowledgeBase: {
@ -63,7 +62,6 @@ const urlsConfig = {
getExperienceCouponIssueDetail: "getExperienceCouponIssueDetail", getExperienceCouponIssueDetail: "getExperienceCouponIssueDetail",
getExperienceCouponIssueList: "getExperienceCouponIssueList", getExperienceCouponIssueList: "getExperienceCouponIssueList",
getMiniAppCustomers: 'getMiniAppCustomers', getMiniAppCustomers: 'getMiniAppCustomers',
getCorpMiniAppCustomers: 'getCorpMiniAppCustomers',
getTeamCustomers: 'getTeamCustomers', getTeamCustomers: 'getTeamCustomers',
getTreatmentRecord: "getTreatmentRecord", getTreatmentRecord: "getTreatmentRecord",
getDeductRecord: "getDeductRecord", getDeductRecord: "getDeductRecord",