Merge commit '7c31d5350d4a76d0e0f0ce161cc170617d66368d' into dev-wdb
This commit is contained in:
commit
82d69c5a72
@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="card">
|
||||
<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">
|
||||
<view class="name-row">
|
||||
@ -81,9 +89,10 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view id="tabs-anchor" class="tabs-anchor"></view>
|
||||
<view class="tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
:key="t.key"
|
||||
@ -93,9 +102,9 @@
|
||||
>
|
||||
{{ t.title }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<view class="content">
|
||||
<HealthProfileTab
|
||||
v-if="currentTab === 'visitRecord'"
|
||||
:data="archive"
|
||||
@ -117,7 +126,10 @@
|
||||
:floatingBottom="floatingBottom"
|
||||
:fromChat="fromChat"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="scroll-spacer" />
|
||||
</scroll-view>
|
||||
|
||||
<view v-if="showGoChat" class="footer">
|
||||
<button class="bind-btn" @click="goChat">
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -43,70 +43,70 @@
|
||||
|
||||
<view class="list">
|
||||
<view v-for="i in list" :key="i._id" class="card" @click="toDetail(i)">
|
||||
<view class="head">
|
||||
<view class="date"
|
||||
>计划日期: <text class="date-val">{{ i.planDate }}</text></view
|
||||
>
|
||||
<view class="executor truncate"
|
||||
>{{ i.executorName
|
||||
}}<text v-if="i.executeTeamName"
|
||||
>({{ i.executeTeamName }})</text
|
||||
></view
|
||||
>
|
||||
</view>
|
||||
<view class="body">
|
||||
<view class="title-row">
|
||||
<view class="type">{{ i.eventTypeLabel }}</view>
|
||||
<view class="status" :class="`st-${i.status}`">{{
|
||||
i.eventStatusLabel
|
||||
}}</view>
|
||||
<view class="head">
|
||||
<view class="date"
|
||||
>计划日期: <text class="date-val">{{ i.planDate }}</text></view
|
||||
>
|
||||
<view class="executor truncate"
|
||||
>{{ i.executorName
|
||||
}}<text v-if="i.executeTeamName"
|
||||
>({{ i.executeTeamName }})</text
|
||||
></view
|
||||
>
|
||||
</view>
|
||||
<view class="content">{{ i.taskContent || "暂无内容" }}</view>
|
||||
<view
|
||||
v-if="i.sendContent || (i.fileList && i.fileList.length > 0)"
|
||||
class="send-content-wrapper"
|
||||
>
|
||||
<view class="send-content-section">
|
||||
<view class="send-content-label">发送内容:</view>
|
||||
<view class="send-content-body">
|
||||
<view v-if="i.sendContent" class="send-text">{{
|
||||
i.sendContent
|
||||
}}</view>
|
||||
<view
|
||||
v-if="i.fileList && i.fileList.length > 0"
|
||||
class="file-list"
|
||||
:class="{ 'no-send-text': !i.sendContent }"
|
||||
>
|
||||
<view class="body">
|
||||
<view class="title-row">
|
||||
<view class="type">{{ i.eventTypeLabel }}</view>
|
||||
<view class="status" :class="`st-${i.status}`">{{
|
||||
i.eventStatusLabel
|
||||
}}</view>
|
||||
</view>
|
||||
<view class="content">{{ i.taskContent || "暂无内容" }}</view>
|
||||
<view
|
||||
v-if="i.sendContent || (i.fileList && i.fileList.length > 0)"
|
||||
class="send-content-wrapper"
|
||||
>
|
||||
<view class="send-content-section">
|
||||
<view class="send-content-label">发送内容:</view>
|
||||
<view class="send-content-body">
|
||||
<view v-if="i.sendContent" class="send-text">{{
|
||||
i.sendContent
|
||||
}}</view>
|
||||
<view
|
||||
v-for="(file, idx) in i.fileList"
|
||||
:key="idx"
|
||||
class="file-item"
|
||||
:class="`file-type-${file.type}`"
|
||||
v-if="i.fileList && i.fileList.length > 0"
|
||||
class="file-list"
|
||||
:class="{ 'no-send-text': !i.sendContent }"
|
||||
>
|
||||
<view class="file-name">{{
|
||||
file.file?.name || file.name
|
||||
}}</view>
|
||||
<view
|
||||
v-for="(file, idx) in i.fileList"
|
||||
:key="idx"
|
||||
class="file-item"
|
||||
:class="`file-type-${file.type}`"
|
||||
>
|
||||
<view class="file-name">{{
|
||||
file.file?.name || file.name
|
||||
}}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<button
|
||||
v-if="canShowSendButton(i)"
|
||||
class="action-btn send-btn"
|
||||
@click.stop="goChatAndSend(i)"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</view>
|
||||
<button
|
||||
v-if="canShowSendButton(i)"
|
||||
class="action-btn send-btn"
|
||||
@click.stop="goChatAndSend(i)"
|
||||
<view v-if="i.status === 'treated'" class="result"
|
||||
>【处理结果】 {{ i.result || "" }}</view
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
<view class="footer-row">
|
||||
<view class="footer"
|
||||
>创建: {{ i.createTimeStr }} {{ i.creatorName }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="i.status === 'treated'" class="result"
|
||||
>【处理结果】 {{ i.result || "" }}</view
|
||||
>
|
||||
<view class="footer-row">
|
||||
<view class="footer"
|
||||
>创建: {{ i.createTimeStr }} {{ i.creatorName }}</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0" class="empty">暂无数据</view>
|
||||
@ -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,13 +1,16 @@
|
||||
<template>
|
||||
<view class="followup-task-page">
|
||||
<!-- 回访任务列表组件 -->
|
||||
<FollowUpManageTab
|
||||
:data="patientData"
|
||||
:archiveId="archiveId"
|
||||
:reachBottomTime="reachBottomTime"
|
||||
:floatingBottom="0"
|
||||
:fromChat="true"
|
||||
/>
|
||||
<scroll-view scroll-y class="content" :lower-threshold="80" @scrolltolower="onScrollToLower">
|
||||
<!-- 回访任务列表组件 -->
|
||||
<FollowUpManageTab
|
||||
:data="patientData"
|
||||
:archiveId="archiveId"
|
||||
:reachBottomTime="reachBottomTime"
|
||||
: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,65 +1355,101 @@ 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: '添加客服',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
openAddCustomerServiceEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 100上限:无法继续新增 -> 引导联系客服(预留入口)
|
||||
if (managedArchiveCountAllTeams.value >= 100) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '当前管理档案数已达 100 个,无法继续新增。如需提升档案管理数,请联系客服处理。',
|
||||
cancelText: '知道了',
|
||||
confirmText: '添加客服',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
openAddCustomerServiceEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 规则沿用原前端逻辑:
|
||||
// - 认证中按未认证控制(上限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;
|
||||
|
||||
// 未认证 + 达到10上限:提示去认证
|
||||
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
|
||||
if (verifyStatus.value === 'verifying') {
|
||||
toast('信息认证中,请耐心等待!');
|
||||
return;
|
||||
const rawLimit = Number(res?.limit ?? res?.data?.limit);
|
||||
if (Number.isFinite(rawLimit)) {
|
||||
limit = rawLimit;
|
||||
unlimited = rawLimit === -1;
|
||||
}
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
|
||||
cancelText: '暂不认证',
|
||||
confirmText: '去认证',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
startVerifyFlow();
|
||||
}
|
||||
}
|
||||
} 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.tapIndex === 0) {
|
||||
openInvitePatientEntry();
|
||||
} else if (res.tapIndex === 1) {
|
||||
openCreatePatientEntry();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const limitText = Number.isFinite(limit) ? String(limit) : '';
|
||||
|
||||
// 已认证:达到上限 -> 引导联系客服(预留入口)
|
||||
if (isVerified.value && total >= limit) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: limitText
|
||||
? `当前管理档案数已达上限 ${limitText} 个,无法继续新增。如需提升档案管理数,请联系客服处理。`
|
||||
: '当前管理档案数已达上限,无法继续新增。如需提升档案管理数,请联系客服处理。',
|
||||
cancelText: '知道了',
|
||||
confirmText: '添加客服',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
openAddCustomerServiceEntry();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 未认证/认证中:达到上限 -> 提示去认证(认证中直接toast阻止)
|
||||
if (!isVerified.value && total >= limit) {
|
||||
if (verifyStatus.value === 'verifying') {
|
||||
toast('信息认证中,请耐心等待!');
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: limitText
|
||||
? `当前管理档案数已达上限 ${limitText} 个,完成认证可提升档案管理上限。`
|
||||
: '当前管理档案数已达上限,完成认证可提升档案管理上限。',
|
||||
cancelText: '暂不认证',
|
||||
confirmText: '去认证',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
startVerifyFlow();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 未达上限:显示新增入口
|
||||
|
||||
@ -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