Compare commits

...

5 Commits

3 changed files with 242 additions and 125 deletions

View File

@ -174,48 +174,6 @@ function normalizeDigits(value) {
return s.replace(/\D/g, '');
}
function isChinaMobile(value) {
const digits = normalizeDigits(value);
return /^1[3-9]\d{9}$/.test(digits);
}
function isLikelyArchiveNo(value) {
const s = normalizeText(value);
if (!s) return false;
if (/[\u4e00-\u9fa5]/.test(s)) return false; //
if (isChinaMobile(s)) return false;
// /
return /^[0-9A-Za-z-]{3,}$/.test(s);
}
function matchesAnyArchiveNo(patient, q) {
const needle = normalizeText(q).toLowerCase();
if (!needle) return false;
const values = [
patient?.customerNumber,
patient?.customerProfileNo2,
patient?.customerProfileNo3,
patient?.hospitalId,
].map((v) => normalizeText(v).toLowerCase()).filter(Boolean);
//
if (values.some((v) => v === needle)) return true;
// /
return values.some((v) => v.includes(needle));
}
function matchesAnyMobile(patient, q) {
const needle = normalizeDigits(q);
if (!needle) return false;
const mobiles = [
patient?.mobile,
...(Array.isArray(patient?.mobiles) ? patient.mobiles : []),
patient?.phone,
patient?.phone1,
].map(normalizeDigits).filter(Boolean);
return mobiles.some((m) => m === needle);
}
async function fetchList(params) {
const res = await api('searchCorpCustomerForCaseList', params);
if (!res?.success) {
@ -254,44 +212,11 @@ const doSearch = useDebounce(async () => {
const seq = (searchSeq.value += 1);
searching.value = true;
try {
if (isChinaMobile(q)) {
const digits = normalizeDigits(q);
const list = await fetchList({
corpId,
userId,
teamId,
page: 1,
pageSize: 50,
mobile: digits,
});
if (seq !== searchSeq.value) return;
//
searchResultsRaw.value = list.filter((p) => matchesAnyMobile(p, digits));
return;
}
if (isLikelyArchiveNo(q)) {
const list = await fetchList({
corpId,
userId,
teamId,
page: 1,
pageSize: 50,
archiveNo: q,
});
if (seq !== searchSeq.value) return;
// customerNumber/2/3
searchResultsRaw.value = list.filter((p) => matchesAnyArchiveNo(p, q));
return;
}
const list = await fetchList({
corpId,
userId,
teamId,
name: q,
keyword: q,
page: 1,
pageSize: 50,
});

View File

@ -1,13 +1,13 @@
<template>
<view class="page">
<!-- Mobile 来源: ykt-management-mobile/src/pages/customer/visit-record-detail/visit-record-detail.vue -->
<view class="body">
<view class="body">
<scroll-view scroll-y class="scroll">
<view class="form-wrap">
<FormTemplate v-if="temp" ref="formRef" :items="showItems" :form="forms" @change="onChange" />
</view>
<view style="height: 240rpx;"></view>
<view class="scroll-gap" />
</scroll-view>
<view class="footer">
@ -301,7 +301,6 @@ async function remove() {
.page {
min-height: 100vh;
background: #f5f6f8;
padding-bottom: calc(152rpx + env(safe-area-inset-bottom));
}
.body {
height: 100vh;
@ -310,6 +309,10 @@ async function remove() {
}
.scroll {
flex: 1;
min-height: 0;
}
.scroll-gap {
height: calc(240rpx + env(safe-area-inset-bottom));
}
.header {
background: #fff;

View File

@ -2,7 +2,19 @@
<view class="container">
<!-- Header -->
<view class="header">
<view class="team-selector" @click="toggleTeamPopup">
<picker
v-if="useTeamPicker"
mode="selector"
:range="teamNameRange"
:value="teamPickerIndex"
@change="onTeamPickerChange"
>
<view class="team-selector">
<text class="team-name">{{ teamDisplay }}</text>
<text class="team-icon"></text>
</view>
</picker>
<view v-else class="team-selector" @click="toggleTeamPopup">
<text class="team-name">{{ teamDisplay }}</text>
<text class="team-icon"></text>
</view>
@ -139,6 +151,10 @@ const currentTeam = ref(null);
const currentTabKey = ref('all');
const scrollIntoId = ref('');
const teamGroups = ref([]);
const useTeamPicker = computed(() => (Array.isArray(teams.value) ? teams.value.length : 0) > 6);
const teamNameRange = computed(() => (Array.isArray(teams.value) ? teams.value.map((t) => t?.name || '') : []));
const teamPickerIndex = ref(0);
const suppressTabReload = ref(false);
const tabs = computed(() => {
const base = [
{ key: 'all', label: '全部', kind: 'all' },
@ -184,12 +200,18 @@ const BATCH_CUSTOMER_IDS_KEY = 'ykt_case_batch_customer_ids';
const page = ref(1);
const pages = ref(0);
const pageSize = ref(50);
const pageSize = ref(200);
const totalFromApi = ref(0);
const loading = ref(false);
const rawPatients = ref([]);
const more = computed(() => page.value < pages.value);
const currentTab = computed(() => tabs.value.find((t) => t.key === currentTabKey.value) || tabs.value[0]);
const lastTeamsLoadOk = ref(true);
const lastGroupsLoadOk = ref(true);
const lastPatientsLoadOk = ref(true);
const loadedGroupsTeamId = ref('');
let enterRefreshInflight = null;
const hasEnteredOnce = ref(false);
const accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
@ -198,10 +220,92 @@ const { withInfo } = useInfoCheck();
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`);
watch(
[teams, currentTeam],
() => {
const list = Array.isArray(teams.value) ? teams.value : [];
const tid = currentTeam.value?.teamId ? String(currentTeam.value.teamId) : '';
const idx = tid ? list.findIndex((t) => String(t?.teamId || '') === tid) : -1;
teamPickerIndex.value = idx >= 0 ? idx : 0;
},
{ immediate: true }
);
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function getNetworkTypeSafe() {
try {
return await new Promise((resolve) => {
let done = false;
const timer = setTimeout(() => {
if (done) return;
done = true;
resolve('unknown');
}, 800);
uni.getNetworkType({
success: (res) => {
if (done) return;
done = true;
clearTimeout(timer);
resolve(String(res?.networkType || 'unknown'));
},
fail: () => {
if (done) return;
done = true;
clearTimeout(timer);
resolve('unknown');
},
});
});
} catch {
return 'unknown';
}
}
function pickPageSizeByNetwork(type) {
const t = String(type || '').toLowerCase();
if (t === '2g') return 100;
if (t === '3g') return 150;
if (t === '4g') return 250;
if (t === '5g' || t === 'wifi') return 300;
return 200;
}
async function apiWithRetry(urlId, data, showLoading = true, opts = {}) {
const retries = Number(opts?.retries ?? 1);
const baseDelay = Number(opts?.baseDelay ?? 350);
const shouldRetryMessage = (msg) =>
/timeout|timed out|超时|网络|network|fail|connect|ECONN|ENOTFOUND/i.test(String(msg || ''));
for (let i = 0; i <= retries; i += 1) {
try {
const res = await api(urlId, data, showLoading);
if (res?.success) return res;
const message = String(res?.message || '');
if (i < retries && shouldRetryMessage(message)) {
await sleep(Math.min(baseDelay * Math.pow(2, i), 1500));
continue;
}
return res;
} catch (e) {
if (i < retries) {
await sleep(Math.min(baseDelay * Math.pow(2, i), 1500));
continue;
}
throw e;
}
}
return { success: false, message: '请求失败' };
}
function normalizeUserId(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'object') {
@ -661,11 +765,24 @@ async function loadGroups() {
const teamId = getTeamId();
if (!corpId || !teamId) return;
const projection = { _id: 1, groupName: 1, parentGroupId: 1, sortOrder: 1 };
const res = await api('getGroups', { corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false });
let res;
try {
res = await apiWithRetry(
'getGroups',
{ corpId, teamId, page: 1, pageSize: 1000, projection, countGroupMember: false },
false,
{ retries: 1 }
);
} catch {
res = null;
}
if (!res?.success) {
teamGroups.value = [];
lastGroupsLoadOk.value = false;
if (!loadedGroupsTeamId.value || loadedGroupsTeamId.value !== teamId) teamGroups.value = [];
return;
}
lastGroupsLoadOk.value = true;
loadedGroupsTeamId.value = teamId;
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
teamGroups.value = sortGroupList(list);
@ -1012,7 +1129,7 @@ function groupByLetter(list) {
return letters.map((letter) => ({ letter, data: map.get(letter) || [] }));
}
async function loadTeams() {
async function loadTeams(opts = {}) {
if (!doctorInfo.value && account.value?.openid) {
try {
await getDoctorInfo();
@ -1029,15 +1146,22 @@ async function loadTeams() {
return;
}
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
let res;
try {
res = await apiWithRetry('getTeamBymember', { corpId, corpUserId: userId }, !opts?.silent, { retries: 2 });
} catch {
res = null;
}
if (!res?.success) {
toast(res?.message || '获取团队失败');
lastTeamsLoadOk.value = false;
if (!opts?.silent) toast(res?.message || '获取团队失败');
return;
}
const list = Array.isArray(res?.data) ? res.data : Array.isArray(res?.data?.data) ? res.data.data : [];
const normalized = list.map(normalizeTeam).filter(Boolean);
teams.value = normalized;
lastTeamsLoadOk.value = true;
const saved = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY);
const savedTeamId = saved?.teamId ? String(saved.teamId) : '';
@ -1045,9 +1169,9 @@ async function loadTeams() {
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
}
async function reload(reset = true) {
if (!currentTeam.value) return;
if (loading.value) return;
async function reload(reset = true, opts = {}) {
if (!currentTeam.value) return false;
if (loading.value) return false;
await ensureDoctorForQuery();
const userId = getUserId();
@ -1058,19 +1182,27 @@ async function reload(reset = true) {
return;
}
if (reset) {
const keepOnFail = Boolean(opts?.keepOnFail);
const targetPage = reset ? 1 : page.value;
if (reset && !keepOnFail) {
page.value = 1;
rawPatients.value = [];
pages.value = 0;
totalFromApi.value = 0;
}
if (reset) {
const net = await getNetworkTypeSafe();
pageSize.value = pickPageSizeByNetwork(net);
}
const query = {
corpId,
userId,
teamId,
page: page.value,
pageSize: 1000, //
page: targetPage,
pageSize: pageSize.value, //
sortByFirstLetter: true, //
};
@ -1086,14 +1218,24 @@ async function reload(reset = true) {
}
loading.value = true;
const res = await api('searchCorpCustomerForCaseList', query);
loading.value = false;
let res;
try {
res = await apiWithRetry('searchCorpCustomerForCaseList', query, !opts?.silent, { retries: 1 });
} catch {
res = null;
} finally {
loading.value = false;
}
if (!res?.success) {
toast(res?.message || '获取患者列表失败');
return;
lastPatientsLoadOk.value = false;
if (!opts?.silent || (rawPatients.value || []).length === 0) toast(res?.message || '获取患者列表失败');
return false;
}
if (reset) page.value = 1;
lastPatientsLoadOk.value = true;
const payload =
res && typeof res === 'object'
? res.data && typeof res.data === 'object' && !Array.isArray(res.data)
@ -1102,7 +1244,7 @@ async function reload(reset = true) {
: {};
const list = Array.isArray(payload.list) ? payload.list : Array.isArray(payload.data) ? payload.data : [];
const next = list.map(formatPatient);
rawPatients.value = page.value === 1 ? next : [...rawPatients.value, ...next];
rawPatients.value = targetPage === 1 ? next : [...rawPatients.value, ...next];
// /
void prefetchUserNamesFromPatients(next).catch(() => {});
pages.value = Number(payload.pages || 0) || 0;
@ -1116,6 +1258,8 @@ async function reload(reset = true) {
totalFromApi.value ||
0
) || (totalFromApi.value || 0);
return true;
}
const handlePatientClick = (patient) => {
@ -1191,8 +1335,40 @@ const scrollToLetter = (letter) => {
scrollIntoId.value = letterToDomId(letter);
};
async function applyTeamSelection(nextTeam) {
if (!nextTeam) return;
const nextId = String(nextTeam?.teamId || '');
if (nextId && nextId === getTeamId()) return;
suppressTabReload.value = true;
try {
currentTeam.value = nextTeam;
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
currentTabKey.value = 'all';
await loadGroups();
await loadTeamMembers();
await reload(true);
} finally {
suppressTabReload.value = false;
}
}
async function onTeamPickerChange(e) {
const prev = teamPickerIndex.value;
if (checkBatchMode()) {
teamPickerIndex.value = prev;
return;
}
const idx = Number(e?.detail?.value ?? prev);
if (!Number.isFinite(idx)) return;
teamPickerIndex.value = idx;
await applyTeamSelection((Array.isArray(teams.value) ? teams.value : [])[idx]);
}
const toggleTeamPopup = () => {
if (checkBatchMode()) return;
if (useTeamPicker.value) return;
if (!teams.value.length) {
toast('暂无可选团队');
return;
@ -1200,12 +1376,7 @@ const toggleTeamPopup = () => {
uni.showActionSheet({
itemList: teams.value.map((i) => i.name),
success: async (res) => {
currentTeam.value = teams.value[res.tapIndex] || teams.value[0] || null;
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
currentTabKey.value = 'all';
await loadGroups();
await loadTeamMembers();
await reload(true);
await applyTeamSelection(teams.value[res.tapIndex] || teams.value[0] || null);
}
});
};
@ -1432,7 +1603,6 @@ async function transferToCustomerPool(customerIds) {
}
toast('操作成功');
cancelBatch();
await reload(true);
} catch (e) {
toast('操作失败');
} finally {
@ -1495,7 +1665,6 @@ async function transferToOtherTeam(customerIds) {
}
toast('操作成功');
cancelBatch();
await reload(true);
} catch (e) {
toast('操作失败');
} finally {
@ -1536,10 +1705,12 @@ const handleShare = () => {
uni.navigateTo({ url: '/pages/case/batch-share' });
};
function loadMore() {
async function loadMore() {
if (!more.value || loading.value) return;
page.value += 1;
reload(false);
const nextPage = page.value + 1;
page.value = nextPage;
const ok = await reload(false, { silent: true });
if (!ok) page.value = nextPage - 1;
}
watch(currentTeam, (t) => {
@ -1549,6 +1720,7 @@ watch(currentTeam, (t) => {
watch(currentTabKey, () => {
if (!currentTeam.value) return;
if (suppressTabReload.value) return;
loadTeamMembers();
reload(true);
});
@ -1562,36 +1734,53 @@ function onTabClick(tab) {
currentTabKey.value = tab.key;
}
async function refreshOnEnter() {
if (enterRefreshInflight) return enterRefreshInflight;
enterRefreshInflight = (async () => {
const silent = hasEnteredOnce.value;
let didLoadSomething = false;
// /线
if (!currentTeam.value) {
const saved = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY);
if (saved && saved.teamId) currentTeam.value = saved;
}
await loadTeams({ silent });
if (lastTeamsLoadOk.value) didLoadSomething = true;
if (currentTeam.value) {
await loadGroups();
await loadTeamMembers();
const ok = await reload(true, { silent, keepOnFail: true });
if (ok) didLoadSomething = true;
}
await refreshVerifyStatus();
if (didLoadSomething) hasEnteredOnce.value = true;
})().finally(() => {
enterRefreshInflight = null;
});
return enterRefreshInflight;
}
onLoad(async () => {
await loadTeams();
if (currentTeam.value) {
await loadGroups();
await loadTeamMembers();
await reload(true);
}
await refreshVerifyStatus();
await refreshOnEnter();
});
onShow(async () => {
const need = uni.getStorageSync(NEED_RELOAD_STORAGE_KEY);
if (need) {
uni.removeStorageSync(NEED_RELOAD_STORAGE_KEY);
await reload(true);
// 退
isBatchMode.value = false;
selectedItems.value = [];
}
const needGroups = uni.getStorageSync(GROUPS_RELOAD_KEY);
if (needGroups) {
uni.removeStorageSync(GROUPS_RELOAD_KEY);
await loadGroups();
await reload(true);
} else {
await loadGroups();
}
await loadTeamMembers();
await refreshVerifyStatus();
if (needGroups) uni.removeStorageSync(GROUPS_RELOAD_KEY);
await refreshOnEnter();
});
</script>