Compare commits
29 Commits
main
...
dev-linpin
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f5da96a83 | ||
|
|
16c24b036f | ||
|
|
a368474ffb | ||
|
|
5d0da3af7c | ||
|
|
1ae7b23641 | ||
| 57803051e1 | |||
| fe7583017d | |||
| 94a515924b | |||
|
|
56c08eb255 | ||
| bbe50bdd72 | |||
| 6aa126bc81 | |||
| 58c9329eb6 | |||
| 9f5331cd3b | |||
|
|
fdf9428f36 | ||
| 92fe63f308 | |||
|
|
3770881c92 | ||
|
|
2dc3f07802 | ||
| edb9ee5c38 | |||
| b1a6e58a6f | |||
| d9b271e757 | |||
| e23ab0f6d3 | |||
|
|
68436fc7f4 | ||
|
|
2ac04ba97e | ||
|
|
bbf9e81f1f | ||
|
|
f13e6ede64 | ||
|
|
68c6db7422 | ||
|
|
dace2bb2da | ||
|
|
1654b4267e | ||
|
|
ea9535be7b |
47
components/form-template/form-cell/form-evaluated-joint.vue
Normal file
47
components/form-template/form-cell/form-evaluated-joint.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<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>
|
||||
48
components/form-template/form-cell/form-exercise-level.vue
Normal file
48
components/form-template/form-cell/form-exercise-level.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<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>
|
||||
58
components/form-template/form-cell/form-select-and-other.vue
Normal file
58
components/form-template/form-cell/form-select-and-other.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<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>
|
||||
@ -10,6 +10,12 @@
|
||||
@change="change" />
|
||||
<form-select v-else-if="attrs.type === 'select'" v-bind="attrs" :form="form" :disableChange="disableChange"
|
||||
@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"
|
||||
@change="change" />
|
||||
<form-mult-disease v-else-if="attrs.type === 'selfMultipleDiseases'" v-bind="attrs" :form="form" @change="change"
|
||||
@ -33,6 +39,9 @@ import { useAttrs } from 'vue';
|
||||
|
||||
import formInput from './form-input.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 formDatepicker from './form-datepicker.vue';
|
||||
import formRegion from './form-region.vue';
|
||||
@ -108,4 +117,4 @@ export default {
|
||||
}
|
||||
</script> -->
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
@ -33,7 +33,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const formCellType = ['input', 'select', 'date', 'radio', 'region', 'textarea', 'multiSelectAndOther', 'selfMultipleDiseases', 'files','diagnosis','positiveFind'];
|
||||
const formCellType = ['input', 'select', 'date', 'radio', 'region', 'textarea', 'multiSelectAndOther', 'selectAndOther', 'selfMultipleDiseases', 'files', 'diagnosis', 'positiveFind', 'evaluatedJoint', 'exerciseLevel'];
|
||||
const formCellTitle = ['surgicalHistory'];
|
||||
const customCellType = ['BMI', 'bloodPressure'];
|
||||
const disabledMap = computed(() => props.disableTitles.reduce((m, i) => {
|
||||
|
||||
91
hooks/use-team-access.js
Normal file
91
hooks/use-team-access.js
Normal file
@ -0,0 +1,91 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
41
pages.json
41
pages.json
@ -156,6 +156,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/ai-consult",
|
||||
"name": "ai-consult",
|
||||
"pages": [
|
||||
{
|
||||
"path": "list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "咨询记录",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "咨询助理",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "entry",
|
||||
"style": {
|
||||
"navigationBarTitleText": "咨询助理",
|
||||
"disableScroll": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"root": "pages/experience-coupon",
|
||||
"name": "experience-coupon",
|
||||
@ -235,6 +262,20 @@
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "step-edit-archive",
|
||||
"style": {
|
||||
"navigationBarTitleText": "新增档案",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "fill-his-archive",
|
||||
"style": {
|
||||
"navigationBarTitleText": "完善信息",
|
||||
"disableScroll": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "archive-result",
|
||||
"style": {
|
||||
|
||||
161
pages/ai-consult/chat.vue
Normal file
161
pages/ai-consult/chat.vue
Normal file
@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<view class="chat-page">
|
||||
<scroll-view scroll-y class="chat-content" :scroll-into-view="bottomId">
|
||||
<view class="message-list">
|
||||
<view v-for="(item, index) in messages" :key="item._id" :id="`message-${item._id}`" class="message-item" :class="`message-${item.sender}`">
|
||||
<view v-if="shouldShowTime(item, index)" class="time-divider">{{ formatMessageTime(item.createTime) }}</view>
|
||||
<evaluation-card v-if="item.messageType === 'consult_ended' && (item.rateId || session?.rateId)" class="rate-card" :extension="{ rateId: item.rateId || session.rateId }" :corp-id="corpId" :doctor-info="rateAssistantInfo" />
|
||||
<view v-else-if="item.sender === 'system' && item.messageType !== 'risk_notice'" class="system-message" :class="{ warn: item.messageType === 'sensitive_notice' }">{{ displayContent(item) }}</view>
|
||||
<view v-else-if="item.messageType !== 'risk_notice'" class="message-content">
|
||||
<image v-if="item.sender === 'assistant'" class="avatar assistant-avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" />
|
||||
<view class="message-bubble-container">
|
||||
<view v-if="item.sender === 'assistant'" class="username-label">{{ assistantDisplayName }}</view>
|
||||
<view class="message-bubble" :class="{ pending: item._sendStatus === 'pending' }">
|
||||
<text class="message-text">{{ displayContent(item) }}</text>
|
||||
</view>
|
||||
<text v-if="item._sendStatus === 'failed'" class="send-status">发送失败,请重试</text>
|
||||
</view>
|
||||
<view v-if="item.sender === 'patient'" class="avatar patient-avatar">我</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="waitingAi" class="message-item message-assistant">
|
||||
<view class="message-content"><image class="avatar assistant-avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" /><view class="message-bubble-container"><view class="username-label">{{ assistantDisplayName }}</view><view class="message-bubble thinking"><view class="thinking-dot"></view><view class="thinking-dot"></view><view class="thinking-dot"></view></view></view></view>
|
||||
</view>
|
||||
<view id="bottom" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view v-if="active" class="input-section">
|
||||
<textarea v-model="content" class="text-input" maxlength="1000" auto-height confirm-type="send" :show-confirm-bar="false" placeholder="请输入消息" @confirm="send" />
|
||||
<button class="send-btn" :disabled="!content.trim()" @click="send">发送</button>
|
||||
</view>
|
||||
<view v-else class="ended">本次咨询已结束</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick, computed } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import EvaluationCard from '@/pages/message/components/special-message/evaluation.vue';
|
||||
import { removeAiLabel } from '@/utils/ai-consult-display';
|
||||
|
||||
const { openid } = storeToRefs(useAccountStore());
|
||||
const corpId = ref('');
|
||||
const sessionId = ref('');
|
||||
const session = ref(null);
|
||||
const messages = ref([]);
|
||||
const content = ref('');
|
||||
const pendingRequestCount = ref(0);
|
||||
const waitingAi = ref(false);
|
||||
const bottomId = ref('');
|
||||
const active = ref(false);
|
||||
const assistantDisplayName = computed(() => removeAiLabel(session.value?.assistantName, '咨询助理'));
|
||||
const rateAssistantInfo = computed(() => ({ name: assistantDisplayName.value, title: '咨询助理', department: session.value?.teamName || '', avatar: '' }));
|
||||
|
||||
function displayContent(item) {
|
||||
const value = item?.content || '';
|
||||
if (typeof value !== 'string' || item?.sender === 'patient') return value;
|
||||
if (item?.sender !== 'assistant') return removeAiLabel(value);
|
||||
const contents = [];
|
||||
const matcher = /"streamContent"\s*:\s*"((?:\\.|[^"\\])*)"/g;
|
||||
let match;
|
||||
while ((match = matcher.exec(value))) {
|
||||
try {
|
||||
const text = JSON.parse(`"${match[1]}"`);
|
||||
if (text) contents.push(text);
|
||||
} catch (error) {
|
||||
// 兼容历史会话中无法解析的单段协议消息。
|
||||
}
|
||||
}
|
||||
return removeAiLabel(contents.join('') || value);
|
||||
}
|
||||
function formatMessageTime(value) {
|
||||
const date = new Date(Number(value));
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
function shouldShowTime(item, index) {
|
||||
if (index === 0) return Boolean(item?.createTime);
|
||||
return Number(item?.createTime) - Number(messages.value[index - 1]?.createTime) > 5 * 60 * 1000;
|
||||
}
|
||||
async function scrollToBottom() {
|
||||
bottomId.value = '';
|
||||
await nextTick();
|
||||
bottomId.value = 'bottom';
|
||||
}
|
||||
async function detail() {
|
||||
const res = await api('getAiConsultSessionDetail', { corpId: corpId.value, sessionId: sessionId.value, miniAppId: openid.value || uni.getStorageSync('openid') }, false);
|
||||
if (!res?.success) return uni.showToast({ title: removeAiLabel(res?.message, '加载失败'), icon: 'none' });
|
||||
session.value = res.data.session;
|
||||
messages.value = res.data.messages || [];
|
||||
active.value = session.value.status === 'active';
|
||||
uni.setNavigationBarTitle({ title: assistantDisplayName.value });
|
||||
await scrollToBottom();
|
||||
}
|
||||
async function send() {
|
||||
if (!active.value || !content.value.trim()) return;
|
||||
const value = content.value.trim();
|
||||
const localMessage = { _id: `local-${Date.now()}`, sender: 'patient', content: value, messageType: 'text', createTime: Date.now(), _sendStatus: 'pending' };
|
||||
messages.value.push(localMessage);
|
||||
content.value = '';
|
||||
pendingRequestCount.value += 1;
|
||||
waitingAi.value = true;
|
||||
await scrollToBottom();
|
||||
try {
|
||||
const res = await api('sendAiConsultMessage', { corpId: corpId.value, sessionId: sessionId.value, customerId: session.value.customerId, miniAppId: openid.value || uni.getStorageSync('openid'), content: value }, false);
|
||||
if (!res?.success) {
|
||||
localMessage._sendStatus = 'failed';
|
||||
uni.showToast({ title: removeAiLabel(res?.message, '发送失败'), icon: 'none' });
|
||||
return;
|
||||
}
|
||||
await detail();
|
||||
} catch (error) {
|
||||
localMessage._sendStatus = 'failed';
|
||||
uni.showToast({ title: '发送失败,请重试', icon: 'none' });
|
||||
} finally {
|
||||
pendingRequestCount.value -= 1;
|
||||
waitingAi.value = pendingRequestCount.value > 0;
|
||||
await scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
onLoad(opts => { corpId.value = opts.corpId; sessionId.value = opts.sessionId; detail(); });
|
||||
onShow(detail);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-page { height: 100vh; display: flex; flex-direction: column; background: #f5f7fb; overflow: hidden; }
|
||||
.chat-content { flex: 1; height: 0; box-sizing: border-box; }
|
||||
.message-list { padding: 28rpx 24rpx 40rpx; }
|
||||
.message-item { margin-bottom: 28rpx; }
|
||||
.message-content { display: flex; align-items: flex-start; gap: 16rpx; }
|
||||
.message-patient .message-content { justify-content: flex-end; }
|
||||
.avatar { width: 72rpx; height: 72rpx; flex: 0 0 72rpx; display: flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 24rpx; font-weight: 600; }
|
||||
.assistant-avatar { color: #fff; background: linear-gradient(135deg, #4d93ff, #0877f1); }
|
||||
.patient-avatar { color: #0877f1; background: #e2efff; }
|
||||
.message-bubble-container { max-width: 74%; min-width: 0; }
|
||||
.message-patient .message-bubble-container { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.username-label { margin: 2rpx 0 10rpx; color: #8a919f; font-size: 24rpx; line-height: 32rpx; }
|
||||
.message-bubble { padding: 18rpx 22rpx; border-radius: 8rpx 22rpx 22rpx; background: #fff; box-shadow: 0 4rpx 14rpx rgba(17, 48, 89, .06); }
|
||||
.message-patient .message-bubble { color: #fff; border-radius: 22rpx 8rpx 22rpx 22rpx; background: #0877f1; box-shadow: 0 4rpx 14rpx rgba(8, 119, 241, .18); }
|
||||
.message-bubble.pending { opacity: .72; }
|
||||
.message-text { color: inherit; font-size: 30rpx; line-height: 46rpx; white-space: pre-wrap; word-break: break-word; }
|
||||
.time-divider { margin: 2rpx 0 22rpx; color: #a0a6b0; text-align: center; font-size: 23rpx; }
|
||||
.system-message { display: inline-block; margin: 0 auto; padding: 10rpx 20rpx; color: #79808c; background: #e9ecf1; border-radius: 22rpx; font-size: 24rpx; line-height: 36rpx; }
|
||||
.message-system { text-align: center; }
|
||||
.system-message.warn { color: #d84256; background: #fff0f1; }
|
||||
.send-status { margin-top: 8rpx; color: #e34d59; font-size: 23rpx; }
|
||||
.thinking { display: flex; align-items: center; gap: 8rpx; min-width: 84rpx; padding: 26rpx 28rpx; }
|
||||
.thinking-dot { width: 10rpx; height: 10rpx; border-radius: 50%; background: #7f8998; animation: thinking 1.2s infinite ease-in-out; }
|
||||
.thinking-dot:nth-child(2) { animation-delay: .16s; }
|
||||
.thinking-dot:nth-child(3) { animation-delay: .32s; }
|
||||
@keyframes thinking { 0%, 60%, 100% { opacity: .35; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-7rpx); } }
|
||||
.input-section { display: flex; align-items: flex-end; gap: 16rpx; padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom)); background: #fff; border-top: 1rpx solid #edf0f4; }
|
||||
.text-input { flex: 1; max-height: 180rpx; padding: 16rpx 20rpx; box-sizing: border-box; color: #283242; background: #f3f5f8; border-radius: 12rpx; font-size: 29rpx; line-height: 40rpx; }
|
||||
.send-btn { flex: 0 0 auto; height: 72rpx; margin: 0; padding: 0 26rpx; color: #fff; background: #0877f1; border-radius: 12rpx; font-size: 27rpx; line-height: 72rpx; }
|
||||
.send-btn[disabled] { color: #fff; background: #a9cfff; }
|
||||
.rate-card { display: block; margin: 20rpx 0 0; }
|
||||
.ended { padding: 30rpx; color: #7d8590; text-align: center; background: #fff; font-size: 27rpx; }
|
||||
</style>
|
||||
101
pages/ai-consult/entry.vue
Normal file
101
pages/ai-consult/entry.vue
Normal file
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<template v-if="customers.length">
|
||||
<view class="title">请选择本次咨询档案</view>
|
||||
<view v-for="item in customers" :key="item._id" class="customer-card" @click="open(item)">
|
||||
<view class="card-header">
|
||||
<view class="customer-name">{{ item.name || '未命名' }}</view>
|
||||
<view v-if="item.relationship" class="relationship">{{ item.relationship }}</view>
|
||||
<view class="header-spacer" />
|
||||
<view v-if="enableHis" :class="['his-status', item.isConnectHis ? 'connected' : 'unconnected']">
|
||||
{{ item.isConnectHis ? '已关联院内档案' : '未关联院内档案' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="customer-info">
|
||||
<text v-if="item.sex">{{ item.sex }}</text>
|
||||
<text v-if="item.sex && item.age">/</text>
|
||||
<text v-if="item.age">{{ item.age }}岁</text>
|
||||
<text v-if="item.sex || item.age">,</text>
|
||||
<text v-if="item.mobile">{{ item.mobile }}</text>
|
||||
</view>
|
||||
<view class="customer-info">证件号:{{ item.idCard || '--' }}</view>
|
||||
</view>
|
||||
<view class="card-footer">选择此档案咨询</view>
|
||||
</view>
|
||||
</template>
|
||||
<view v-else-if="loaded" class="empty">正在前往建档...</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { removeAiLabel } from '@/utils/ai-consult-display';
|
||||
|
||||
const { account, openid } = storeToRefs(useAccountStore());
|
||||
const corpId = ref('');
|
||||
const teamId = ref('');
|
||||
const assistantId = ref('');
|
||||
const qrid = ref('');
|
||||
const customers = ref([]);
|
||||
const enableHis = ref(false);
|
||||
const loaded = ref(false);
|
||||
|
||||
function getEntryUrl() {
|
||||
return `/pages/ai-consult/entry?corpId=${corpId.value}&teamId=${teamId.value}&assistantId=${assistantId.value}&qrid=${qrid.value}`;
|
||||
}
|
||||
|
||||
function toLogin() {
|
||||
uni.redirectTo({ url: `/pages/login/login?redirectUrl=${encodeURIComponent(getEntryUrl())}` });
|
||||
}
|
||||
|
||||
function toCreateArchive() {
|
||||
uni.redirectTo({ url: `/pages/archive/edit-archive?corpId=${corpId.value}&teamId=${teamId.value}&redirectUrl=${encodeURIComponent(getEntryUrl())}` });
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const miniAppId = openid.value || uni.getStorageSync('openid');
|
||||
const res = await api('getMiniAppCustomers', { corpId: corpId.value, miniAppId }, false);
|
||||
loaded.value = true;
|
||||
if (!res?.success) return uni.showToast({ title: removeAiLabel(res?.message, '查询档案失败'), icon: 'none' });
|
||||
customers.value = res.data || [];
|
||||
enableHis.value = res.enableHis === true;
|
||||
if (!customers.value.length) toCreateArchive();
|
||||
}
|
||||
|
||||
async function open(customer) {
|
||||
const res = await api('openAiConsultSession', { corpId: corpId.value, teamId: teamId.value, assistantId: assistantId.value, qrid: qrid.value, customerId: customer?._id || '', miniAppId: openid.value || uni.getStorageSync('openid') });
|
||||
if (!res?.success) return uni.showToast({ title: removeAiLabel(res?.message, '创建失败'), icon: 'none' });
|
||||
uni.redirectTo({ url: `/pages/ai-consult/chat?corpId=${corpId.value}&sessionId=${res.data.session._id}` });
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
corpId.value = opts.corpId || '';
|
||||
teamId.value = opts.teamId || '';
|
||||
assistantId.value = opts.assistantId || '';
|
||||
qrid.value = opts.qrid || '';
|
||||
if (!account.value?.mobile) return toLogin();
|
||||
load();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page { min-height: 100vh; background: #f7f8fa; padding: 28rpx; }
|
||||
.title { font-size: 34rpx; font-weight: 600; margin-bottom: 20rpx; }
|
||||
.customer-card { overflow: hidden; margin-bottom: 20rpx; background: #fff; border-radius: 16rpx; box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1); }
|
||||
.card-header { display: flex; align-items: center; padding: 24rpx 30rpx; border-bottom: 1rpx solid #eee; }
|
||||
.customer-name { flex-shrink: 0; font-size: 32rpx; font-weight: 600; color: #333; }
|
||||
.relationship { flex-shrink: 0; margin-left: 10rpx; padding: 2rpx 16rpx; border: 1rpx solid #1677ff; border-radius: 4rpx; color: #1677ff; font-size: 24rpx; }
|
||||
.header-spacer { flex: 1; }
|
||||
.his-status { padding: 8rpx 18rpx; border-radius: 6rpx; color: #fff; font-size: 24rpx; line-height: 1.3; }
|
||||
.his-status.connected { background: #22a06b; }
|
||||
.his-status.unconnected { background: #ff9d00; }
|
||||
.card-body { padding: 20rpx 30rpx; border-bottom: 1rpx solid #eee; }
|
||||
.customer-info { min-height: 38rpx; color: #333; font-size: 28rpx; line-height: 42rpx; }
|
||||
.card-footer { padding: 20rpx 30rpx; color: #1677ff; font-size: 28rpx; text-align: right; }
|
||||
.empty { padding-top: 100rpx; color: #888; font-size: 25rpx; text-align: center; }
|
||||
</style>
|
||||
47
pages/ai-consult/list.vue
Normal file
47
pages/ai-consult/list.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<full-page @reachBottom="loadMore">
|
||||
<template #header>
|
||||
<scroll-view scroll-x class="tabs" :show-scrollbar="false">
|
||||
<view v-for="item in sources" :key="item.value" class="tab" :class="{ active: source === item.value }" @click="selectSource(item.value)">{{ item.name }}</view>
|
||||
</scroll-view>
|
||||
<scroll-view scroll-x class="tabs customer-tabs" :show-scrollbar="false">
|
||||
<view v-for="item in customers" :key="item.value" class="tab" :class="{ active: customerId === item.value }" @click="selectCustomer(item.value)">{{ item.name }}</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
<view class="page">
|
||||
<empty-data v-if="!loading && list.length === 0" text="暂无咨询记录" />
|
||||
<view v-for="item in list" :key="`${item.source}-${item.id}`" class="card" @click="openChat(item)">
|
||||
<view class="title"><text>{{ removeAiLabel(item.assistantName, '咨询助理') }}</text><text class="status" :class="item.status">{{ item.statusLabel }}</text></view>
|
||||
<view class="line"><text class="source" :class="item.source">{{ removeAiLabel(item.sourceLabel, '咨询') }}</text></view>
|
||||
<view class="line">团队:{{ item.teamName || item.teamId || '-' }}</view>
|
||||
<view class="line">档案:{{ item.customerName || '-' }}</view>
|
||||
<view class="line">最近互动:{{ format(item.lastActiveTime) }}</view>
|
||||
<view v-if="item.sensitiveHit" class="warn">已触发敏感词</view>
|
||||
</view>
|
||||
</view>
|
||||
</full-page>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import FullPage from '@/components/full-page.vue';
|
||||
import EmptyData from '@/components/empty-data.vue';
|
||||
import { removeAiLabel } from '@/utils/ai-consult-display';
|
||||
const { openid } = storeToRefs(useAccountStore());
|
||||
const corpId = ref(''); const customerId = ref(''); const source = ref('');
|
||||
const sources = [{ name: '全部咨询', value: '' }, { name: '咨询', value: 'ai' }, { name: 'IM咨询', value: 'im' }];
|
||||
const customers = ref([{ name: '全部档案', value: '' }]);
|
||||
const list = ref([]); const page = ref(1); const pages = ref(0); const loading = ref(false);
|
||||
const format = value => value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '-';
|
||||
async function loadCustomers() { const res = await api('getMiniAppCustomers', { corpId: corpId.value, miniAppId: openid.value || uni.getStorageSync('openid') }, false); if (res?.success) customers.value = [customers.value[0], ...(res.data || []).map(item => ({ name: item.name || '未命名', value: item._id }))]; }
|
||||
async function load(reset = false) { if (loading.value) return; if (reset) { page.value = 1; list.value = []; } loading.value = true; const res = await api('getUnifiedConsultSessions', { corpId: corpId.value, customerId: customerId.value, miniAppId: openid.value || uni.getStorageSync('openid'), source: source.value, page: page.value, pageSize: 20 }, false); if (res?.success) { list.value = page.value === 1 ? res.list : [...list.value, ...res.list]; pages.value = res.pages || 0; } loading.value = false; }
|
||||
function selectCustomer(value) { if (customerId.value === value) return; customerId.value = value; load(true); }
|
||||
function selectSource(value) { if (source.value === value) return; source.value = value; load(true); }
|
||||
function loadMore() { if (!loading.value && page.value < pages.value) { page.value += 1; load(); } }
|
||||
function openChat(item) { const url = item.source === 'im' ? `/pages/message/index?conversationID=GROUP${item.groupId}&groupID=${item.groupId}` : `/pages/ai-consult/chat?corpId=${item.corpId}&sessionId=${item.id}`; uni.navigateTo({ url }); }
|
||||
onLoad(async opts => { corpId.value = opts.corpId || ''; await loadCustomers(); await load(true); });
|
||||
</script>
|
||||
<style scoped>.page{min-height:100vh;background:#f7f8fa;padding:24rpx}.tabs{white-space:nowrap;background:#fff;padding:18rpx 20rpx}.customer-tabs{border-top:1rpx solid #f2f3f5;padding-top:12rpx}.tab{display:inline-block;padding:10rpx 22rpx;margin-right:16rpx;border-radius:28rpx;color:#666;background:#f5f5f5}.tab.active{color:#0877f1;background:#e8f3ff}.card{background:#fff;border-radius:16rpx;padding:24rpx;margin-bottom:20rpx}.title{display:flex;justify-content:space-between;font-size:32rpx;font-weight:600;margin-bottom:16rpx}.status{font-size:24rpx;color:#ff8a00}.status.active,.status.processing{color:#18a058}.line{font-size:26rpx;color:#777;line-height:44rpx}.source{font-size:22rpx;border-radius:6rpx;padding:2rpx 10rpx;color:#0877f1;background:#e8f3ff}.source.im{color:#7c55d1;background:#f1ebff}.warn{font-size:24rpx;color:#e34d59;margin-top:8rpx}</style>
|
||||
@ -109,6 +109,15 @@ async function getMatchedHisArchive(data) {
|
||||
customer.value = data;
|
||||
corpName.value = res.corpName || '';
|
||||
showBindHisPopup.value = true;
|
||||
} else if (res && res.isCustomerInfoNotMatchQueryPlans) {
|
||||
try {
|
||||
await confirm('请完善信息', { cancelText: '取消', confirmText: '确认' });
|
||||
uni.navigateTo({
|
||||
url: `/pages/archive/fill-his-archive?teamId=${teamId.value}&corpId=${corpId.value}&id=${data._id}`
|
||||
});
|
||||
} catch (error) {
|
||||
// 用户取消后停留在档案管理页。
|
||||
}
|
||||
} else {
|
||||
confirm(res?.message || '获取档案信息失败', { showCancel: false })
|
||||
}
|
||||
@ -133,6 +142,7 @@ useLoad(options => {
|
||||
})
|
||||
|
||||
useShow(() => {
|
||||
console.log('show~~~~~~')
|
||||
if (teamId.value && corpId.value) {
|
||||
getMembers()
|
||||
}
|
||||
|
||||
@ -10,12 +10,13 @@
|
||||
</view>
|
||||
</view>
|
||||
<template #footer>
|
||||
<button-footer :showCancel="customerId ? true : false" cancelText="删除" :confirmText="healthTypes.length?'下一步':'保存'" @cancel="unBindArchive()"
|
||||
@confirm="confirm()" />
|
||||
<!-- :showCancel="customerId ? true : false" -->
|
||||
<button-footer :showCancel="false" cancelText="删除" :confirmText="healthTypes.length ? '下一步' : '保存'"
|
||||
@cancel="unBindArchive()" @confirm="confirm()" />
|
||||
</template>
|
||||
</full-page>
|
||||
<bind-popup :customers="customers" :corpName="corpName" :enableHis="enableHis" :visible="visible" @close="visible = false"
|
||||
@confirm="bindArchive($event)" />
|
||||
<bind-popup :customers="customers" :corpName="corpName" :enableHis="enableHis" :visible="visible"
|
||||
@close="visible = false" @confirm="bindArchive($event)" />
|
||||
<verify-popup :visible="verifyVisible" @close="verifyVisible = false" />
|
||||
</template>
|
||||
<script setup>
|
||||
@ -44,13 +45,14 @@ const { getExternalUserId } = useAccount()
|
||||
const corpId = ref('');
|
||||
const corpName = ref('');
|
||||
const corpUserId = ref('');
|
||||
const bindCustomerId = ref('');
|
||||
const referenceCustomerId = ref('');
|
||||
const customer = ref({});
|
||||
const customerId = ref('');
|
||||
const customers = ref([]);
|
||||
const disableTitles = ref(['mobile']);
|
||||
// const disableTitles = ref(['mobile']);
|
||||
const form = ref({});
|
||||
const formItems = ref([]);
|
||||
const teamFormItems = ref([]);
|
||||
const loading = ref(false);
|
||||
const teamId = ref('');
|
||||
const tempRef = ref(null);
|
||||
@ -59,11 +61,35 @@ const visible = ref(false);
|
||||
const referenceCustomer = ref(null)
|
||||
const healthTypes = ref([]);
|
||||
const enableHis = ref(false);
|
||||
const source = ref('');
|
||||
const redirectUrl = ref('');
|
||||
const archiveQueryPlans = ref([]);
|
||||
const pageOptions = ref({});
|
||||
|
||||
const formData = computed(() => {
|
||||
if (customerId.value) {
|
||||
return { ...customer.value, ...form.value }
|
||||
}
|
||||
return { ...customer.value, ...form.value, mobile: account.value?.mobile }
|
||||
});
|
||||
|
||||
const formItems = computed(() => {
|
||||
return teamFormItems.value.map(i => {
|
||||
if (i.title === 'mobile' && formData.value.mobile && formData.value.mobile === account.value?.mobile) {
|
||||
return { ...i, appendText: '(授权手机号)' }
|
||||
}
|
||||
return i
|
||||
})
|
||||
})
|
||||
|
||||
const disableTitles = computed(() => {
|
||||
const list = ['mobile'];
|
||||
if (customer.value._id && customer.value.isConnectHis) {
|
||||
list.push('name', 'idCard', 'sex', 'age', 'birthday')
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
function change({ title, value }) {
|
||||
if (title) {
|
||||
form.value[title] = value;
|
||||
@ -102,36 +128,12 @@ function preProcessFrom() {
|
||||
form.value.relationship = '本人';
|
||||
}
|
||||
const cardTypeItem = formItems.value.find(item => item.title === 'cardType');
|
||||
const cardTypeRange = cardTypeItem && Array.isArray(cardTypeItem.range) ? relationItem.range : [];
|
||||
const cardTypeRange = cardTypeItem && Array.isArray(cardTypeItem.range) ? cardTypeItem.range : [];
|
||||
if (cardTypeRange.includes('身份证')) {
|
||||
form.value.cardType = '身份证';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品要求, 编辑的情况下 姓名、身份证号、性别、年龄、出生年月
|
||||
* 建档成功或者绑定档案成功后,姓名、身份证号、性别、年龄、出生年月。如果有内容的都不允许修改。没有内容的则允许编辑。
|
||||
*/
|
||||
function setDisabledTitles(data) {
|
||||
const list = ['mobile'];
|
||||
if (data.name) {
|
||||
list.push('name');
|
||||
}
|
||||
if (data.idCard) {
|
||||
list.push('idCard');
|
||||
}
|
||||
if (data.sex) {
|
||||
list.push('sex');
|
||||
}
|
||||
if (data.age) {
|
||||
list.push('age');
|
||||
}
|
||||
if (data.birthday) {
|
||||
list.push('birthday');
|
||||
}
|
||||
disableTitles.value = list;
|
||||
}
|
||||
|
||||
async function addArchive() {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
@ -166,6 +168,15 @@ async function addArchive() {
|
||||
set('home-invite-team-info', { teamId: teamId.value })
|
||||
if (res && res.success) {
|
||||
uni.$emit('reloadTeamCustomers')
|
||||
if (source.value === 'experienceCoupon') {
|
||||
await toast('档案创建成功');
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
if (redirectUrl.value) {
|
||||
uni.redirectTo({ url: redirectUrl.value });
|
||||
return;
|
||||
}
|
||||
// getTeam(corpId.value, teamId.value, res.data.id);
|
||||
if (healthTypes.value.length) {
|
||||
const nextType = healthTypes.value[0];
|
||||
@ -187,6 +198,20 @@ async function getResponsiblePerson() {
|
||||
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) {
|
||||
let responsiblePerson = '';
|
||||
if (externalUserId.value) {
|
||||
@ -197,23 +222,29 @@ async function bindArchive(customerId) {
|
||||
if (res && res.success) {
|
||||
await toast('绑定成功');
|
||||
uni.$emit('reloadTeamCustomers')
|
||||
uni.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
backAfterArchiveBound()
|
||||
} else {
|
||||
toast(res?.message || '绑定失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (await initArchiveQueryConfig()) return;
|
||||
if (referenceCustomerId.value) {
|
||||
getReferenceCustomer();
|
||||
}
|
||||
if (customerId.value) {
|
||||
await getCustomer();
|
||||
} else {
|
||||
const res = await getArchives();
|
||||
await getExternalUserId(corpId.value);
|
||||
const res = bindCustomerId.value ? await getExperienceCouponBindArchive() : await getArchives();
|
||||
if (res.length > 0) {
|
||||
visible.value = true;
|
||||
} else if (bindCustomerId.value || shouldBackAfterArchiveBound()) {
|
||||
await toast('指定绑定档案不存在或已绑定');
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
getExternalUserId(corpId.value);
|
||||
getTeam(corpId.value, teamId.value)
|
||||
}
|
||||
await getBaseForm();
|
||||
@ -223,8 +254,50 @@ async function init() {
|
||||
|
||||
}
|
||||
|
||||
async function initArchiveQueryConfig() {
|
||||
const res = await api('getCorpInfo', { corpId: corpId.value }, false);
|
||||
const data = Array.isArray(res.data)
|
||||
? res.data[0]
|
||||
: Array.isArray(res.data?.data)
|
||||
? res.data.data[0]
|
||||
: res.data?.data || res.data || {};
|
||||
const config = data.hisArchiveQueryConfig && typeof data.hisArchiveQueryConfig === 'object'
|
||||
? data.hisArchiveQueryConfig
|
||||
: {};
|
||||
const plans = Array.isArray(config.plans)
|
||||
? config.plans
|
||||
: Array.isArray(config.schemes)
|
||||
? config.schemes
|
||||
: [];
|
||||
archiveQueryPlans.value = plans.filter(plan => plan && Array.isArray(plan.fields) && plan.fields.length);
|
||||
if (archiveQueryPlans.value.length) {
|
||||
redirectArchivePage('step-edit-archive');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function redirectArchivePage(page) {
|
||||
const query = Object.entries(pageOptions.value)
|
||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
.join('&');
|
||||
uni.redirectTo({
|
||||
url: `/pages/archive/${page}${query ? `?${query}` : ''}`
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
const res = await api('getUnbindMiniAppCustomers', { corpId: corpId.value, mobile: account.value?.mobile || '' });
|
||||
const res = await api('getUnbindMiniAppCustomers', { corpId: corpId.value, mobile: account.value?.mobile || '', externalUserId: externalUserId.value });
|
||||
customers.value = res && Array.isArray(res.data) ? res.data : [];
|
||||
corpName.value = res && res.corpName ? res.corpName : '';
|
||||
enableHis.value = res && res.enableHis ? res.enableHis : false;
|
||||
@ -234,12 +307,7 @@ async function getArchives() {
|
||||
async function getBaseForm() {
|
||||
const res = await api('getTeamBaseInfo', { corpId: corpId.value, teamId: teamId.value });
|
||||
if (res && res.success) {
|
||||
formItems.value = Array.isArray(res.data) ? res.data : [];
|
||||
const mobileIndex = formItems.value.findIndex(item => item.title === 'mobile');
|
||||
if (mobileIndex > -1) {
|
||||
formItems.value[mobileIndex].appendText = `(授权手机号)`;
|
||||
}
|
||||
|
||||
teamFormItems.value = Array.isArray(res.data) ? res.data : [];
|
||||
} else {
|
||||
toast(res?.message || '查询失败');
|
||||
return Promise.reject()
|
||||
@ -250,7 +318,6 @@ async function getCustomer() {
|
||||
const res = await api('getCustomerByCustomerId', { customerId: customerId.value });
|
||||
if (res && res.success && res.data) {
|
||||
customer.value = res.data;
|
||||
setDisabledTitles(res.data)
|
||||
} else {
|
||||
await toast(res?.message || '查询档案信息失败');
|
||||
uni.navigateBack();
|
||||
@ -302,14 +369,15 @@ async function getTeam(corpId, teamId, customerId) {
|
||||
|
||||
|
||||
onLoad(options => {
|
||||
pageOptions.value = { ...options };
|
||||
teamId.value = options.teamId;
|
||||
corpId.value = options.corpId;
|
||||
customerId.value = options.id || '';
|
||||
bindCustomerId.value = options.bindCustomerId || '';
|
||||
corpUserId.value = options.corpUserId || '';
|
||||
referenceCustomerId.value = options.referenceCustomerId || '';
|
||||
if (referenceCustomerId.value) {
|
||||
getReferenceCustomer();
|
||||
}
|
||||
source.value = options.source || '';
|
||||
redirectUrl.value = options.redirectUrl || '';
|
||||
uni.setNavigationBarTitle({ title: customerId.value ? '编辑档案' : '新增档案' })
|
||||
})
|
||||
|
||||
@ -318,4 +386,4 @@ useLoad(options => {
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped></style>
|
||||
<style scoped></style>
|
||||
|
||||
213
pages/archive/fill-his-archive.vue
Normal file
213
pages/archive/fill-his-archive.vue
Normal file
@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<full-page :customScroll="false">
|
||||
<view v-if="configLoaded && !queryFields.length" class="flex items-center justify-center h-full">
|
||||
<empty-data text="当前机构暂未配置院内档案查询方案" />
|
||||
</view>
|
||||
<view v-else-if="queryFields.length" class="p-15">
|
||||
<view class="bg-white rounded shadow-lg">
|
||||
<form-template ref="formRef" :items="queryFormItems" :form="formData" @change="change" />
|
||||
</view>
|
||||
</view>
|
||||
<template #footer>
|
||||
<button-footer v-if="queryFields.length" :showCancel="false" confirmText="保存" @confirm="save" />
|
||||
</template>
|
||||
</full-page>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import api from '@/utils/api';
|
||||
import { toast } from '@/utils/widget';
|
||||
|
||||
import ButtonFooter from '@/components/button-footer.vue';
|
||||
import EmptyData from '@/components/empty-data.vue';
|
||||
import FullPage from '@/components/full-page.vue';
|
||||
import formTemplate from '@/components/form-template/index.vue';
|
||||
|
||||
const corpId = ref('');
|
||||
const teamId = ref('');
|
||||
const customerId = ref('');
|
||||
const customer = ref({});
|
||||
const form = ref({});
|
||||
const archiveQueryPlans = ref([]);
|
||||
const configLoaded = ref(false);
|
||||
const saving = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
const queryFields = computed(() => {
|
||||
const fields = [];
|
||||
const fieldKeys = new Set();
|
||||
archiveQueryPlans.value.forEach((plan) => {
|
||||
(Array.isArray(plan?.fields) ? plan.fields : []).forEach((field) => {
|
||||
const source = typeof field === 'string' ? { fieldKey: field } : field || {};
|
||||
const fieldKey = String(source.fieldKey || source.feildKey || source.key || source.title || '').trim();
|
||||
if (!fieldKey || fieldKeys.has(fieldKey)) return;
|
||||
fieldKeys.add(fieldKey);
|
||||
fields.push({
|
||||
fieldKey,
|
||||
label: source.label || source.labelSnapshot || source.name || fieldKey,
|
||||
});
|
||||
});
|
||||
});
|
||||
return fields;
|
||||
});
|
||||
|
||||
// 查询方案字段在此页面一律按普通文本输入框处理。
|
||||
const queryFormItems = computed(() => queryFields.value.map((field) => ({
|
||||
title: field.fieldKey,
|
||||
name: field.label,
|
||||
type: 'input',
|
||||
})));
|
||||
|
||||
const formData = computed(() => ({ ...customer.value, ...form.value }));
|
||||
|
||||
function change({ title, value }) {
|
||||
if (title) form.value[title] = value;
|
||||
}
|
||||
|
||||
async function initArchiveQueryConfig() {
|
||||
try {
|
||||
const res = await api('getCorpInfo', { corpId: corpId.value }, false);
|
||||
const data = Array.isArray(res?.data)
|
||||
? res.data[0]
|
||||
: Array.isArray(res?.data?.data)
|
||||
? res.data.data[0]
|
||||
: res?.data?.data || res?.data || {};
|
||||
const config = data?.hisArchiveQueryConfig && typeof data.hisArchiveQueryConfig === 'object'
|
||||
? data.hisArchiveQueryConfig
|
||||
: {};
|
||||
const plans = Array.isArray(config.plans)
|
||||
? config.plans
|
||||
: Array.isArray(config.schemes)
|
||||
? config.schemes
|
||||
: [];
|
||||
archiveQueryPlans.value = plans.filter((plan) => plan && Array.isArray(plan.fields) && plan.fields.length);
|
||||
} finally {
|
||||
configLoaded.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function getCustomer() {
|
||||
const res = await api('getCustomerByCustomerId', { customerId: customerId.value });
|
||||
if (res?.success && res.data) {
|
||||
customer.value = res.data;
|
||||
return true;
|
||||
}
|
||||
await toast(res?.message || '查询档案信息失败');
|
||||
uni.navigateBack();
|
||||
return false;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
// if (saving.value || !formRef.value?.verify()) return;
|
||||
saving.value = true;
|
||||
try {
|
||||
const matchedPlans = archiveQueryPlans.value.filter(isQueryPlanSatisfied);
|
||||
if (!matchedPlans.length) {
|
||||
toast('请完善信息');
|
||||
return;
|
||||
}
|
||||
|
||||
const matchedArchive = await queryHisArchive(matchedPlans);
|
||||
if (!matchedArchive) {
|
||||
toast('没有查询到患者院内档案,可先前往医院建档。');
|
||||
return;
|
||||
}
|
||||
if (!matchedArchive.archive.customerNumber) {
|
||||
toast('院内档案缺少门诊ID,无法关联');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateRes = await api('updateCustomer', {
|
||||
id: customerId.value,
|
||||
params: {
|
||||
...matchedArchive.conditions,
|
||||
customerNumber: matchedArchive.archive.customerNumber,
|
||||
isConnectHis: true,
|
||||
},
|
||||
});
|
||||
if (!updateRes?.success) {
|
||||
toast(updateRes?.message || '保存失败');
|
||||
return;
|
||||
}
|
||||
await toast('保存并关联成功');
|
||||
uni.$emit('reloadTeamCustomers');
|
||||
uni.navigateBack();
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlanFields(plan) {
|
||||
return (Array.isArray(plan?.fields) ? plan.fields : [])
|
||||
.map((field) => {
|
||||
const source = typeof field === 'string' ? { fieldKey: field } : field || {};
|
||||
const fieldKey = String(source.fieldKey || source.feildKey || source.key || source.title || '').trim();
|
||||
if (!fieldKey) return null;
|
||||
return { fieldKey };
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeFieldValue(value) {
|
||||
return value === undefined || value === null ? '' : String(value).trim();
|
||||
}
|
||||
|
||||
function isQueryPlanSatisfied(plan) {
|
||||
const fields = getPlanFields(plan);
|
||||
return fields.length > 0 && fields.every((field) => normalizeFieldValue(formData.value[field.fieldKey]) !== '');
|
||||
}
|
||||
|
||||
function getQueryConditions(plan) {
|
||||
return getPlanFields(plan).reduce((conditions, field) => {
|
||||
conditions[field.fieldKey] = formData.value[field.fieldKey];
|
||||
return conditions;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function isHisArchiveMatched(archive, conditions) {
|
||||
return Object.keys(conditions).every((fieldKey) =>
|
||||
normalizeFieldValue(archive?.[fieldKey]) === normalizeFieldValue(conditions[fieldKey])
|
||||
);
|
||||
}
|
||||
|
||||
async function queryHisArchive(plans) {
|
||||
for (const plan of plans) {
|
||||
const conditions = getQueryConditions(plan);
|
||||
try {
|
||||
const res = await api('getHisCustomerArchive', {
|
||||
corpId: corpId.value,
|
||||
planId: plan.planId || plan.schemeId || '',
|
||||
...conditions,
|
||||
}, false);
|
||||
const archive = (Array.isArray(res?.list) ? res.list : [])
|
||||
.find((item) => isHisArchiveMatched(item, conditions));
|
||||
if (archive) return { archive, conditions };
|
||||
} catch (error) {
|
||||
// 当前方案查询失败时继续尝试后续满足条件的方案。
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function init() {
|
||||
await initArchiveQueryConfig();
|
||||
if (!queryFields.value.length) return;
|
||||
await getCustomer();
|
||||
}
|
||||
|
||||
onLoad((options) => {
|
||||
corpId.value = options.corpId || '';
|
||||
teamId.value = options.teamId || '';
|
||||
customerId.value = options.id || '';
|
||||
if (!corpId.value || !customerId.value) {
|
||||
toast('页面参数错误');
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
545
pages/archive/step-edit-archive.vue
Normal file
545
pages/archive/step-edit-archive.vue
Normal file
@ -0,0 +1,545 @@
|
||||
<template>
|
||||
<full-page v-if="!visible" :customScroll="empty" :title="queryHisTitles.length">
|
||||
<view v-if="formItems.length === 0" class="flex items-center justify-center h-full">
|
||||
<empty-data />
|
||||
</view>
|
||||
<view v-else class="p-15">
|
||||
<view class="bg-white rounded shadow-lg">
|
||||
<form-template v-if="step === 0" ref="tempRef" :disableTitles="disableTitles" :items="hisQueryFormItems"
|
||||
:form="formData" @change="change($event)" />
|
||||
<form-template v-else-if="step === 1" ref="tempRef" :disableTitles="disableTitles" :items="restFormItems"
|
||||
:form="formData" @change="change($event)" />
|
||||
</view>
|
||||
</view>
|
||||
<template #footer>
|
||||
<!-- :showCancel="customerId ? true : false" -->
|
||||
<button-footer v-if="step === 0" :showCancel="false" confirmText="下一步" @confirm="queryHisArchives()" />
|
||||
<button-footer v-else-if="step === 1" :showCancel="false" cancelText="" confirmText=" 保存" @cancel="step = 0"
|
||||
@confirm="confirm()" />
|
||||
</template>
|
||||
</full-page>
|
||||
<bind-popup :customers="customers" :corpName="corpName" :enableHis="enableHis" :visible="visible"
|
||||
@close="visible = false" @confirm="bindArchive($event)" />
|
||||
<verify-popup :visible="verifyVisible" @close="verifyVisible = false" />
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import dayjs from 'dayjs';
|
||||
import useGuard from '@/hooks/useGuard';
|
||||
import useAccount from '@/store/account';
|
||||
import api from '@/utils/api';
|
||||
import { toast, confirm as uniConfirm } from '@/utils/widget';
|
||||
import validate from '@/utils/validate';
|
||||
import { set } from "@/utils/cache";
|
||||
|
||||
import ButtonFooter from '@/components/button-footer.vue';
|
||||
import EmptyData from '@/components/empty-data.vue';
|
||||
import FullPage from '@/components/full-page.vue';
|
||||
import bindPopup from './bind-popup.vue';
|
||||
import verifyPopup from './verify-popup.vue';
|
||||
import formTemplate from '@/components/form-template/index.vue';
|
||||
|
||||
const empty = ref(false)
|
||||
const { useLoad } = useGuard();
|
||||
const { account, externalUserId } = storeToRefs(useAccount());
|
||||
const { getExternalUserId } = useAccount()
|
||||
const corpId = ref('');
|
||||
const corpName = ref('');
|
||||
const corpUserId = ref('');
|
||||
const bindCustomerId = ref('');
|
||||
const referenceCustomerId = ref('');
|
||||
const customer = ref({});
|
||||
const customerId = ref('');
|
||||
const customers = ref([]);
|
||||
// const disableTitles = ref(['mobile']);
|
||||
const form = ref({});
|
||||
const teamFormItems = ref([]);
|
||||
const loading = ref(false);
|
||||
const teamId = ref('');
|
||||
const tempRef = ref(null);
|
||||
const verifyVisible = ref(false);
|
||||
const visible = ref(false);
|
||||
const referenceCustomer = ref(null)
|
||||
const healthTypes = ref([]);
|
||||
const enableHis = ref(false);
|
||||
const source = ref('');
|
||||
const redirectUrl = ref('');
|
||||
const archiveQueryPlans = ref([]);
|
||||
const pageOptions = ref({});
|
||||
const step = ref(0);
|
||||
const hisArchive = ref(null);
|
||||
const queryingHisArchives = ref(false);
|
||||
const queryHisTitles = computed(() => {
|
||||
const res = archiveQueryPlans.value.length ? ['name', 'mobile'] : [];
|
||||
archiveQueryPlans.value.forEach(item => {
|
||||
getPlanFields(item).forEach(field => {
|
||||
if (!res.includes(field.fieldKey.trim())) {
|
||||
res.push(field.fieldKey.trim())
|
||||
}
|
||||
})
|
||||
})
|
||||
return customerId.value ? [] : res;
|
||||
})
|
||||
|
||||
const formItems = computed(() => {
|
||||
return teamFormItems.value.map(i => {
|
||||
if (i.title === 'mobile' && formData.value.mobile && formData.value.mobile === account.value?.mobile) {
|
||||
return { ...i, appendText: '(授权手机号)' }
|
||||
}
|
||||
return i
|
||||
})
|
||||
})
|
||||
|
||||
const hisQueryFormItems = computed(() => formItems.value.filter(i => queryHisTitles.value.includes(i.title)));
|
||||
const restFormItems = computed(() => formItems.value.filter(i => !queryHisTitles.value.includes(i.title)));
|
||||
|
||||
const formData = computed(() => {
|
||||
if (customerId.value) {
|
||||
return { ...customer.value, ...form.value }
|
||||
}
|
||||
return { ...customer.value, ...form.value, mobile: account.value?.mobile }
|
||||
});
|
||||
|
||||
const disableTitles = computed(() => {
|
||||
const list = ['mobile'];
|
||||
if (customer.value._id && customer.value.isConnectHis) {
|
||||
list.push('name', 'idCard', 'sex', 'age', 'birthday')
|
||||
}
|
||||
return list
|
||||
})
|
||||
|
||||
function change({ title, value }) {
|
||||
if (title) {
|
||||
form.value[title] = value;
|
||||
}
|
||||
if (title == 'idCard') {
|
||||
const [isIdCard, birthday, gender] = validate.isChinaId(value);
|
||||
if (isIdCard) {
|
||||
form.value.birthday = birthday;
|
||||
form.value.sex = gender == 'MALE' ? '男' : '女';
|
||||
const age = dayjs().diff(birthday, 'year');
|
||||
form.value.age = Math.max(1, age);
|
||||
}
|
||||
} else if (title === 'birthday' && formItems.value.some(i => i.title === 'age') && value && dayjs(value).valueOf()) {
|
||||
const age = dayjs().diff(value, 'year');
|
||||
form.value.age = Math.max(1, age);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function queryHisArchives() {
|
||||
if (queryingHisArchives.value) return;
|
||||
|
||||
const matchedPlans = archiveQueryPlans.value.filter(isQueryPlanSatisfied);
|
||||
if (!matchedPlans.length) {
|
||||
hisArchive.value = null;
|
||||
step.value = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
queryingHisArchives.value = true;
|
||||
try {
|
||||
const responses = await Promise.all(matchedPlans.map(async (plan) => {
|
||||
const conditions = getQueryConditions(plan);
|
||||
try {
|
||||
const res = await api('getHisCustomerArchive', {
|
||||
corpId: corpId.value,
|
||||
planId: plan.planId || plan.schemeId || '',
|
||||
...conditions,
|
||||
}, false);
|
||||
return {
|
||||
plan,
|
||||
conditions,
|
||||
list: res && Array.isArray(res.list) ? res.list : [],
|
||||
};
|
||||
} catch (error) {
|
||||
return { plan, conditions, list: [] };
|
||||
}
|
||||
}));
|
||||
for (const response of responses) {
|
||||
const archive = response.list.find((item) => isHisArchiveMatched(item, response.plan, response.conditions));
|
||||
if (archive) {
|
||||
hisArchive.value = archive;
|
||||
step.value = 1;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const responseWithArchives = responses.find((response) => response.list.length);
|
||||
if (!responseWithArchives) {
|
||||
await uniConfirm('没有查询到患者院内档案', { cancelText: '修改信息', confirmText: '新建档案' });
|
||||
archiveQueryPlans.value = []
|
||||
hisArchive.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const mismatchMessage = getMismatchMessage(responseWithArchives);
|
||||
try {
|
||||
await uniConfirm(mismatchMessage, { cancelText: '修改信息', confirmText: '新建档案' });
|
||||
archiveQueryPlans.value = []
|
||||
hisArchive.value = null;
|
||||
step.value = 1;
|
||||
} catch (error) {
|
||||
// 选择“修改信息”时留在当前步骤继续填写。
|
||||
}
|
||||
} finally {
|
||||
queryingHisArchives.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlanFields(plan) {
|
||||
return (Array.isArray(plan?.fields) ? plan.fields : [])
|
||||
.map((field) => {
|
||||
const source = typeof field === 'string' ? { fieldKey: field } : field || {};
|
||||
const fieldKey = source.fieldKey || source.feildKey || source.key || source.title;
|
||||
if (!fieldKey) return null;
|
||||
return {
|
||||
fieldKey,
|
||||
label: source.label || source.labelSnapshot || source.name || fieldKey,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function getCurrentFormValue(fieldKey) {
|
||||
return formData.value[fieldKey];
|
||||
}
|
||||
|
||||
function normalizeFieldValue(value) {
|
||||
return value === undefined || value === null ? '' : String(value).trim();
|
||||
}
|
||||
|
||||
function isQueryPlanSatisfied(plan) {
|
||||
const fields = getPlanFields(plan);
|
||||
return fields.length > 0 && fields.every((field) => normalizeFieldValue(getCurrentFormValue(field.fieldKey)));
|
||||
}
|
||||
|
||||
function getQueryConditions(plan) {
|
||||
return getPlanFields(plan).reduce((conditions, field) => {
|
||||
conditions[field.fieldKey] = normalizeFieldValue(getCurrentFormValue(field.fieldKey));
|
||||
return conditions;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getMatchFields(plan, conditions) {
|
||||
const fields = getPlanFields(plan).map((field) => ({
|
||||
...field,
|
||||
value: conditions[field.fieldKey],
|
||||
}));
|
||||
['name', 'mobile'].forEach((fieldKey) => {
|
||||
const value = normalizeFieldValue(getCurrentFormValue(fieldKey));
|
||||
if (value && !fields.some((field) => field.fieldKey === fieldKey)) {
|
||||
fields.push({ fieldKey, label: fieldKey === 'name' ? '姓名' : '手机号', value });
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
function isHisArchiveMatched(archive, plan, conditions) {
|
||||
return getMatchFields(plan, conditions).every((field) =>
|
||||
normalizeFieldValue(archive?.[field.fieldKey]) === normalizeFieldValue(field.value)
|
||||
);
|
||||
}
|
||||
|
||||
function getMismatchMessage(response) {
|
||||
const archive = response.list[0] || {};
|
||||
const mismatch = getMatchFields(response.plan, response.conditions).find((field) =>
|
||||
normalizeFieldValue(archive[field.fieldKey]) !== normalizeFieldValue(field.value)
|
||||
);
|
||||
if (!mismatch) return '院内档案信息与当前填写信息不一致,请确认后再继续。';
|
||||
const actualValue = normalizeFieldValue(archive[mismatch.fieldKey]) || '为空';
|
||||
return `${mismatch.label}不匹配`;
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (!tempRef.value.verify()) return;
|
||||
if (customerId.value) {
|
||||
updateArchive();
|
||||
} else {
|
||||
addArchive();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品要求, 建档页面:与联系人关系:默认选中“本人”,证件类型:默认选中“身份证”
|
||||
*/
|
||||
function preProcessFrom() {
|
||||
const relationItem = formItems.value.find(item => item.title === 'relationship');
|
||||
const range = relationItem && Array.isArray(relationItem.range) ? relationItem.range : [];
|
||||
if (range.includes('本人')) {
|
||||
form.value.relationship = '本人';
|
||||
}
|
||||
const cardTypeItem = formItems.value.find(item => item.title === 'cardType');
|
||||
const cardTypeRange = cardTypeItem && Array.isArray(cardTypeItem.range) ? cardTypeItem.range : [];
|
||||
if (cardTypeRange.includes('身份证')) {
|
||||
form.value.cardType = '身份证';
|
||||
}
|
||||
}
|
||||
|
||||
async function addArchive() {
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
const params = {
|
||||
...form.value,
|
||||
addMethod: 'customerManual',
|
||||
teamId: teamId.value,
|
||||
corpId: corpId.value,
|
||||
mobile: account.value.mobile,
|
||||
miniAppId: account.value.openid,
|
||||
externalUserId: externalUserId.value,
|
||||
realUnionid: account.value.unionid || '',
|
||||
}
|
||||
if (hisArchive.value && hisArchive.value.customerNumber) {
|
||||
params.customerNumber = hisArchive.value.customerNumber;
|
||||
params.isConnectHis = true
|
||||
}
|
||||
|
||||
if (externalUserId.value) {
|
||||
const corpUserId = await getResponsiblePerson();
|
||||
if (corpUserId) {
|
||||
params.personResponsibles = [{ corpUserId, teamId: teamId.value }]
|
||||
}
|
||||
}
|
||||
if (referenceCustomerId.value && !referenceCustomer.value) {
|
||||
await getReferenceCustomer();
|
||||
}
|
||||
if (referenceCustomer.value) {
|
||||
params.referenceCustomerId = referenceCustomer.value._id;
|
||||
params.referenceUserId = '';
|
||||
params.reference = referenceCustomer.value.name;
|
||||
params.referenceType = '客户';
|
||||
params.customerSource = ['客户推荐']
|
||||
}
|
||||
loading.value = false;
|
||||
const res = await api('addCustomer', { params });
|
||||
set('home-invite-team-info', { teamId: teamId.value })
|
||||
if (res && res.success) {
|
||||
uni.$emit('reloadTeamCustomers')
|
||||
if (source.value === 'experienceCoupon') {
|
||||
await toast('档案创建成功');
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
if (redirectUrl.value) {
|
||||
uni.redirectTo({ url: redirectUrl.value });
|
||||
return;
|
||||
}
|
||||
// getTeam(corpId.value, teamId.value, res.data.id);
|
||||
if (healthTypes.value.length) {
|
||||
const nextType = healthTypes.value[0];
|
||||
const nextTypes = healthTypes.value.slice(1);
|
||||
const url = `/pages/health/record?type=${nextType}&teamId=${teamId.value}&corpId=${corpId.value}&customerId=${res.data.id}&nextTypes=${nextTypes.join(',')}&source=afterArchive`
|
||||
uni.redirectTo({ url });
|
||||
} else {
|
||||
uni.redirectTo({
|
||||
url: `/pages/archive/archive-result?corpId=${corpId.value}&teamId=${teamId.value}&customerId=${res.data.id}`
|
||||
})
|
||||
}
|
||||
} else {
|
||||
toast(res?.message || '新增档案失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function getResponsiblePerson() {
|
||||
const res = await api('getResponsiblePerson', { corpId: corpId.value, teamId: teamId.value, externalUserId: externalUserId.value, corpUserId: corpUserId.value });
|
||||
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) {
|
||||
let responsiblePerson = '';
|
||||
if (externalUserId.value) {
|
||||
const corpUserId = await getResponsiblePerson();
|
||||
responsiblePerson = corpUserId || '';
|
||||
}
|
||||
const res = await api('bindMiniAppArchive', { id: customerId, corpId: corpId.value, teamId: teamId.value, miniAppId: account.value.openid, externalUserId: externalUserId.value, responsiblePerson });
|
||||
if (res && res.success) {
|
||||
await toast('绑定成功');
|
||||
uni.$emit('reloadTeamCustomers')
|
||||
backAfterArchiveBound()
|
||||
} else {
|
||||
toast(res?.message || '绑定失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function init() {
|
||||
if (await initArchiveQueryConfig()) return;
|
||||
if (referenceCustomerId.value) {
|
||||
getReferenceCustomer();
|
||||
}
|
||||
if (customerId.value) {
|
||||
await getCustomer();
|
||||
} else {
|
||||
await getExternalUserId(corpId.value);
|
||||
const res = bindCustomerId.value ? await getExperienceCouponBindArchive() : await getArchives();
|
||||
if (res.length > 0) {
|
||||
visible.value = true;
|
||||
} else if (bindCustomerId.value || shouldBackAfterArchiveBound()) {
|
||||
await toast('指定绑定档案不存在或已绑定');
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
getTeam(corpId.value, teamId.value)
|
||||
}
|
||||
await getBaseForm();
|
||||
if (!customerId.value) {
|
||||
preProcessFrom()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function initArchiveQueryConfig() {
|
||||
const res = await api('getCorpInfo', { corpId: corpId.value }, false);
|
||||
const data = Array.isArray(res.data)
|
||||
? res.data[0]
|
||||
: Array.isArray(res.data?.data)
|
||||
? res.data.data[0]
|
||||
: res.data?.data || res.data || {};
|
||||
const config = data.hisArchiveQueryConfig && typeof data.hisArchiveQueryConfig === 'object'
|
||||
? data.hisArchiveQueryConfig
|
||||
: {};
|
||||
const plans = Array.isArray(config.plans)
|
||||
? config.plans
|
||||
: Array.isArray(config.schemes)
|
||||
? config.schemes
|
||||
: [];
|
||||
archiveQueryPlans.value = plans.filter(plan => plan && Array.isArray(plan.fields) && plan.fields.length);
|
||||
if (!archiveQueryPlans.value.length) {
|
||||
redirectArchivePage('edit-archive');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function redirectArchivePage(page) {
|
||||
const query = Object.entries(pageOptions.value)
|
||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
.join('&');
|
||||
uni.redirectTo({
|
||||
url: `/pages/archive/${page}${query ? `?${query}` : ''}`
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
const res = await api('getUnbindMiniAppCustomers', { corpId: corpId.value, mobile: account.value?.mobile || '', externalUserId: externalUserId.value });
|
||||
customers.value = res && Array.isArray(res.data) ? res.data : [];
|
||||
corpName.value = res && res.corpName ? res.corpName : '';
|
||||
enableHis.value = res && res.enableHis ? res.enableHis : false;
|
||||
return customers.value
|
||||
}
|
||||
|
||||
async function getBaseForm() {
|
||||
const res = await api('getTeamBaseInfo', { corpId: corpId.value, teamId: teamId.value });
|
||||
if (res && res.success) {
|
||||
teamFormItems.value = Array.isArray(res.data) ? res.data : [];
|
||||
// const mobileIndex = formItems.value.findIndex(item => item.title === 'mobile');
|
||||
// if (mobileIndex > -1) {
|
||||
// formItems.value[mobileIndex].appendText = `(授权手机号)`;
|
||||
// }
|
||||
|
||||
} else {
|
||||
toast(res?.message || '查询失败');
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
async function getCustomer() {
|
||||
const res = await api('getCustomerByCustomerId', { customerId: customerId.value });
|
||||
if (res && res.success && res.data) {
|
||||
customer.value = res.data;
|
||||
} else {
|
||||
await toast(res?.message || '查询档案信息失败');
|
||||
uni.navigateBack();
|
||||
return Promise.reject()
|
||||
}
|
||||
}
|
||||
|
||||
async function updateArchive() {
|
||||
const res = await api('updateCustomer', { id: customerId.value, params: { ...form.value } });
|
||||
if (res && res.success) {
|
||||
await toast('修改成功');
|
||||
uni.$emit('reloadTeamCustomers')
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
toast(res?.message || '修改失败');
|
||||
}
|
||||
}
|
||||
|
||||
async function unBindArchive() {
|
||||
await uniConfirm('确定删除档案吗?')
|
||||
const res = await api('unbindMiniAppArchive', { id: customer.value._id, corpId: corpId.value, teamId: teamId.value, miniAppId: account.value.openid });
|
||||
if (res && res.success) {
|
||||
await toast('删除成功');
|
||||
uni.$emit('reloadTeamCustomers')
|
||||
uni.navigateBack();
|
||||
} else {
|
||||
toast(res?.message || '删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function getReferenceCustomer() {
|
||||
const res = await api('getRefrencePeople', { corpId: corpId.value, id: referenceCustomerId.value });
|
||||
referenceCustomer.value = res && res.data ? res.data : null;
|
||||
}
|
||||
|
||||
async function getTeam(corpId, teamId, customerId) {
|
||||
const res = await api('getTeamData', { teamId, corpId });
|
||||
if (res && res.data) {
|
||||
const team = res.data;
|
||||
const qrcode = team && Array.isArray(team.qrcodes) ? team.qrcodes[0] : null;
|
||||
const healthTempList = qrcode && Array.isArray(qrcode.healthTempList) ? qrcode.healthTempList : [];
|
||||
healthTypes.value = healthTempList.filter(i => typeof i.templateType === 'string' && i.templateType.trim() && i.archiveRecommend === true).map(i => i.templateType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onLoad(options => {
|
||||
pageOptions.value = { ...options };
|
||||
teamId.value = options.teamId;
|
||||
corpId.value = options.corpId;
|
||||
customerId.value = options.id || '';
|
||||
bindCustomerId.value = options.bindCustomerId || '';
|
||||
corpUserId.value = options.corpUserId || '';
|
||||
referenceCustomerId.value = options.referenceCustomerId || '';
|
||||
source.value = options.source || '';
|
||||
redirectUrl.value = options.redirectUrl || '';
|
||||
uni.setNavigationBarTitle({ title: customerId.value ? '编辑档案' : '新增档案' })
|
||||
})
|
||||
|
||||
useLoad(options => {
|
||||
init();
|
||||
})
|
||||
|
||||
watch(hisQueryFormItems, (h) => {
|
||||
if (h && h.length === 0 && step.value === 0) {
|
||||
step.value = 1;
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
<style scoped></style>
|
||||
@ -1,20 +1,29 @@
|
||||
<template>
|
||||
<full-page>
|
||||
<view class="claim-page">
|
||||
<view v-if="tip" class="tip">{{ tip }}</view>
|
||||
<view v-else class="tip">加载中...</view>
|
||||
</view>
|
||||
<view>
|
||||
<full-page>
|
||||
<view class="claim-page">
|
||||
<view v-if="tip" class="tip">{{ tip }}</view>
|
||||
<view v-else class="tip">加载中...</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 class="poster-dialog">
|
||||
<image class="poster-image" :src="issue?.posterUrl || ''" mode="widthFix" />
|
||||
<image
|
||||
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-btn" :class="{ disabled: claiming }" @click="acceptCoupon">
|
||||
{{ claiming ? "领取中..." : "接受" }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</full-page>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -23,21 +32,29 @@ 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 { hideLoading, loading, toast } from "@/utils/widget";
|
||||
import { normalizeCorpId } from "@/utils/api-base-config";
|
||||
import { useTeamAccess } from "@/hooks/use-team-access";
|
||||
import FullPage from "@/components/full-page.vue";
|
||||
|
||||
const env = __VITE_ENV__;
|
||||
const appid = env.MP_WX_APP_ID;
|
||||
const { account } = storeToRefs(useAccount());
|
||||
const { login } = useAccount();
|
||||
const { getTeams, login } = useAccount();
|
||||
const { openTeamLogin: openTeamInviteLogin, ensureTeamAdded: ensureJoinedTeam } = useTeamAccess();
|
||||
|
||||
const corpId = ref("");
|
||||
const issueId = ref("");
|
||||
const memberId = ref("");
|
||||
const issue = ref(null);
|
||||
const issueCustomer = ref(null);
|
||||
const tip = ref("");
|
||||
const claiming = ref(false);
|
||||
const autoPrompted = ref(false);
|
||||
const posterVisible = ref(false);
|
||||
const archiveBindPending = ref(false);
|
||||
const bindingTeamId = ref("");
|
||||
const bindCorpName = ref("");
|
||||
|
||||
const projectNameText = computed(() => {
|
||||
const projects = Array.isArray(issue.value?.projectSnapshot) ? issue.value.projectSnapshot : [];
|
||||
@ -54,7 +71,7 @@ async function ensureLogin() {
|
||||
if (!account.value) await login();
|
||||
if (!account.value) {
|
||||
tip.value = "请先登录后再领取体验券";
|
||||
uni.navigateTo({ url: "/pages/login/login" });
|
||||
await openTeamLogin();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -77,53 +94,132 @@ async function loadIssueDetail() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
async function getIssueCustomer({ force = false } = {}) {
|
||||
if (!corpId.value || !memberId.value) return null;
|
||||
if (issueCustomer.value && !force) return issueCustomer.value;
|
||||
|
||||
try {
|
||||
const countRes = await api(
|
||||
"getWxAppCustomerCount",
|
||||
{ miniAppId: openid, corpId: corpId.value },
|
||||
const detail = await api(
|
||||
"getCustomerByCustomerId",
|
||||
{ corpId: corpId.value, customerId: memberId.value },
|
||||
false
|
||||
);
|
||||
return Number(countRes?.data || 0) > 0;
|
||||
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("getWxAppCustomerCount failed", e);
|
||||
return false;
|
||||
console.warn("getCustomerByCustomerId failed", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function goBindArchive() {
|
||||
uni.navigateTo({
|
||||
url: `/pages/archive/archive-manage?corpId=${encodeURIComponent(corpId.value || "")}`,
|
||||
async function hasBoundArchive() {
|
||||
const openid = account.value?.openid || "";
|
||||
if (!openid) return false;
|
||||
|
||||
const customer = await getIssueCustomer({ force: true });
|
||||
return Boolean(customer && String(customer.miniAppId || "") === String(openid));
|
||||
}
|
||||
|
||||
function buildClaimUrl() {
|
||||
const params = [
|
||||
`corpId=${encodeURIComponent(corpId.value || "")}`,
|
||||
`issueId=${encodeURIComponent(issueId.value || "")}`,
|
||||
memberId.value ? `memberId=${encodeURIComponent(memberId.value)}` : "",
|
||||
].filter(Boolean).join("&");
|
||||
return `/pages/experience-coupon/claim?${params}`;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
function buildArchiveBindUrl() {
|
||||
const params = [
|
||||
`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() {
|
||||
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;
|
||||
const expireAt = getIssueExpireAt();
|
||||
return Boolean(expireAt && Date.now() > expireAt);
|
||||
}
|
||||
|
||||
async function acceptCoupon() {
|
||||
@ -141,6 +237,10 @@ async function acceptCoupon() {
|
||||
if (!res?.success) {
|
||||
tip.value = res?.message || "领取失败";
|
||||
toast(tip.value);
|
||||
if (tip.value.includes("绑定本体验券发放的档案")) {
|
||||
posterVisible.value = false;
|
||||
openArchiveBindPage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
toast("领取成功");
|
||||
@ -184,17 +284,7 @@ async function promptClaim() {
|
||||
|
||||
const bound = await hasBoundArchive();
|
||||
if (!bound) {
|
||||
tip.value = "请先绑定档案后再领取体验券";
|
||||
try {
|
||||
await confirm("请先绑定档案后再领取体验券", {
|
||||
title: "提示",
|
||||
confirmText: "去绑定",
|
||||
cancelText: "取消",
|
||||
});
|
||||
goBindArchive();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
openArchiveBindPage();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -205,17 +295,39 @@ async function promptClaim() {
|
||||
async function bootstrap() {
|
||||
loading("加载中...");
|
||||
try {
|
||||
const okLogin = await ensureLogin();
|
||||
if (!okLogin) return;
|
||||
const okDetail = await loadIssueDetail();
|
||||
if (!okDetail) return;
|
||||
const okLogin = await ensureLogin();
|
||||
if (!okLogin) return;
|
||||
const phoneAuthorized = await ensurePhoneAuthorized();
|
||||
if (!phoneAuthorized) return;
|
||||
const teamAdded = await ensureTeamAdded();
|
||||
if (!teamAdded) return;
|
||||
await promptClaim();
|
||||
} finally {
|
||||
hideLoading();
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((options = {}) => {
|
||||
function parseOptions(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 || "");
|
||||
issueId.value = options.issueId || options.id || "";
|
||||
memberId.value = options.memberId || options.customerId || "";
|
||||
@ -228,9 +340,11 @@ onLoad((options = {}) => {
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (issue.value && issue.value.status === "issued" && tip.value.includes("绑定")) {
|
||||
if (archiveBindPending.value && issue.value && issue.value.status === "issued") {
|
||||
archiveBindPending.value = false;
|
||||
autoPrompted.value = false;
|
||||
promptClaim();
|
||||
tip.value = "";
|
||||
posterVisible.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -249,9 +363,13 @@ onShow(() => {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.poster-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -264,6 +382,7 @@ onShow(() => {
|
||||
.poster-dialog {
|
||||
width: 100%;
|
||||
max-width: 620rpx;
|
||||
max-height: calc(100vh - 80rpx);
|
||||
overflow: hidden;
|
||||
border-radius: 20rpx;
|
||||
background: #fff;
|
||||
@ -272,15 +391,26 @@ onShow(() => {
|
||||
.poster-image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 58vh;
|
||||
max-height: 700rpx;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.poster-image--placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9aa5b1;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.poster-content {
|
||||
padding: 28rpx 30rpx 8rpx;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
max-height: 160rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.poster-btn {
|
||||
@ -298,4 +428,4 @@ onShow(() => {
|
||||
.poster-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -9,12 +9,13 @@
|
||||
>
|
||||
<view class="page-body">
|
||||
<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-for="item in list"
|
||||
:key="item._id"
|
||||
class="coupon-card"
|
||||
:class="item.cardClass"
|
||||
@click="openPoster(item)"
|
||||
>
|
||||
<image
|
||||
@ -25,7 +26,14 @@
|
||||
/>
|
||||
<view v-else class="coupon-poster coupon-poster--empty">体验券</view>
|
||||
<view class="coupon-body">
|
||||
<view class="coupon-title">{{ item.activityName || "体验券" }}</view>
|
||||
<view class="coupon-title-row">
|
||||
<view class="coupon-title">{{ item.activityName || "体验券" }}</view>
|
||||
</view>
|
||||
<view class="coupon-state-row">
|
||||
<view class="coupon-status" :class="item.statusClass">
|
||||
{{ item.displayStatusText }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon-info-grid">
|
||||
<view class="coupon-info-row coupon-info-row--projects">
|
||||
<text class="coupon-info-label">项目</text>
|
||||
@ -43,14 +51,20 @@
|
||||
</view>
|
||||
<view class="coupon-info-row">
|
||||
<text class="coupon-info-label">项目有效期</text>
|
||||
<text>{{ item.validText }}</text>
|
||||
<text>{{ item.projectValidText }}</text>
|
||||
</view>
|
||||
<view class="coupon-info-row">
|
||||
<text class="coupon-info-label">活动有效期</text>
|
||||
<text>{{ item.activityValidText }}</text>
|
||||
<text class="coupon-info-label">体验券有效期</text>
|
||||
<text>{{ item.couponValidText }}</text>
|
||||
</view>
|
||||
<view class="coupon-info-row">
|
||||
<text class="coupon-info-label">发送时间</text>
|
||||
<text>{{ item.issueTimeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon-action">点击查看海报领取</view>
|
||||
<view class="coupon-action" :class="{ disabled: item.displayStatus !== 'issued' }">
|
||||
{{ item.actionText }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@ -102,9 +116,18 @@ const posterVisible = ref(false);
|
||||
const current = ref(null);
|
||||
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) {
|
||||
if (!ts) return "";
|
||||
const d = new Date(Number(ts));
|
||||
const time = toTimestamp(ts);
|
||||
if (!time) return "";
|
||||
const d = new Date(time);
|
||||
if (Number.isNaN(d.getTime())) return "";
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, "0");
|
||||
@ -112,15 +135,26 @@ function formatDate(ts) {
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function formatDateTime(ts) {
|
||||
const time = toTimestamp(ts);
|
||||
if (!time) return "-";
|
||||
const d = new Date(time);
|
||||
if (Number.isNaN(d.getTime())) return "-";
|
||||
const h = String(d.getHours()).padStart(2, "0");
|
||||
const min = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${formatDate(time)} ${h}:${min}`;
|
||||
}
|
||||
|
||||
function projectText(projects) {
|
||||
const arr = Array.isArray(projects) ? projects : [];
|
||||
if (!arr.length) return "未配置项目";
|
||||
return arr.map((p) => `${p.projectName || "项目"}×${p.usageCount || 1}`).join("、");
|
||||
}
|
||||
|
||||
function buildValidText(item) {
|
||||
function buildProjectValidText(item) {
|
||||
const rule = item.projectValidRuleSnapshot || {};
|
||||
if (item.projectExpireTime) return `有效期至 ${formatDate(item.projectExpireTime)}`;
|
||||
const projectExpireAt = toTimestamp(item.projectExpireTime);
|
||||
if (projectExpireAt) return `有效期至 ${formatDate(projectExpireAt)}`;
|
||||
if (rule.type === "fixedDate" && rule.fixedDate) {
|
||||
return `有效期至 ${formatDate(rule.fixedDate)}`;
|
||||
}
|
||||
@ -130,23 +164,20 @@ function buildValidText(item) {
|
||||
return "领取后按活动规则生效";
|
||||
}
|
||||
|
||||
function buildActivityValidText(item) {
|
||||
function getCouponExpireAt(item) {
|
||||
return toTimestamp(item.activityEndTime || item.displayExpireTime);
|
||||
}
|
||||
|
||||
function buildCouponValidText(item) {
|
||||
const start = formatDate(item.activityStartTime) || "-";
|
||||
const end = formatDate(item.activityEndTime) || "-";
|
||||
const expireAt = getCouponExpireAt(item);
|
||||
const end = formatDate(expireAt) || "-";
|
||||
if (start === "-" && end === "-") return "未配置有效期";
|
||||
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) {
|
||||
const expireAt = getExpireAt(item);
|
||||
const expireAt = getCouponExpireAt(item);
|
||||
if (!expireAt) return false;
|
||||
return Date.now() > expireAt;
|
||||
}
|
||||
@ -184,20 +215,33 @@ async function resolveContext(options = {}) {
|
||||
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);
|
||||
function resolveDisplayStatus(item) {
|
||||
const status = item.displayStatus || item.status || "issued";
|
||||
if (status === "claimed") {
|
||||
return {
|
||||
displayStatus: "claimed",
|
||||
displayStatusText: "已领取",
|
||||
actionText: "已领取",
|
||||
cardClass: "coupon-card--claimed",
|
||||
statusClass: "coupon-status--claimed",
|
||||
};
|
||||
}
|
||||
if (status === "expired" || status === "void" || isExpired(item)) {
|
||||
return {
|
||||
displayStatus: "expired",
|
||||
displayStatusText: "已过期",
|
||||
actionText: "已过期",
|
||||
cardClass: "coupon-card--expired",
|
||||
statusClass: "coupon-status--expired",
|
||||
};
|
||||
}
|
||||
return {
|
||||
displayStatus: "issued",
|
||||
displayStatusText: "待领取",
|
||||
actionText: "点击查看海报领取",
|
||||
cardClass: "coupon-card--issued",
|
||||
statusClass: "coupon-status--issued",
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCoupons(options = {}) {
|
||||
@ -213,7 +257,6 @@ async function loadCoupons(options = {}) {
|
||||
{
|
||||
corpId: corpId.value,
|
||||
customerId: customerId.value,
|
||||
status: "issued",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
},
|
||||
@ -225,20 +268,19 @@ async function loadCoupons(options = {}) {
|
||||
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),
|
||||
activityValidText: buildActivityValidText(item),
|
||||
});
|
||||
}
|
||||
list.value = valid;
|
||||
list.value = raw
|
||||
.map((item) => {
|
||||
const status = resolveDisplayStatus(item);
|
||||
return {
|
||||
...item,
|
||||
...status,
|
||||
projectText: projectText(item.projectSnapshot),
|
||||
projectValidText: buildProjectValidText(item),
|
||||
couponValidText: buildCouponValidText(item),
|
||||
issueTimeText: formatDateTime(item.issueTime || item.createTime),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => Number(b.issueTime || b.createTime || 0) - Number(a.issueTime || a.createTime || 0));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@ -255,6 +297,10 @@ async function refreshCoupons() {
|
||||
}
|
||||
|
||||
function openPoster(item) {
|
||||
if (item?.displayStatus !== "issued") {
|
||||
toast(item?.displayStatusText || "当前体验券不可领取");
|
||||
return;
|
||||
}
|
||||
if (!item?.posterUrl) {
|
||||
toast("该体验券暂无海报");
|
||||
return;
|
||||
@ -338,6 +384,11 @@ onShow(async () => {
|
||||
box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06);
|
||||
}
|
||||
|
||||
.coupon-card--claimed,
|
||||
.coupon-card--expired {
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.coupon-poster {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
@ -359,11 +410,48 @@ onShow(async () => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.coupon-title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.coupon-state-row {
|
||||
display: flex;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.coupon-title {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #222;
|
||||
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 {
|
||||
@ -416,6 +504,10 @@ onShow(async () => {
|
||||
color: #ff8a00;
|
||||
}
|
||||
|
||||
.coupon-action.disabled {
|
||||
color: #a8abb2;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 120rpx 0;
|
||||
text-align: center;
|
||||
|
||||
@ -86,8 +86,14 @@ function formatDate(ts) {
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
function pickCorpName(team = {}) {
|
||||
return team.licenseHospitalName || team.leaderCorp || team.corpName || "-";
|
||||
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 || "";
|
||||
}
|
||||
|
||||
const groups = computed(() => {
|
||||
@ -139,16 +145,6 @@ function applySelection(selection = {}) {
|
||||
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 = {}) {
|
||||
@ -181,7 +177,7 @@ async function resolveContext(options = {}) {
|
||||
|
||||
if (!corpId.value) corpId.value = normalizeCorpId(matched?.corpId || "");
|
||||
if (!teamId.value) teamId.value = matched?.teamId || "";
|
||||
if (!corpName.value) corpName.value = pickCorpName(matched || {});
|
||||
await loadCorpName();
|
||||
|
||||
if (customerId.value) return true;
|
||||
|
||||
@ -240,6 +236,7 @@ async function syncSelectedProfile() {
|
||||
if (!selected) return false;
|
||||
remove(RIGHTS_SELECTION_CACHE_KEY);
|
||||
applySelection(selected);
|
||||
await loadCorpName();
|
||||
await loadRights();
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<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">
|
||||
<scroll-view v-else scroll-y class="archive-list">
|
||||
<view
|
||||
v-for="item in options"
|
||||
:key="item.key"
|
||||
@ -22,7 +22,7 @@
|
||||
<view class="archive-id">证件号:{{ item.idCardText }}</view>
|
||||
<view class="archive-corp">{{ item.corpName }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</full-page>
|
||||
</template>
|
||||
@ -33,14 +33,14 @@ 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 { get, set } 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 loading = ref(false);
|
||||
const options = ref([]);
|
||||
@ -50,10 +50,6 @@ 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 "";
|
||||
@ -77,16 +73,6 @@ function buildMetaText(customer = {}) {
|
||||
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 || "");
|
||||
@ -119,35 +105,34 @@ async function loadArchives() {
|
||||
|
||||
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 currentTeam = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
|
||||
const selectedCorpId = normalizeCorpId(currentTeam.corpId || currentCorpId.value);
|
||||
|
||||
const list = responses
|
||||
.flat()
|
||||
.map(({ team, customer }) => ({
|
||||
key: `${team.corpId}_${customer._id}`,
|
||||
corpId: team.corpId,
|
||||
teamId: team.teamId || "",
|
||||
if (!selectedCorpId) {
|
||||
options.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const [corpRes, customerRes] = await Promise.all([
|
||||
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 || "",
|
||||
name: customer.name || "",
|
||||
relationship: customer.relationship || "",
|
||||
metaText: buildMetaText(customer),
|
||||
idCardText: maskIdCard(customer.idCard),
|
||||
corpName: pickCorpName(team),
|
||||
corpName: archiveCorpName,
|
||||
}))
|
||||
.filter((item) => item.corpId && item.customerId)
|
||||
.filter((item) => item.customerId)
|
||||
.sort((a, b) => {
|
||||
const aScore =
|
||||
(a.corpId === currentCorpId.value ? 4 : 0) +
|
||||
@ -210,12 +195,16 @@ onLoad(async (optionsData = {}) => {
|
||||
|
||||
<style scoped>
|
||||
.page-body {
|
||||
min-height: 100%;
|
||||
height: 100vh;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: #f5f6fa;
|
||||
}
|
||||
|
||||
.archive-list {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.archive-card {
|
||||
background: #fff;
|
||||
border-radius: 18rpx;
|
||||
|
||||
@ -70,7 +70,7 @@ function change({ title, value }) {
|
||||
}
|
||||
|
||||
function confirm() {
|
||||
if (!tempRef.value.verify() || Object.keys(form.value).length === 0) return;
|
||||
if (!tempRef.value.verify()) return;
|
||||
if (id.value) {
|
||||
updateHealthRecord();
|
||||
} else {
|
||||
|
||||
@ -216,23 +216,26 @@ defineExpose({
|
||||
}
|
||||
|
||||
.consult-grid {
|
||||
height: 208rpx;
|
||||
min-height: 208rpx;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 30rpx;
|
||||
padding: 26rpx 22rpx;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
align-items: center;
|
||||
gap: 32rpx;
|
||||
grid-auto-rows: 120rpx;
|
||||
align-items: start;
|
||||
column-gap: 0;
|
||||
row-gap: 24rpx;
|
||||
box-shadow: 0 8rpx 10rpx 0 rgba(60, 169, 145, 0.06);
|
||||
}
|
||||
|
||||
.consult-item {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
gap: 10rpx;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -242,7 +245,7 @@ defineExpose({
|
||||
|
||||
.item-icon {
|
||||
width: 80rpx;
|
||||
height: 80;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@ -270,9 +273,12 @@ defineExpose({
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 30rpx;
|
||||
color: #666d76;
|
||||
max-width: 100%;
|
||||
color: #596273;
|
||||
font-size: 26rpx;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -48,6 +48,7 @@ import pageLoading from "./loading.vue";
|
||||
const { account } = storeToRefs(useAccount());
|
||||
const { login, getTeams } = useAccount();
|
||||
const env = __VITE_ENV__;
|
||||
const appid = env.MP_WX_APP_ID;
|
||||
const shareAppVersion = env.MP_SHARE_WX_APP_VERSION;
|
||||
|
||||
const team = ref(null);
|
||||
@ -58,6 +59,8 @@ const consultRef = ref(null);
|
||||
const archiveRef = ref(null);
|
||||
const corpUserIds = ref({});
|
||||
const referenceCustomerIds = ref({});
|
||||
const pendingAction = ref(null);
|
||||
const awaitingArchiveBinding = ref(false);
|
||||
const HOME_CURRENT_TEAM_CACHE_KEY = "home-current-team-info";
|
||||
|
||||
const corpId = computed(() => team.value?.corpId);
|
||||
@ -84,7 +87,7 @@ function cacheCurrentTeam(currentTeam) {
|
||||
|
||||
function isSameTeam(candidate, target = {}) {
|
||||
const targetTeamId = target.teamId || "";
|
||||
if (!candidate || !candidate.teamId || candidate.teamId !== targetTeamId) return false;
|
||||
if (!candidate || !candidate.teamId || String(candidate.teamId) !== String(targetTeamId)) return false;
|
||||
|
||||
const targetCorpId = normalizeCorpId(target.corpId || "");
|
||||
if (!targetCorpId) return true;
|
||||
@ -95,37 +98,183 @@ function isSameTeam(candidate, target = {}) {
|
||||
|
||||
async function changeTeam({ teamId, corpId, corpName }) {
|
||||
loading.value = true;
|
||||
const res = await api("getTeamData", { teamId, corpId });
|
||||
loading.value = false;
|
||||
if (res && res.data) {
|
||||
team.value = {
|
||||
...res.data,
|
||||
corpId: normalizeCorpId(res.data.corpId || corpId),
|
||||
};
|
||||
team.value.corpName = corpName;
|
||||
cacheCurrentTeam(team.value);
|
||||
} else {
|
||||
try {
|
||||
const res = await api("getTeamData", { teamId, corpId, withCorpName: true });
|
||||
if (res && res.data) {
|
||||
team.value = {
|
||||
...res.data,
|
||||
corpId: normalizeCorpId(res.data.corpId || corpId),
|
||||
};
|
||||
team.value.corpName = corpName || res.data.corpName || res.data.corp_name || "";
|
||||
cacheCurrentTeam(team.value);
|
||||
return team.value;
|
||||
}
|
||||
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) {
|
||||
loading.value = true;
|
||||
teams.value = await getTeams();
|
||||
const inviteIdentity = typeof inviteTeam === "string" ? { teamId: inviteTeam } : (inviteTeam || {});
|
||||
const cachedIdentity = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
|
||||
const targetIdentity = getTeamIdentity({
|
||||
teamId: inviteIdentity.teamId || team.value?.teamId || cachedIdentity.teamId,
|
||||
corpId: inviteIdentity.corpId || team.value?.corpId || cachedIdentity.corpId,
|
||||
});
|
||||
const validTeam = teams.value.find(i => isSameTeam(i, targetIdentity));
|
||||
const firstTeam = teams.value[0]
|
||||
if (validTeam || firstTeam) {
|
||||
changeTeam(validTeam || firstTeam);
|
||||
} else {
|
||||
try {
|
||||
teams.value = await getTeams();
|
||||
const inviteIdentity = typeof inviteTeam === "string" ? { teamId: inviteTeam } : (inviteTeam || {});
|
||||
const cachedIdentity = get(HOME_CURRENT_TEAM_CACHE_KEY) || {};
|
||||
const targetIdentity = getTeamIdentity({
|
||||
teamId: inviteIdentity.teamId || team.value?.teamId || cachedIdentity.teamId,
|
||||
corpId: inviteIdentity.corpId || team.value?.corpId || cachedIdentity.corpId,
|
||||
});
|
||||
let validTeam = teams.value.find(i => isSameTeam(i, targetIdentity));
|
||||
|
||||
if (pendingAction.value && targetIdentity.teamId && account.value?.openid) {
|
||||
if (!validTeam) {
|
||||
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;
|
||||
return null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// useLoad((opts) => {
|
||||
@ -134,8 +283,8 @@ async function getMatchTeams(inviteTeam = null) {
|
||||
// }
|
||||
// });
|
||||
|
||||
onLoad(() => {
|
||||
if (!account.value) login();
|
||||
onLoad((options = {}) => {
|
||||
pendingAction.value = parsePendingAction(options);
|
||||
})
|
||||
|
||||
onShow(async () => {
|
||||
@ -149,7 +298,11 @@ onShow(async () => {
|
||||
referenceCustomerIds.value[inviteTeam.teamId] = inviteTeam.referenceCustomerId;
|
||||
}
|
||||
if (account.value && account.value.openid) {
|
||||
getMatchTeams(inviteTeam && inviteTeam.teamId ? inviteTeam : null);
|
||||
const targetTeam = pendingAction.value?.teamId
|
||||
? { teamId: pendingAction.value.teamId, corpId: pendingAction.value.corpId }
|
||||
: null;
|
||||
await getMatchTeams(targetTeam || (inviteTeam && inviteTeam.teamId ? inviteTeam : null));
|
||||
await continuePendingAction();
|
||||
} else {
|
||||
teams.value = [];
|
||||
}
|
||||
@ -161,7 +314,7 @@ onShow(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
onShareAppMessage((res) => {
|
||||
onShareAppMessage(() => {
|
||||
if (team.value && team.value.supportPatientForward === 'YES') {
|
||||
const customer = customers.value[0];
|
||||
const referenceCustomerId = customer ? customer._id : '';
|
||||
@ -173,9 +326,9 @@ onShareAppMessage((res) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(account, (n, o) => {
|
||||
watch(account, async (n, o) => {
|
||||
if (n && !o) {
|
||||
getMatchTeams();
|
||||
if (!pendingAction.value) await getMatchTeams();
|
||||
} else if (!n && o) {
|
||||
teams.value = [];
|
||||
}
|
||||
|
||||
@ -132,11 +132,14 @@ async function bindTeam() {
|
||||
return toast("关联团队失败");
|
||||
}
|
||||
await syncExternalUserRelation();
|
||||
if (redirectUrl.value) {
|
||||
return attempToPage(redirectUrl.value);
|
||||
}
|
||||
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
|
||||
if (res1 && res1.data > 0) {
|
||||
toHome();
|
||||
} else {
|
||||
attempToPage(redirectUrl.value)
|
||||
attempToPage(`/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,7 +150,7 @@ async function getPhoneNumber(e) {
|
||||
if (!res) return;
|
||||
}
|
||||
if (team.value) {
|
||||
bindTeam()
|
||||
await bindTeam()
|
||||
} else if (redirectUrl.value) {
|
||||
await attempToPage(redirectUrl.value);
|
||||
} else {
|
||||
@ -156,20 +159,33 @@ async function getPhoneNumber(e) {
|
||||
}
|
||||
|
||||
async function attempToPage(url) {
|
||||
const res1 = attempRedirect(url);
|
||||
if (res1) return;
|
||||
const res2 = attempSwitchTab(url);
|
||||
if (res2) return;
|
||||
toHome();
|
||||
if (!url) {
|
||||
toHome();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await attempRedirect(url);
|
||||
return;
|
||||
} catch {
|
||||
// redirectTo 不支持 tabBar 时再尝试 switchTab。
|
||||
}
|
||||
try {
|
||||
await attempSwitchTab(url);
|
||||
return;
|
||||
} catch {
|
||||
toHome();
|
||||
}
|
||||
}
|
||||
|
||||
onLoad((opts) => {
|
||||
redirectUrl.value = opts.redirectUrl || "";
|
||||
if (opts.source === "teamInvite") {
|
||||
team.value = get("invite-team-info");
|
||||
redirectUrl.value = `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
|
||||
if (!redirectUrl.value && team.value) {
|
||||
redirectUrl.value = team.value.redirectUrl || `/pages/archive/edit-archive?corpUserId=${team.value.corpUserId || ''}&teamId=${team.value.teamId}&corpId=${team.value.corpId}`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
redirectUrl.value = opts.redirectUrl || "";
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
|
||||
@ -75,8 +75,13 @@ async function syncExternalUserRelation({ corpId, externalUserId, corpUserId })
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, referenceCustomerId }) {
|
||||
function getAiConsultEntryUrl({ teamId, corpId, qrid }) {
|
||||
return `/pages/ai-consult/entry?teamId=${teamId || ''}&corpId=${corpId || ''}&qrid=${qrid || ''}`;
|
||||
}
|
||||
|
||||
async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, referenceCustomerId, type }) {
|
||||
const normalizedCorpId = normalizeCorpId(corpId);
|
||||
const redirectUrl = type === 'aiConsult' ? getAiConsultEntryUrl({ teamId, corpId: normalizedCorpId, qrid }) : '';
|
||||
loading.value = true;
|
||||
const res = await api("getTeamData", { teamId, corpId: normalizedCorpId, withCorpName: true });
|
||||
loading.value = false;
|
||||
@ -96,13 +101,14 @@ async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, re
|
||||
});
|
||||
await login('', { forceVerify: true })
|
||||
if (account.value && account.value.mobile) {
|
||||
bindTeam({ corpUserId, externalUserId })
|
||||
bindTeam({ corpUserId, externalUserId, redirectUrl })
|
||||
} else {
|
||||
set("invite-team-info", {
|
||||
corpUserId,
|
||||
externalUserId,
|
||||
qrid,
|
||||
referenceCustomerId,
|
||||
redirectUrl,
|
||||
corpId: team.value.corpId,
|
||||
teamId: team.value.teamId,
|
||||
corpName: team.value.corpName,
|
||||
@ -121,7 +127,7 @@ async function changeTeam({ teamId, corpId, corpUserId, externalUserId, qrid, re
|
||||
}
|
||||
}
|
||||
|
||||
async function bindTeam({ corpUserId, externalUserId }) {
|
||||
async function bindTeam({ corpUserId, externalUserId, redirectUrl = '' }) {
|
||||
const res = await api('bindWxappWithTeam', { appid, corpId: team.value.corpId, teamId: team.value.teamId, openid: account.value.openid });
|
||||
if (!res || !res.success) {
|
||||
return toast("关联团队失败");
|
||||
@ -131,6 +137,9 @@ async function bindTeam({ corpUserId, externalUserId }) {
|
||||
externalUserId,
|
||||
corpUserId,
|
||||
});
|
||||
if (redirectUrl) {
|
||||
return uni.redirectTo({ url: redirectUrl });
|
||||
}
|
||||
const res1 = await api('getWxAppCustomerCount', { miniAppId: account.value.openid, corpId: team.value.corpId, teamId: team.value.teamId });
|
||||
if (res1 && res1.data > 0) {
|
||||
uni.switchTab({
|
||||
@ -143,24 +152,47 @@ 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) => {
|
||||
if (options.q) {
|
||||
opts.value = JSON.stringify(options)
|
||||
changeTeam(parseInviteOptions(options));
|
||||
} else if (options.type === 'experienceCoupon' && options.issueId && options.corpId) {
|
||||
const normalizedOptions = normalizePlainOptions(options);
|
||||
if (normalizedOptions.q) {
|
||||
opts.value = JSON.stringify(normalizedOptions)
|
||||
changeTeam(parseInviteOptions(normalizedOptions));
|
||||
} else if (normalizedOptions.type === 'experienceCoupon' && normalizedOptions.issueId && normalizedOptions.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)}` : "",
|
||||
`corpId=${encodeURIComponent(normalizedOptions.corpId || "")}`,
|
||||
`issueId=${encodeURIComponent(normalizedOptions.issueId || "")}`,
|
||||
normalizedOptions.memberId ? `memberId=${encodeURIComponent(normalizedOptions.memberId)}` : "",
|
||||
normalizedOptions.couponId ? `couponId=${encodeURIComponent(normalizedOptions.couponId)}` : "",
|
||||
normalizedOptions.unionid ? `unionid=${encodeURIComponent(normalizedOptions.unionid)}` : "",
|
||||
normalizedOptions.externalUserId ? `externalUserId=${encodeURIComponent(normalizedOptions.externalUserId)}` : "",
|
||||
].filter(Boolean).join("&");
|
||||
uni.redirectTo({
|
||||
url: `/pages/experience-coupon/claim?${params}`,
|
||||
});
|
||||
} else if (options.type === 'archive' || (options.teamId && options.corpId)) {
|
||||
changeTeam(options);
|
||||
} else if (normalizedOptions.type === 'appointmentRegistration' && normalizedOptions.customerId && normalizedOptions.corpId) {
|
||||
const params = [
|
||||
`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);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -1231,6 +1231,25 @@ $primary-color: #0877F1;
|
||||
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 {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
@ -59,6 +59,28 @@
|
||||
mode="aspectFill" />
|
||||
</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
|
||||
v-else
|
||||
@ -112,8 +134,32 @@ 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) => {
|
||||
// 如果没有尺寸信息,使用默认样式
|
||||
imageInfo.width = imageInfo.width || imageInfo.Width;
|
||||
@ -299,4 +345,95 @@ async function markArticleRead(sendId, articleId) {
|
||||
|
||||
<style scoped lang="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>
|
||||
@ -85,7 +85,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { getRate, submitRate } from '@/api/corp/rate';
|
||||
import api from '@/utils/api';
|
||||
import { toast } from '@/utils/widget';
|
||||
|
||||
// Props
|
||||
@ -98,6 +98,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
corpId: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
@ -121,12 +125,15 @@ const ratingText = computed(() => {
|
||||
const openEvaluationPopup = async () => {
|
||||
evaluationRating.value = 0
|
||||
evaluationComment.value = ''
|
||||
const res = await getRate(props.extension.rateId);
|
||||
const res = await api('getRateRecord', {
|
||||
id: props.extension.rateId,
|
||||
corpId: props.extension.corpId || props.corpId
|
||||
});
|
||||
if (res && res.success) {
|
||||
record.value = res.data;
|
||||
evaluationRating.value = typeof res.data.rate === 'number' ? res.data.rate : 0;
|
||||
evaluationComment.value = typeof res.data.words === 'string' ? res.data.words : '';
|
||||
if (record.value.status === 'init') {
|
||||
record.value = res.record || {};
|
||||
evaluationRating.value = typeof record.value.rate === 'number' ? record.value.rate : 0;
|
||||
evaluationComment.value = typeof record.value.words === 'string' ? record.value.words : '';
|
||||
if (!res.rated && res.enable) {
|
||||
evaluationPopup.value.open();
|
||||
emit('popupStatusChange', true); // 通知父组件弹窗已打开
|
||||
} else {
|
||||
@ -158,8 +165,9 @@ const submitEvaluation = async () => {
|
||||
}
|
||||
if (loading.value) return;
|
||||
loading.value = true;
|
||||
const res = await submitRate({
|
||||
const res = await api('submitRateRecord', {
|
||||
id: props.extension.rateId,
|
||||
corpId: props.extension.corpId || props.corpId,
|
||||
rate: evaluationRating.value,
|
||||
words: evaluationComment.value
|
||||
});
|
||||
@ -179,4 +187,4 @@ const submitEvaluation = async () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "../../chat.scss";
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
v-if="payload.description === 'PATIENT_RATE_MESSAGE'"
|
||||
:doctorInfo="doctorInfo"
|
||||
:extension="extension"
|
||||
:corpId="corpId"
|
||||
@popupStatusChange="handlePopupStatusChange"
|
||||
/>
|
||||
</template>
|
||||
@ -17,6 +18,10 @@ const props = defineProps({
|
||||
doctorInfo:{
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
corpId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
@ -37,4 +42,4 @@ const extension = computed(() => {
|
||||
const handlePopupStatusChange = (isOpen) => {
|
||||
emit('popupStatusChange', isOpen);
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -20,10 +20,10 @@
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view class="chat-content" :style="{
|
||||
bottom: (keyboardHeight > 0 ? keyboardHeight + 60 : 60) + 'px',
|
||||
}" scroll-y="true" enhanced="true" bounces="false" :scroll-into-view="scrollIntoView" @scroll="onScroll"
|
||||
@scrolltoupper="handleScrollToUpper" @click="closeMorePanel" ref="chatScrollView">
|
||||
<scroll-view class="chat-content" :style="{
|
||||
bottom: (keyboardHeight > 0 ? keyboardHeight + 60 : 60) + 'px',
|
||||
}" scroll-y="true" enhanced="true" bounces="false" :scroll-into-view="scrollIntoView" @scroll="onScroll"
|
||||
@scrolltoupper="handleScrollToUpper" @click="closeMorePanel" ref="chatScrollView">
|
||||
<!-- 加载更多提示 -->
|
||||
<view class="load-more-tip" v-if="messageList.length >= 15">
|
||||
<view class="loading" v-if="isLoadingMore">
|
||||
@ -390,6 +390,19 @@ 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) {
|
||||
// 图片消息不需要气泡背景
|
||||
@ -397,7 +410,10 @@ function getBubbleClass(message) {
|
||||
return "image-bubble";
|
||||
}
|
||||
if (message.type === "TIMCustomElem") {
|
||||
return message.flow === "out" ? "" : "";
|
||||
if (getCustomMessageType(message) === "benefit") {
|
||||
return "benefit-bubble";
|
||||
}
|
||||
return "custom-bubble";
|
||||
}
|
||||
return message.flow === "out" ? "user-bubble" : "doctor-bubble";
|
||||
}
|
||||
@ -572,44 +588,44 @@ const initTIMCallbacks = async () => {
|
||||
) {
|
||||
seenIds.add(message.ID);
|
||||
uniqueMessages.push(message);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedMessages = [];
|
||||
const mergedSeenIds = new Set();
|
||||
const existingMessages = Array.isArray(messageList.value)
|
||||
? messageList.value
|
||||
: [];
|
||||
|
||||
existingMessages.forEach((message) => {
|
||||
if (!message?.ID) return;
|
||||
if (message.conversationID !== chatInfo.value.conversationID) return;
|
||||
if (mergedSeenIds.has(message.ID)) return;
|
||||
mergedSeenIds.add(message.ID);
|
||||
mergedMessages.push(message);
|
||||
});
|
||||
|
||||
uniqueMessages.forEach((message) => {
|
||||
if (!message?.ID) return;
|
||||
if (mergedSeenIds.has(message.ID)) return;
|
||||
mergedSeenIds.add(message.ID);
|
||||
mergedMessages.push(message);
|
||||
});
|
||||
|
||||
mergedMessages.sort((a, b) => {
|
||||
const ta = Number(a?.lastTime || a?.time || 0) || 0;
|
||||
const tb = Number(b?.lastTime || b?.time || 0) || 0;
|
||||
return ta - tb;
|
||||
});
|
||||
|
||||
messageList.value = mergedMessages;
|
||||
console.log(
|
||||
"消息列表已更新,原始",
|
||||
messages.length,
|
||||
"条,过滤后",
|
||||
messageList.value.length,
|
||||
"条消息"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const mergedMessages = [];
|
||||
const mergedSeenIds = new Set();
|
||||
const existingMessages = Array.isArray(messageList.value)
|
||||
? messageList.value
|
||||
: [];
|
||||
|
||||
existingMessages.forEach((message) => {
|
||||
if (!message?.ID) return;
|
||||
if (message.conversationID !== chatInfo.value.conversationID) return;
|
||||
if (mergedSeenIds.has(message.ID)) return;
|
||||
mergedSeenIds.add(message.ID);
|
||||
mergedMessages.push(message);
|
||||
});
|
||||
|
||||
uniqueMessages.forEach((message) => {
|
||||
if (!message?.ID) return;
|
||||
if (mergedSeenIds.has(message.ID)) return;
|
||||
mergedSeenIds.add(message.ID);
|
||||
mergedMessages.push(message);
|
||||
});
|
||||
|
||||
mergedMessages.sort((a, b) => {
|
||||
const ta = Number(a?.lastTime || a?.time || 0) || 0;
|
||||
const tb = Number(b?.lastTime || b?.time || 0) || 0;
|
||||
return ta - tb;
|
||||
});
|
||||
|
||||
messageList.value = mergedMessages;
|
||||
console.log(
|
||||
"消息列表已更新,原始",
|
||||
messages.length,
|
||||
"条,过滤后",
|
||||
messageList.value.length,
|
||||
"条消息"
|
||||
);
|
||||
|
||||
isCompleted.value = data.isCompleted || false;
|
||||
isLoadingMore.value = false;
|
||||
@ -777,11 +793,11 @@ const scrollToBottom = (immediate = false) => {
|
||||
};
|
||||
|
||||
// 关闭功能栏
|
||||
const closeMorePanel = () => {
|
||||
uni.$emit("closeMorePanel");
|
||||
chatInputRef.value?.blurInput?.();
|
||||
uni.hideKeyboard();
|
||||
};
|
||||
const closeMorePanel = () => {
|
||||
uni.$emit("closeMorePanel");
|
||||
chatInputRef.value?.blurInput?.();
|
||||
uni.hideKeyboard();
|
||||
};
|
||||
|
||||
// 滚动事件
|
||||
const onScroll = throttle((e) => {
|
||||
|
||||
@ -8,10 +8,11 @@
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
<!-- 消息列表项 -->
|
||||
<view v-for="conversation in conversationList" :key="conversation.groupID || conversation.conversationID"
|
||||
<view v-for="conversation in conversationList" :key="conversation.source === 'ai' ? `ai-${conversation.sessionId}` : (conversation.groupID || conversation.conversationID)"
|
||||
class="message-item" @click="handleClickConversation(conversation)">
|
||||
<view class="avatar-container">
|
||||
<GroupAvatar :avatarList="getAvatarList(conversation.groupID)" :size="96" classType="square" />
|
||||
<image v-if="conversation.source === 'ai'" class="avatar" src="/static/home/ai-consult-assistant.png" mode="aspectFill" />
|
||||
<GroupAvatar v-else :avatarList="getAvatarList(conversation.groupID)" :size="96" classType="square" />
|
||||
<view v-if="conversation.unreadCount > 0" class="unread-badge">
|
||||
<text class="unread-text">{{
|
||||
conversation.unreadCount > 99 ? "99+" : conversation.unreadCount
|
||||
@ -21,13 +22,14 @@
|
||||
|
||||
<view class="content">
|
||||
<view class="header">
|
||||
<text class="name">{{ conversation.teamName }}</text>
|
||||
<text class="name">{{ conversation.displayName || conversation.teamName }}</text>
|
||||
|
||||
<text class="time">{{
|
||||
formatMessageTime(conversation.lastMessageTime)
|
||||
}}</text>
|
||||
</view>
|
||||
<view class="patient-info">
|
||||
<text v-if="conversation.source === 'ai'" class="source-tag">咨询</text>
|
||||
咨询人 | {{ conversation.patientName }}
|
||||
</view>
|
||||
<view class="message-preview">
|
||||
@ -72,6 +74,8 @@ import { mergeConversationWithGroupDetails } from "@/utils/conversation-merger.j
|
||||
import { globalUnreadListenerManager } from "@/utils/global-unread-listener.js";
|
||||
import { get } from "@/utils/cache";
|
||||
import { normalizeCorpId } from "@/utils/api-base-config";
|
||||
import api from "@/utils/api";
|
||||
import { removeAiLabel } from "@/utils/ai-consult-display";
|
||||
import useGroupAvatars from "./hooks/use-group-avatars.js";
|
||||
import GroupAvatar from "@/components/group-avatar.vue";
|
||||
import {
|
||||
@ -229,9 +233,10 @@ const loadConversationList = async () => {
|
||||
const result = await globalTimChatManager.getGroupList();
|
||||
if (result && result.success && result.groupList) {
|
||||
// 合并后端群组详细信息(已包含格式化和排序)
|
||||
conversationList.value = await mergeConversationWithGroupDetails(
|
||||
const imConversations = await mergeConversationWithGroupDetails(
|
||||
result.groupList
|
||||
);
|
||||
conversationList.value = mergeConsultationList(imConversations);
|
||||
console.log(
|
||||
"群聊列表加载成功,共",
|
||||
conversationList.value.length,
|
||||
@ -250,6 +255,46 @@ const loadConversationList = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
function mergeConsultationList(imConversations) {
|
||||
const aiConversations = conversationList.value.filter((item) => item.source === "ai");
|
||||
return [...imConversations, ...aiConversations].sort(
|
||||
(left, right) => (right.lastMessageTime || 0) - (left.lastMessageTime || 0)
|
||||
);
|
||||
}
|
||||
|
||||
const loadAiConsultationList = async () => {
|
||||
const corpId = getCurrentTeamCorpId();
|
||||
const miniAppId = openid.value || account.value?.openid || "";
|
||||
if (!corpId || !miniAppId) return;
|
||||
try {
|
||||
const result = await api("getUnifiedConsultSessions", {
|
||||
corpId,
|
||||
miniAppId,
|
||||
source: "ai",
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
}, false);
|
||||
if (!result?.success) return;
|
||||
const imConversations = conversationList.value.filter((item) => item.source !== "ai");
|
||||
const aiConversations = (result.list || []).map((item) => ({
|
||||
source: "ai",
|
||||
sessionId: item.id,
|
||||
corpId: item.corpId,
|
||||
displayName: removeAiLabel(item.assistantName, "咨询助理"),
|
||||
teamName: item.teamName || "",
|
||||
patientName: item.customerName || "未命名患者",
|
||||
lastMessage: removeAiLabel(item.statusLabel, "咨询"),
|
||||
lastMessageTime: item.lastActiveTime || 0,
|
||||
unreadCount: 0,
|
||||
}));
|
||||
conversationList.value = [...imConversations, ...aiConversations].sort(
|
||||
(left, right) => (right.lastMessageTime || 0) - (left.lastMessageTime || 0)
|
||||
);
|
||||
} catch (error) {
|
||||
console.log("加载AI咨询记录异常:", error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖更新定时器
|
||||
let updateTimer = null;
|
||||
|
||||
@ -438,6 +483,13 @@ const formatMessageTime = (timestamp) => {
|
||||
const handleClickConversation = async (conversation) => {
|
||||
console.log("点击会话:", conversation);
|
||||
|
||||
if (conversation.source === "ai") {
|
||||
uni.navigateTo({
|
||||
url: `/pages/ai-consult/chat?corpId=${conversation.corpId}&sessionId=${conversation.sessionId}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即清除本地未读数显示
|
||||
const conversationIndex = conversationList.value.findIndex(
|
||||
(conv) => conv.conversationID === conversation.conversationID
|
||||
@ -480,11 +532,11 @@ const handleLoadMore = () => {
|
||||
|
||||
// 下拉刷新
|
||||
const handleRefresh = async () => {
|
||||
if (!hasImCorpId.value) return;
|
||||
refreshing.value = true;
|
||||
|
||||
try {
|
||||
await loadConversationList();
|
||||
if (hasImCorpId.value) await loadConversationList();
|
||||
await loadAiConsultationList();
|
||||
} finally {
|
||||
refreshing.value = false;
|
||||
}
|
||||
@ -499,7 +551,8 @@ async function init() {
|
||||
console.log("IM初始化失败,继续加载列表");
|
||||
}
|
||||
// 先加载初始会话列表
|
||||
await loadConversationList();
|
||||
if (imReady) await loadConversationList();
|
||||
await loadAiConsultationList();
|
||||
// 再设置监听器,后续通过事件更新列表
|
||||
setupConversationListener();
|
||||
} catch (error) {
|
||||
@ -580,9 +633,7 @@ onHide(() => {
|
||||
});
|
||||
|
||||
watch(hasImCorpId, (n, o) => {
|
||||
if (n && !o) {
|
||||
init();
|
||||
}
|
||||
if (n || !o) init();
|
||||
}, { immediate: true })
|
||||
// 页面卸载
|
||||
onUnmounted(() => {
|
||||
@ -783,6 +834,16 @@ onUnmounted(() => {
|
||||
padding-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.source-tag {
|
||||
display: inline-block;
|
||||
margin-right: 10rpx;
|
||||
padding: 1rpx 8rpx;
|
||||
border-radius: 6rpx;
|
||||
color: #0877f1;
|
||||
background: #e8f3ff;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.subscribe-entry {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
|
||||
@ -61,11 +61,20 @@
|
||||
<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 { normalizeCorpId } from "@/utils/api-base-config";
|
||||
import { toast } from "@/utils/widget";
|
||||
import { useTeamAccess } from "@/hooks/use-team-access";
|
||||
import { hideLoading, loading as showLoading, toast } from "@/utils/widget";
|
||||
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 teamId = ref("");
|
||||
const customerId = ref("");
|
||||
@ -76,6 +85,10 @@ const records = ref([]);
|
||||
const staffNameMap = ref({});
|
||||
const loading = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const corpUserId = ref("");
|
||||
const routeExternalUserId = ref("");
|
||||
const authPending = ref(false);
|
||||
const initialized = ref(false);
|
||||
|
||||
const statusMap = {
|
||||
pending: { label: "未到院" },
|
||||
@ -86,6 +99,126 @@ const statusMap = {
|
||||
const avatarText = computed(() => (customerName.value || "档案").slice(0, 1));
|
||||
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) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
@ -126,13 +259,13 @@ function statusText(item) {
|
||||
}
|
||||
|
||||
function appointmentTypeText(item) {
|
||||
if (item?.appointmentType === "schedule") return item?.scheduleTtile || "排班";
|
||||
if (item?.appointmentType === "medical") return "复诊预约";
|
||||
return "治疗预约";
|
||||
if (item?.appointmentType === "followup") return "复诊预约";
|
||||
if (item?.appointmentType === "treatment") return "治疗预约";
|
||||
return "其他预约";
|
||||
}
|
||||
|
||||
function practitionerLabel(item) {
|
||||
return item?.appointmentType === "medical" ? "医生" : "治疗师";
|
||||
return item?.appointmentType === "treatment" ? "治疗师" : "医生";
|
||||
}
|
||||
|
||||
function practitionerText(item) {
|
||||
@ -243,13 +376,28 @@ onLoad(async (options = {}) => {
|
||||
customerId.value = options.customerId || options.id || "";
|
||||
customerName.value = options.name ? decodeURIComponent(options.name) : "";
|
||||
corpName.value = options.corpName ? decodeURIComponent(options.corpName) : "";
|
||||
corpUserId.value = options.corpUserId || "";
|
||||
routeExternalUserId.value = options.externalUserId || "";
|
||||
if (options.month) selectedMonth.value = options.month;
|
||||
await loadCorpName();
|
||||
const ready = await ensureAccessReady();
|
||||
if (!ready) return;
|
||||
initialized.value = true;
|
||||
await loadStaffNameMap();
|
||||
await loadRecords();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
if (corpId.value && customerId.value) loadRecords();
|
||||
onShow(async () => {
|
||||
if (authPending.value) {
|
||||
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>
|
||||
|
||||
@ -387,4 +535,4 @@ onShow(() => {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -47,18 +47,6 @@
|
||||
<text class="row-value strong">{{ item.treatmentDoctorName || item.doctorName ||
|
||||
staffText(item.treatmentDoctorUserId) }}</text>
|
||||
</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>
|
||||
@ -88,6 +76,16 @@ const refreshing = ref(false);
|
||||
const avatarText = computed(() => (customerName.value || "档案").slice(0, 1));
|
||||
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) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
@ -211,8 +209,8 @@ onLoad(async (options = {}) => {
|
||||
teamId.value = options.teamId || "";
|
||||
customerId.value = options.customerId || options.id || "";
|
||||
customerName.value = options.name ? decodeURIComponent(options.name) : "";
|
||||
corpName.value = options.corpName ? decodeURIComponent(options.corpName) : "";
|
||||
if (options.month) selectedMonth.value = options.month;
|
||||
await loadCorpName();
|
||||
await loadStaffNameMap();
|
||||
await loadRecords();
|
||||
});
|
||||
|
||||
@ -48,6 +48,14 @@ export default [
|
||||
path: 'pages/archive/edit-archive',
|
||||
meta: { title: '新增档案', login: true }
|
||||
},
|
||||
{
|
||||
path: 'pages/archive/step-edit-archive',
|
||||
meta: { title: '新增档案', login: true }
|
||||
},
|
||||
{
|
||||
path: 'pages/archive/fill-his-archive',
|
||||
meta: { title: '完善信息', login: true }
|
||||
},
|
||||
{
|
||||
path: 'pages/archive/archive-result',
|
||||
meta: { title: '团队服务' }
|
||||
|
||||
BIN
static/home/ai-consult-assistant.png
Normal file
BIN
static/home/ai-consult-assistant.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
@ -159,11 +159,12 @@ export default defineStore("accountStore", () => {
|
||||
const normalizedCorpId = normalizeCorpId(corpId);
|
||||
const unionid = account.value?.unionid;
|
||||
const openid = account.value?.openid;
|
||||
const miniAppId = account.value?.openid;
|
||||
if (!(normalizedCorpId && unionid && openid)) {
|
||||
externalUserId.value = '';
|
||||
return
|
||||
};
|
||||
const res = await api('getUnionidToExternalUserid', { unionid, openid, corpId: normalizedCorpId }, false);
|
||||
const res = await api('getUnionidToExternalUserid', { unionid, openid, corpId: normalizedCorpId, miniAppId }, false);
|
||||
const id = res && res.success && typeof res.data === 'string' && res.data.trim() ? res.data.trim() : '';
|
||||
externalUserId.value = id;
|
||||
return id;
|
||||
|
||||
4
utils/ai-consult-display.js
Normal file
4
utils/ai-consult-display.js
Normal file
@ -0,0 +1,4 @@
|
||||
export function removeAiLabel(value, fallback = '') {
|
||||
const text = typeof value === 'string' ? value.replace(/AI/g, '').trim() : '';
|
||||
return text || fallback;
|
||||
}
|
||||
13
utils/api.js
13
utils/api.js
@ -21,6 +21,15 @@ const urlsConfig = {
|
||||
recordBusinessCardBehavior: 'recordBusinessCardBehavior',
|
||||
getJoinedTeams: 'getJoinedTeams',
|
||||
getCorpBusinessCardStatus: 'getCorpBusinessCardStatus',
|
||||
getCorpInfo: 'getCorpInfo',
|
||||
getAiConsultPermission: 'getAiConsultPermission',
|
||||
getAiConsultAssistants: 'getAiConsultAssistants',
|
||||
getAiConsultSessions: 'getAiConsultSessions',
|
||||
getUnifiedConsultSessions: 'getUnifiedConsultSessions',
|
||||
getAiConsultSessionDetail: 'getAiConsultSessionDetail',
|
||||
openAiConsultSession: 'openAiConsultSession',
|
||||
sendAiConsultMessage: 'sendAiConsultMessage',
|
||||
closeAiConsultSession: 'closeAiConsultSession',
|
||||
},
|
||||
|
||||
knowledgeBase: {
|
||||
@ -62,6 +71,7 @@ const urlsConfig = {
|
||||
getExperienceCouponIssueDetail: "getExperienceCouponIssueDetail",
|
||||
getExperienceCouponIssueList: "getExperienceCouponIssueList",
|
||||
getMiniAppCustomers: 'getMiniAppCustomers',
|
||||
getCorpMiniAppCustomers: 'getCorpMiniAppCustomers',
|
||||
getTeamCustomers: 'getTeamCustomers',
|
||||
getTreatmentRecord: "getTreatmentRecord",
|
||||
getDeductRecord: "getDeductRecord",
|
||||
@ -80,6 +90,9 @@ const urlsConfig = {
|
||||
bindHisArchive: 'bindHisArchive',
|
||||
getMatchedHisArchive: 'getMatchedHisArchive',
|
||||
},
|
||||
customerHisSync: {
|
||||
getHisCustomerArchive: 'getHisCustomerArchive',
|
||||
},
|
||||
wecom: {
|
||||
addContactWay: 'getCorpFriendQrcode'
|
||||
},
|
||||
|
||||
@ -173,6 +173,7 @@ const request = async (options = {}, showLoading = true) => {
|
||||
return res.data;
|
||||
}
|
||||
return {
|
||||
...(res && res.data && typeof res.data === "object" ? res.data : {}),
|
||||
success: false,
|
||||
message: res.data && res.data.message ? res.data.message : "请求失败",
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user