fix:修复滑动问题

This commit is contained in:
Jafeng 2026-02-11 15:24:39 +08:00
parent dcd5071847
commit a6179e0624
11 changed files with 388 additions and 120 deletions

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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 {

View File

@ -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;
}

View File

@ -233,7 +233,6 @@ const doSearch = useDebounce(async () => {
const userId = getUserId();
const { corpId, teamId } = getTeamContext();
if (!corpId || !teamId || !userId) {
toast('缺少用户/团队信息,请先完成登录与个人信息');
return;
}

View File

@ -1090,7 +1090,6 @@ async function loadTeams(opts = {}) {
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
if (!corpId || !userId) {
toast('缺少用户信息,请先完善个人信息');
return;
}