Merge branch 'dev-wdb' of http://175.27.226.205:3000/huxuejian/ykt-wxapp into dev-wdb
This commit is contained in:
commit
609fc8cd18
@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="page-scroll"
|
||||
:scroll-top="pageScrollTop"
|
||||
:scroll-with-animation="true"
|
||||
:lower-threshold="80"
|
||||
@scrolltolower="onScrollToLower"
|
||||
>
|
||||
<view class="card">
|
||||
<view class="header">
|
||||
<view class="header-main">
|
||||
@ -83,6 +91,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view id="tabs-anchor" class="tabs-anchor"></view>
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
@ -119,6 +128,9 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view class="scroll-spacer" />
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showGoChat" class="footer">
|
||||
<button class="bind-btn" @click="goChat">
|
||||
<uni-icons type="chat-filled" size="18" color="#fff" />
|
||||
@ -213,13 +225,13 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, getCurrentInstance, nextTick, ref } from 'vue';
|
||||
import { onLoad, onPullDownRefresh, onReachBottom, onReady, onShow } from '@dcloudio/uni-app';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import HealthProfileTab from './components/archive-detail/health-profile-tab.vue';
|
||||
import ServiceInfoTab from './components/archive-detail/service-info-tab.vue';
|
||||
import FollowUpManageTab from './components/archive-detail/follow-up-manage-tab.vue';
|
||||
import HealthProfileTab from '@/pages/case/components/archive-detail/health-profile-tab.vue';
|
||||
import ServiceInfoTab from '@/pages/case/components/archive-detail/service-info-tab.vue';
|
||||
import FollowUpManageTab from '@/pages/case/components/archive-detail/follow-up-manage-tab.vue';
|
||||
import api from '@/utils/api';
|
||||
import useAccountStore from '@/store/account';
|
||||
import { hideLoading, loading, toast } from '@/utils/widget';
|
||||
@ -238,15 +250,32 @@ const currentTab = ref('visitRecord');
|
||||
const reachBottomTime = ref(0);
|
||||
const archiveId = ref('');
|
||||
const fromChat = ref(false);
|
||||
const tabsScrollTop = ref(0);
|
||||
const pageScrollTop = ref(0);
|
||||
const instanceProxy = getCurrentInstance()?.proxy;
|
||||
|
||||
function scrollTabsToTop() {
|
||||
const q = instanceProxy ? uni.createSelectorQuery().in(instanceProxy) : uni.createSelectorQuery();
|
||||
q.select('.page-scroll').boundingClientRect();
|
||||
q.select('#tabs-anchor').boundingClientRect();
|
||||
q.select('.page-scroll').scrollOffset();
|
||||
q.exec((res) => {
|
||||
const scrollRect = res && res[0];
|
||||
const anchorRect = res && res[1];
|
||||
const viewport = res && res[2];
|
||||
if (!scrollRect || !anchorRect || !viewport) return;
|
||||
|
||||
const delta = Number(anchorRect.top || 0) - Number(scrollRect.top || 0);
|
||||
const target = Number(viewport.scrollTop || 0) + delta;
|
||||
// scrollTop 值相同可能不会触发滚动,轻微抖动保证生效
|
||||
if (pageScrollTop.value === target) pageScrollTop.value = target + 1;
|
||||
pageScrollTop.value = target;
|
||||
});
|
||||
}
|
||||
|
||||
function switchTab(key) {
|
||||
currentTab.value = key;
|
||||
// 切换 tab 后,将 tab 滚动到页面顶部(隐藏头部信息区域)
|
||||
nextTick(() => {
|
||||
uni.pageScrollTo({ scrollTop: tabsScrollTop.value || 0, duration: 300 });
|
||||
});
|
||||
nextTick(() => scrollTabsToTop());
|
||||
}
|
||||
|
||||
const archive = ref({
|
||||
@ -368,8 +397,6 @@ async function fetchArchive() {
|
||||
}
|
||||
}
|
||||
saveToStorage();
|
||||
// 档案信息刷新后,tabs 的位置可能变化,重新测量
|
||||
nextTick(() => setTimeout(measureTabsTop, 30));
|
||||
} catch (e) {
|
||||
toast('获取档案失败');
|
||||
} finally {
|
||||
@ -477,7 +504,6 @@ async function updateArchive(patch) {
|
||||
const createTeamId = team?.teamId ? String(team.teamId) : '';
|
||||
const createTeamName = team?.name ? String(team.name) : '';
|
||||
if (!userId || !corpId) {
|
||||
toast('缺少登录信息,请先完成登录');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -536,22 +562,6 @@ onLoad((options) => {
|
||||
fetchArchive();
|
||||
});
|
||||
|
||||
function measureTabsTop() {
|
||||
const q = instanceProxy ? uni.createSelectorQuery().in(instanceProxy) : uni.createSelectorQuery();
|
||||
q.select('.tabs').boundingClientRect();
|
||||
q.selectViewport().scrollOffset();
|
||||
q.exec((res) => {
|
||||
const rect = res && res[0];
|
||||
const viewport = res && res[1];
|
||||
if (!rect || !viewport) return;
|
||||
tabsScrollTop.value = (rect.top || 0) + (viewport.scrollTop || 0);
|
||||
});
|
||||
}
|
||||
|
||||
onReady(() => {
|
||||
setTimeout(measureTabsTop, 30);
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||
const cachedId = cached && typeof cached === 'object' ? String(cached._id || cached.id || '') : '';
|
||||
@ -563,21 +573,12 @@ onShow(() => {
|
||||
};
|
||||
chatGroupId.value = normalizeGroupId(archive.value.chatGroupId || '');
|
||||
}
|
||||
setTimeout(measureTabsTop, 30);
|
||||
fetchArchive();
|
||||
});
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
uni.pageScrollTo({
|
||||
scrollTop: tabsScrollTop.value || 0,
|
||||
duration: 0,
|
||||
});
|
||||
setTimeout(() => uni.stopPullDownRefresh(), 150);
|
||||
});
|
||||
|
||||
onReachBottom(() => {
|
||||
function onScrollToLower() {
|
||||
reachBottomTime.value = Date.now();
|
||||
});
|
||||
}
|
||||
|
||||
const sexOrAge = computed(() => {
|
||||
const sex = archive.value.sex ? String(archive.value.sex) : '';
|
||||
@ -919,7 +920,6 @@ const saveAddGroup = async () => {
|
||||
const teamId = team?.teamId ? String(team.teamId) : '';
|
||||
const creator = getUserId();
|
||||
if (!corpId || !teamId || !creator) {
|
||||
toast('缺少用户/团队信息');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -947,13 +947,21 @@ const saveAddGroup = async () => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f6f8;
|
||||
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -1140,7 +1148,6 @@ const saveAddGroup = async () => {
|
||||
}
|
||||
|
||||
.tabs {
|
||||
margin-top: 20rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
border-top: 2rpx solid #f2f2f2;
|
||||
@ -1150,6 +1157,10 @@ const saveAddGroup = async () => {
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.tabs-anchor {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
@ -1179,6 +1190,12 @@ const saveAddGroup = async () => {
|
||||
|
||||
.content {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.scroll-spacer {
|
||||
height: calc(180rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@ -308,7 +308,6 @@ async function savePatch(patch) {
|
||||
const createTeamId = team?.teamId ? String(team.teamId) : '';
|
||||
const createTeamName = team?.name ? String(team.name) : '';
|
||||
if (!archiveId.value || !userId || !corpId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -115,7 +115,6 @@ async function transferToCustomerPool() {
|
||||
const currentTeamId = getCurrentTeamId();
|
||||
const creatorUserId = getUserId();
|
||||
if (!corpId || !currentTeamId || !creatorUserId) {
|
||||
toast('缺少用户/团队信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -147,7 +146,6 @@ async function transferToOtherTeam() {
|
||||
const currentTeamId = getCurrentTeamId();
|
||||
const creatorUserId = getUserId();
|
||||
if (!corpId || !currentTeamId || !creatorUserId) {
|
||||
toast('缺少用户/团队信息');
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -379,6 +379,214 @@ function resolveUserName(userId) {
|
||||
return String(map[id] || "") || id;
|
||||
}
|
||||
|
||||
function normalizeName(v) {
|
||||
const s = v === 0 ? "0" : v ? String(v) : "";
|
||||
const trimmed = s.trim();
|
||||
if (!trimmed) return "";
|
||||
if (["-", "—", "--"].includes(trimmed)) return "";
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function getExecuteTeamId(todo) {
|
||||
const row = todo && typeof todo === "object" ? todo : {};
|
||||
return String(
|
||||
row.executeTeamId ||
|
||||
row.executeTeamID ||
|
||||
row.teamId ||
|
||||
row.teamID ||
|
||||
row.executeTeam?._id ||
|
||||
row.executeTeam?.teamId ||
|
||||
""
|
||||
)
|
||||
.trim();
|
||||
}
|
||||
|
||||
const loadedTeamMemberIds = new Set();
|
||||
const teamMemberInflight = new Map();
|
||||
async function loadTeamMembers(teamId) {
|
||||
const tid = String(teamId || "").trim();
|
||||
if (!tid) return;
|
||||
if (loadedTeamMemberIds.has(tid)) return;
|
||||
const existingInflight = teamMemberInflight.get(tid);
|
||||
if (existingInflight) return existingInflight;
|
||||
|
||||
const inflight = (async () => {
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
if (!corpId) return;
|
||||
|
||||
try {
|
||||
const fallback = await api("getTeamData", { corpId, teamId: tid }, false);
|
||||
if (!fallback?.success) return;
|
||||
const t = fallback?.data && typeof fallback.data === "object" ? fallback.data : {};
|
||||
const members = Array.isArray(t.memberList) ? t.memberList : [];
|
||||
const map = members.reduce((acc, m) => {
|
||||
if (typeof m === "string") {
|
||||
const id = String(m || "");
|
||||
if (id) acc[id] = id;
|
||||
return acc;
|
||||
}
|
||||
const uid = String(m?.userid || m?.userId || m?.corpUserId || m?._id || m?.id || "").trim();
|
||||
if (!uid) return acc;
|
||||
const display = String(m?.anotherName || m?.name || m?.userid || m?.userId || "").trim();
|
||||
acc[uid] = display || uid;
|
||||
return acc;
|
||||
}, {});
|
||||
if (Object.keys(map).length) userNameMap.value = { ...(userNameMap.value || {}), ...map };
|
||||
|
||||
// 补缺:仅当当前没有映射时才用 avatars 接口补齐,避免覆盖正确姓名
|
||||
try {
|
||||
const res = await api("getTeamMemberAvatarsAndName", { corpId, teamId: tid }, false);
|
||||
if (res?.success && res?.data && typeof res.data === "object") {
|
||||
const raw = res.data;
|
||||
const patch = Object.keys(raw).reduce((acc, uid) => {
|
||||
const id = String(uid || "").trim();
|
||||
if (!id) return acc;
|
||||
const existing = userNameMap.value?.[id];
|
||||
if (existing && existing !== id) return acc;
|
||||
const name = String(raw?.[uid]?.name || raw?.[uid]?.anotherName || "").trim();
|
||||
if (!name || name === id) return acc;
|
||||
acc[id] = name;
|
||||
return acc;
|
||||
}, {});
|
||||
if (Object.keys(patch).length) userNameMap.value = { ...(userNameMap.value || {}), ...patch };
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
} finally {
|
||||
loadedTeamMemberIds.add(tid);
|
||||
}
|
||||
})().finally(() => {
|
||||
teamMemberInflight.delete(tid);
|
||||
});
|
||||
|
||||
teamMemberInflight.set(tid, inflight);
|
||||
return inflight;
|
||||
}
|
||||
|
||||
let corpMemberBatchInflight = null;
|
||||
async function batchLoadCorpMembers(userIds) {
|
||||
const ids = Array.isArray(userIds)
|
||||
? userIds.map((v) => String(v || "").trim()).filter(Boolean)
|
||||
: [];
|
||||
if (!ids.length) return;
|
||||
const uniq = Array.from(new Set(ids));
|
||||
const unknown = uniq.filter((id) => {
|
||||
const existing = userNameMap.value?.[id];
|
||||
return !existing || existing === id;
|
||||
});
|
||||
if (!unknown.length) return;
|
||||
|
||||
if (corpMemberBatchInflight) return corpMemberBatchInflight;
|
||||
|
||||
await ensureDoctor();
|
||||
const corpId = getCorpId();
|
||||
if (!corpId) return;
|
||||
|
||||
corpMemberBatchInflight = (async () => {
|
||||
try {
|
||||
const res = await api(
|
||||
"getCorpMember",
|
||||
{
|
||||
page: 1,
|
||||
pageSize: Math.min(Math.max(unknown.length, 10), 500),
|
||||
params: {
|
||||
corpId,
|
||||
memberList: unknown,
|
||||
},
|
||||
},
|
||||
false
|
||||
);
|
||||
if (!res?.success) return;
|
||||
|
||||
const rows = Array.isArray(res?.data)
|
||||
? res.data
|
||||
: Array.isArray(res?.data?.data)
|
||||
? res.data.data
|
||||
: [];
|
||||
if (!rows.length) return;
|
||||
|
||||
const patch = rows.reduce((acc, m) => {
|
||||
const id = String(m?.userid || m?.userId || m?.corpUserId || "").trim();
|
||||
if (!id) return acc;
|
||||
const existing = userNameMap.value?.[id];
|
||||
if (existing && existing !== id) return acc;
|
||||
const display = String(m?.anotherName || m?.name || "").trim();
|
||||
if (!display || display === id) return acc;
|
||||
acc[id] = display;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(patch).length) userNameMap.value = { ...(userNameMap.value || {}), ...patch };
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
})().finally(() => {
|
||||
corpMemberBatchInflight = null;
|
||||
});
|
||||
|
||||
return corpMemberBatchInflight;
|
||||
}
|
||||
|
||||
let ensureNamesInflight = null;
|
||||
async function ensureTodoNames(todos) {
|
||||
if (ensureNamesInflight) return ensureNamesInflight;
|
||||
const rows = Array.isArray(todos) ? todos : [];
|
||||
if (!rows.length) return;
|
||||
|
||||
const teamIds = Array.from(
|
||||
new Set(
|
||||
rows
|
||||
.map((t) => getExecuteTeamId(t) || getCurrentTeamId())
|
||||
.map((v) => String(v || "").trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
);
|
||||
const unknownUserIds = Array.from(
|
||||
new Set(
|
||||
rows
|
||||
.flatMap((t) => [t?.executorUserId, t?.creatorUserId])
|
||||
.map((v) => String(v || "").trim())
|
||||
.filter(Boolean)
|
||||
.filter((id) => {
|
||||
const existing = userNameMap.value?.[id];
|
||||
return !existing || existing === id;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
ensureNamesInflight = (async () => {
|
||||
// 先按团队拉成员(并发限制),再按 corp 兜底补齐
|
||||
const limit = 3;
|
||||
let idx = 0;
|
||||
const workers = Array.from({ length: Math.min(limit, teamIds.length || 1) }, async () => {
|
||||
while (idx < teamIds.length) {
|
||||
const tid = teamIds[idx++];
|
||||
try {
|
||||
await loadTeamMembers(tid);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
});
|
||||
await Promise.allSettled(workers);
|
||||
|
||||
await batchLoadCorpMembers(unknownUserIds);
|
||||
|
||||
// 重新补齐列表显示
|
||||
list.value = (Array.isArray(list.value) ? list.value : []).map((t) => ({
|
||||
...t,
|
||||
executorName: normalizeName(t?.executorName) || resolveUserName(t?.executorUserId),
|
||||
creatorName: normalizeName(t?.creatorName) || resolveUserName(t?.creatorUserId),
|
||||
}));
|
||||
})().finally(() => {
|
||||
ensureNamesInflight = null;
|
||||
});
|
||||
|
||||
return ensureNamesInflight;
|
||||
}
|
||||
|
||||
function refreshChatRoom() {
|
||||
// 兼容:优先用档案详情数据里已有的 chatGroupId
|
||||
const fromArchive =
|
||||
@ -460,7 +668,6 @@ async function getMore() {
|
||||
const corpId = getCorpId();
|
||||
const userId = getUserId();
|
||||
if (!corpId) {
|
||||
toast("缺少 corpId,请先完成登录/团队选择");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -498,6 +705,9 @@ async function getMore() {
|
||||
pages.value = Math.ceil(total.value / pageSize) || 0;
|
||||
list.value = page.value === 1 ? next : [...list.value, ...next];
|
||||
page.value += 1;
|
||||
|
||||
// 不阻塞渲染:后台补齐 executor/creator 的姓名
|
||||
void ensureTodoNames(list.value);
|
||||
} catch (e) {
|
||||
console.error("getCustomerTodos failed:", e);
|
||||
toast("获取回访任务失败");
|
||||
@ -506,6 +716,7 @@ async function getMore() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toggleMy() {
|
||||
query.isMy = !query.isMy;
|
||||
resetList();
|
||||
@ -1248,6 +1459,7 @@ watch(
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
|
||||
@ -592,6 +592,7 @@ watch(
|
||||
background: #f5f6f8;
|
||||
border-bottom: 2rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.filter-pill {
|
||||
background: #fff;
|
||||
border: 2rpx solid #e6e6e6;
|
||||
@ -638,6 +639,7 @@ watch(
|
||||
.list {
|
||||
padding: 0 28rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
|
||||
@ -725,6 +725,7 @@ watch(
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: block;
|
||||
flex: 1;
|
||||
@ -872,6 +873,7 @@ watch(
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
|
||||
.fab {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<view class="followup-task-page">
|
||||
<scroll-view scroll-y class="content" :lower-threshold="80" @scrolltolower="onScrollToLower">
|
||||
<!-- 回访任务列表组件 -->
|
||||
<FollowUpManageTab
|
||||
:data="patientData"
|
||||
@ -8,6 +9,8 @@
|
||||
:floatingBottom="0"
|
||||
:fromChat="true"
|
||||
/>
|
||||
<view class="scroll-spacer" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@ -30,6 +33,10 @@ onLoad((options) => {
|
||||
const handleBack = () => {
|
||||
uni.navigateBack();
|
||||
};
|
||||
|
||||
function onScrollToLower() {
|
||||
reachBottomTime.value = Date.now();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -40,6 +47,15 @@ const handleBack = () => {
|
||||
background: #f5f6f8;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.scroll-spacer {
|
||||
height: calc(120px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="body">
|
||||
<scroll-view scroll-y class="scroll">
|
||||
<scroll-view scroll-y class="scroll" :style="{ height: scrollHeight + 'px' }">
|
||||
<view class="form-wrap">
|
||||
<form-template ref="formRef" :items="baseItems" :form="form" :rule="rules" @change="onChange" />
|
||||
</view>
|
||||
@ -43,6 +43,7 @@ const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
|
||||
const formRef = ref(null);
|
||||
const form = reactive({});
|
||||
const baseItems = ref([]);
|
||||
const scrollHeight = ref(0);
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const { account, doctorInfo } = storeToRefs(accountStore);
|
||||
@ -191,6 +192,9 @@ const rules = {
|
||||
};
|
||||
|
||||
onLoad(async () => {
|
||||
// scroll-view 在小程序端需要明确高度,否则在某些机型/样式组合下会出现无法滚动
|
||||
scrollHeight.value = Number(uni.getSystemInfoSync()?.windowHeight || 0) || 0;
|
||||
|
||||
const cached = uni.getStorageSync(STORAGE_KEY);
|
||||
if (cached && typeof cached === 'object') {
|
||||
Object.assign(form, cached);
|
||||
@ -260,18 +264,19 @@ function getAgeFromBirthday(birthday) {
|
||||
background: #f6f6f6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
|
||||
@ -396,6 +396,26 @@ function buildPayload(base, inner) {
|
||||
payload.customerSource = [payload.customerSource];
|
||||
}
|
||||
|
||||
const creatorId = String(payload.creator || '').trim();
|
||||
const teamIdStr = String(payload.teamId || '').trim();
|
||||
const rawPersonResponsibles = payload.personResponsibles;
|
||||
const personResponsiblesList = Array.isArray(rawPersonResponsibles)
|
||||
? rawPersonResponsibles
|
||||
: rawPersonResponsibles && typeof rawPersonResponsibles === 'object'
|
||||
? [rawPersonResponsibles]
|
||||
: [];
|
||||
const normalizedPersonResponsibles = personResponsiblesList
|
||||
.map((i) => ({
|
||||
corpUserId: String(i?.corpUserId || i?.userId || creatorId || '').trim(),
|
||||
teamId: String(i?.teamId || teamIdStr || '').trim(),
|
||||
}))
|
||||
.filter((i) => i.corpUserId && i.teamId);
|
||||
if (normalizedPersonResponsibles.length) {
|
||||
payload.personResponsibles = normalizedPersonResponsibles;
|
||||
} else if (creatorId && teamIdStr) {
|
||||
payload.personResponsibles = [{ corpUserId: creatorId, teamId: teamIdStr }];
|
||||
}
|
||||
|
||||
return { payload, team };
|
||||
}
|
||||
|
||||
@ -421,7 +441,6 @@ async function save() {
|
||||
|
||||
const { payload, team } = buildPayload(base, form);
|
||||
if (!payload.corpId || !payload.teamId || !payload.creator) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -233,7 +233,6 @@ const doSearch = useDebounce(async () => {
|
||||
const userId = getUserId();
|
||||
const { corpId, teamId } = getTeamContext();
|
||||
if (!corpId || !teamId || !userId) {
|
||||
toast('缺少用户/团队信息,请先完成登录与个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1090,7 +1090,6 @@ async function loadTeams(opts = {}) {
|
||||
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
|
||||
|
||||
if (!corpId || !userId) {
|
||||
toast('缺少用户信息,请先完善个人信息');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1356,35 +1355,70 @@ const toggleBatchMode = withInfo(() => {
|
||||
selectedItems.value = [];
|
||||
});
|
||||
|
||||
const handleCreate = withInfo(() => {
|
||||
const handleCreate = withInfo(async () => {
|
||||
if (checkBatchMode()) return;
|
||||
const rawMax = doctorInfo.value?.maxCustomerArchive;
|
||||
const hasMaxField = rawMax !== undefined && rawMax !== null && String(rawMax).trim() !== '';
|
||||
const maxCustomerArchive = hasMaxField ? Number(rawMax) : NaN;
|
||||
|
||||
// maxCustomerArchive:
|
||||
// -1 = 无限;存在该字段则优先按该字段限制;不存在则沿用原有规则(未认证10/已认证100)
|
||||
if (hasMaxField && Number.isFinite(maxCustomerArchive)) {
|
||||
if (maxCustomerArchive !== -1 && managedArchiveCountAllTeams.value >= maxCustomerArchive) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: `当前管理档案数已达上限 ${maxCustomerArchive} 个,无法继续新增。如需提升档案管理数,请联系客服处理。`,
|
||||
cancelText: '知道了',
|
||||
confirmText: '添加客服',
|
||||
// 规则沿用原前端逻辑:
|
||||
// - 认证中按未认证控制(上限10,且直接toast阻止)
|
||||
// - 未认证:达到10提示去认证
|
||||
// - 已认证:达到100提示联系客服
|
||||
// 只是把“在管档案数”来源改为新接口(医生创建的所有团队汇总)。
|
||||
let total = null;
|
||||
let limit = null;
|
||||
let unlimited = false;
|
||||
try {
|
||||
const corpId = getCorpId();
|
||||
const userId = getUserId();
|
||||
if (corpId && userId) {
|
||||
const res = await api('doctorCreatedTeamsCustomerLimitation', { corpId, userId }, false);
|
||||
if (res?.success) {
|
||||
const count = Number(res?.count ?? res?.data?.count);
|
||||
if (Number.isFinite(count)) total = count;
|
||||
|
||||
const rawLimit = Number(res?.limit ?? res?.data?.limit);
|
||||
if (Number.isFinite(rawLimit)) {
|
||||
limit = rawLimit;
|
||||
unlimited = rawLimit === -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (!Number.isFinite(total)) {
|
||||
total = Number(managedArchiveCountAllTeams.value || 0) || 0;
|
||||
}
|
||||
|
||||
// 接口异常兜底:仍按旧前端默认规则(未认证10/已认证100)
|
||||
if (!Number.isFinite(limit)) {
|
||||
limit = isVerified.value ? 100 : 10;
|
||||
unlimited = false;
|
||||
}
|
||||
|
||||
// 无限:直接放行
|
||||
if (unlimited) {
|
||||
uni.showActionSheet({
|
||||
itemList: ['邀请患者建档', '我帮患者建档'],
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
openAddCustomerServiceEntry();
|
||||
if (res.tapIndex === 0) {
|
||||
openInvitePatientEntry();
|
||||
} else if (res.tapIndex === 1) {
|
||||
openCreatePatientEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 100上限:无法继续新增 -> 引导联系客服(预留入口)
|
||||
if (managedArchiveCountAllTeams.value >= 100) {
|
||||
|
||||
const limitText = Number.isFinite(limit) ? String(limit) : '';
|
||||
|
||||
// 已认证:达到上限 -> 引导联系客服(预留入口)
|
||||
if (isVerified.value && total >= limit) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
|
||||
content: limitText
|
||||
? `当前管理档案数已达上限 ${limitText} 个,无法继续新增。如需提升档案管理数,请联系客服处理。`
|
||||
: '当前管理档案数已达上限,无法继续新增。如需提升档案管理数,请联系客服处理。',
|
||||
cancelText: '知道了',
|
||||
confirmText: '添加客服',
|
||||
success: (res) => {
|
||||
@ -1396,15 +1430,17 @@ const handleCreate = withInfo(() => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 未认证 + 达到10上限:提示去认证
|
||||
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
|
||||
// 未认证/认证中:达到上限 -> 提示去认证(认证中直接toast阻止)
|
||||
if (!isVerified.value && total >= limit) {
|
||||
if (verifyStatus.value === 'verifying') {
|
||||
toast('信息认证中,请耐心等待!');
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
|
||||
content: limitText
|
||||
? `当前管理档案数已达上限 ${limitText} 个,完成认证可提升档案管理上限。`
|
||||
: '当前管理档案数已达上限,完成认证可提升档案管理上限。',
|
||||
cancelText: '暂不认证',
|
||||
confirmText: '去认证',
|
||||
success: (res) => {
|
||||
@ -1415,7 +1451,6 @@ const handleCreate = withInfo(() => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 未达上限:显示新增入口
|
||||
uni.showActionSheet({
|
||||
|
||||
@ -6,13 +6,16 @@ $text-color-sub: #999;
|
||||
$primary-color: #0877F1;
|
||||
|
||||
.chat-page {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
transition: padding-bottom 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
/* 患者信息栏样式 - 固定在顶部 */
|
||||
@ -24,10 +27,8 @@ $primary-color: #0877F1;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
padding: 20rpx 32rpx;
|
||||
z-index: 100;
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.patient-info-content {
|
||||
@ -91,15 +92,15 @@ $primary-color: #0877F1;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
flex: 1;
|
||||
position: fixed;
|
||||
top: 100rpx; /* 患者信息栏高度,根据实际调整 */
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 120rpx; /* 输入框高度,根据实际调整 */
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
margin-top: 120rpx;
|
||||
margin-bottom: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transition: bottom 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
}
|
||||
|
||||
.chat-content-compressed {
|
||||
@ -364,18 +365,24 @@ $primary-color: #0877F1;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #e0e0e0;
|
||||
position: relative;
|
||||
z-index: 200;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
flex-shrink: 0;
|
||||
transform: translateZ(0); /* 开启硬件加速,提升性能 */
|
||||
transition: bottom 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||||
will-change: bottom;
|
||||
}
|
||||
|
||||
.input-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
padding: 12rpx 20rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
@ -499,7 +506,8 @@ $primary-color: #0877F1;
|
||||
justify-content: flex-start;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
padding: 20rpx 0 40rpx 60rpx;
|
||||
padding: 20rpx 0 20rpx 60rpx;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
gap: 40rpx 50rpx;
|
||||
flex-wrap: wrap;
|
||||
background-color: #f5f5f5;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="input-section">
|
||||
<view class="input-section" :style="{ bottom: props.keyboardHeight + 'px' }">
|
||||
<view class="input-toolbar">
|
||||
<view @click="toggleVoiceInput" class="voice-toggle-btn">
|
||||
<image v-if="showVoiceInput" src="/static/jianpan.png" class="voice-toggle-icon" mode="aspectFit"></image>
|
||||
@ -8,7 +8,8 @@
|
||||
<view class="input-area">
|
||||
<textarea v-if="!showVoiceInput" class="text-input" v-model="inputText" placeholder="我来说两句..."
|
||||
@confirm="sendTextMessage" @focus="handleInputFocus" @input="handleInput"
|
||||
:auto-height="true" :show-confirm-bar="false" :adjust-position="true" :cursor-spacing="30"
|
||||
:auto-height="true" :show-confirm-bar="false" :adjust-position="false" :hold-keyboard="true"
|
||||
ref="textareaRef"
|
||||
/>
|
||||
<input v-else class="voice-input-btn" :class="{ recording: isRecording }" @touchstart="startRecord"
|
||||
@touchmove="onRecordTouchMove" @touchend="stopRecord" @touchcancel="cancelRecord" :placeholder="isRecording ? '松开发送' : '按住说话'" disabled>
|
||||
@ -84,6 +85,7 @@ const props = defineProps({
|
||||
corpId: { type: String, default: "" },
|
||||
orderStatus: { type: String, default: "" },
|
||||
isGenerating: { type: Boolean, default: false },
|
||||
keyboardHeight: { type: Number, default: 0 },
|
||||
});
|
||||
|
||||
// Emits
|
||||
@ -100,6 +102,7 @@ const showVoiceInput = ref(false);
|
||||
const showMorePanel = ref(false);
|
||||
const isRecording = ref(false);
|
||||
const recordingText = ref("录音中...");
|
||||
const textareaRef = ref(null);
|
||||
const cloudCustomData = computed(() => {
|
||||
const arr = [
|
||||
props.chatRoomBusiness.businessType,
|
||||
@ -167,8 +170,19 @@ const initRecorderManager = () => {
|
||||
const sendTextMessage = async () => {
|
||||
if (!inputText.value.trim()) return;
|
||||
|
||||
await sendMessage("text", inputText.value);
|
||||
const textToSend = inputText.value;
|
||||
inputText.value = "";
|
||||
|
||||
await sendMessage("text", textToSend);
|
||||
|
||||
// 发送后保持焦点,不收起键盘
|
||||
nextTick(() => {
|
||||
// 通过设置 focus 属性来保持键盘显示
|
||||
// 注意:在某些情况下可能需要延迟执行
|
||||
setTimeout(() => {
|
||||
// 这里不需要手动聚焦,因为 hold-keyboard 会保持键盘
|
||||
}, 50);
|
||||
});
|
||||
};
|
||||
|
||||
// 从常用语发送文本消息
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<view class="chat-page">
|
||||
<page-meta :page-style="'overflow:' + (keyboardHeight > 0 ? 'hidden' : 'visible')"></page-meta>
|
||||
<view class="chat-page" :style="{ paddingBottom: keyboardHeight + 'px' }">
|
||||
<!-- 患者信息栏 -->
|
||||
<view class="patient-info-bar" v-if="patientInfo.name">
|
||||
<view class="patient-info-content">
|
||||
@ -18,6 +19,7 @@
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view
|
||||
class="chat-content"
|
||||
:style="{ bottom: (keyboardHeight > 0 ? keyboardHeight + 60 : 60) + 'px' }"
|
||||
scroll-y="true"
|
||||
enhanced="true"
|
||||
bounces="false"
|
||||
@ -168,6 +170,7 @@
|
||||
:patientInfo="patientInfo"
|
||||
:orderStatus="orderStatus"
|
||||
:isGenerating="isGenerating"
|
||||
:keyboardHeight="keyboardHeight"
|
||||
@scrollToBottom="() => scrollToBottom(true)"
|
||||
@messageSent="() => scrollToBottom(true)"
|
||||
@endConsult="handleEndConsult"
|
||||
@ -302,6 +305,10 @@ const chatInfo = ref({
|
||||
userID: "",
|
||||
avatar: "/static/home/avatar.svg",
|
||||
});
|
||||
|
||||
// 键盘高度
|
||||
const keyboardHeight = ref(0);
|
||||
|
||||
// 评价弹窗状态
|
||||
const isEvaluationPopupOpen = ref(false);
|
||||
|
||||
@ -458,6 +465,22 @@ onLoad((options) => {
|
||||
chatInfo.value.userID = decodeQueryValue(options.userID);
|
||||
}
|
||||
|
||||
// 监听键盘高度变化
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
console.log("键盘高度变化:", res.height);
|
||||
const oldHeight = keyboardHeight.value;
|
||||
keyboardHeight.value = res.height;
|
||||
|
||||
// 键盘弹出时(从0变为非0),自动滚动到底部
|
||||
if (oldHeight === 0 && res.height > 0) {
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
scrollToBottom(true);
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
checkLoginAndInitTIM();
|
||||
updateNavigationTitle();
|
||||
});
|
||||
@ -888,16 +911,6 @@ onShow(() => {
|
||||
|
||||
// 监听回访任务发送事件
|
||||
uni.$on("send-followup-message", handleSendFollowUpMessage);
|
||||
|
||||
// 监听键盘高度变化,自动滚动到底部
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
if (res.height > 0) {
|
||||
// 键盘弹出,延迟滚动到底部
|
||||
setTimeout(() => {
|
||||
scrollToBottom(true);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 处理发送回访任务消息
|
||||
@ -1146,6 +1159,9 @@ const handleOpenConsult = async () => {
|
||||
onUnmounted(() => {
|
||||
clearMessageCache();
|
||||
|
||||
// 移除键盘监听
|
||||
uni.offKeyboardHeightChange();
|
||||
|
||||
timChatManager.setCallback("onSDKReady", null);
|
||||
timChatManager.setCallback("onSDKNotReady", null);
|
||||
timChatManager.setCallback("onMessageReceived", null);
|
||||
|
||||
@ -69,6 +69,7 @@ const urlsConfig = {
|
||||
addCustomer: 'add',
|
||||
updateCustomer: 'update',
|
||||
transferCustomers: 'transferCustomers',
|
||||
doctorCreatedTeamsCustomerLimitation: 'doctorCreatedTeamsCustomerLimitation',
|
||||
getGroups: 'getGroups',
|
||||
createGroup: 'createGroup',
|
||||
updateGroup: 'updateGroup',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user