ykt-wxapp/pages/home/case-home.vue

1901 lines
57 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="container">
<!-- Header -->
<view class="header">
<view class="team-selector" @click="toggleTeamPopup">
<text class="team-name">{{ teamDisplay }}</text>
<text class="team-icon"></text>
</view>
<view class="header-actions">
<view class="action-item" @click="goToSearch">
<uni-icons type="search" size="22" color="#333"></uni-icons>
<text class="action-text">搜索</text>
</view>
<view class="action-item" @click="toggleBatchMode">
<uni-icons type="checkbox" size="22" color="#333"></uni-icons>
<text class="action-text">批量</text>
</view>
<view class="action-item" @click="goToGroupManage">
<uni-icons type="staff" size="22" color="#333"></uni-icons>
<text class="action-text">分组</text>
</view>
<view class="action-item" @click="handleCreate">
<uni-icons type="plusempty" size="22" color="#333"></uni-icons>
<text class="action-text">新增</text>
</view>
</view>
</view>
<!-- Tabs and Count -->
<view class="tabs-area">
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
<view class="tabs-container">
<view v-for="tab in tabs" :key="tab.key" class="tab-item" :class="{ active: currentTabKey === tab.key }"
@click="onTabClick(tab)">
{{ tab.label }}
</view>
</view>
</scroll-view>
<view class="total-count-inline">{{ totalPatients }}</view>
</view>
<!-- Main Content -->
<view class="content-body">
<!-- Patient List -->
<scroll-view scroll-y class="patient-list" :scroll-into-view="scrollIntoId" :scroll-with-animation="true"
lower-threshold="80" @scrolltolower="loadMore">
<view v-for="group in patientList" :key="group.letter" :id="letterToDomId(group.letter)">
<view class="group-title">{{ group.letter }}</view>
<view v-for="(patient, pIndex) in group.data" :key="pIndex" class="patient-card"
@click="handlePatientClick(patient)">
<!-- Checkbox for Batch Mode -->
<view v-if="isBatchMode" class="checkbox-area">
<uni-icons :type="selectedItems.includes(getSelectId(patient)) ? 'checkbox-filled' : 'checkbox'" size="24"
:color="selectedItems.includes(getSelectId(patient)) ? '#007aff' : '#ccc'"></uni-icons>
</view>
<view class="card-content">
<!-- Row 1 -->
<view class="card-row-top">
<view class="patient-info">
<text class="patient-name">{{ patient.name }}</text>
<text class="patient-meta">{{ patient.gender }}{{ patient.age ? '/' + patient.age + '岁' : '' }}</text>
</view>
<view class="patient-tags">
<view v-for="(tag, tIndex) in resolveGroupTags(patient)" :key="tIndex" class="tag">
{{ tag }}
</view>
</view>
</view>
<!-- Row 2 -->
<view v-if="currentTabKey === 'new'" class="card-row-middle">
<text v-if="patient.record" class="record-text record-ellipsis">
{{ patient.record.type }} / {{ patient.record.date }} / {{ patient.record.diagnosis }}
</text>
<text v-else class="no-record">暂无病历记录</text>
</view>
<!-- Row 3 -->
<view class="card-row-bottom">
<template v-if="currentTabKey === 'new'"> <!-- New Patient Tab -->
<text class="record-text">
{{ resolveRecentAddTime(patient) }} {{ resolveRecentAddMeta(patient) }}
</text>
</template>
<template v-else>
<text v-if="patient.record" class="record-text record-ellipsis">
{{ patient.record.type }} / {{ patient.record.date }} / {{ patient.record.diagnosis }}
</text>
<text v-else class="no-record">暂无病历记录</text>
</template>
</view>
</view>
</view>
</view>
<!-- Bottom padding for tabbar -->
<view style="height: 200rpx;"></view> <!-- Increased padding -->
</scroll-view>
<!-- Sidebar Index -->
<view v-if="!isBatchMode" class="sidebar-index">
<view v-for="letter in indexList" :key="letter" class="index-item" @click="scrollToLetter(letter)">
{{ letter }}
</view>
</view>
</view>
<!-- Batch Actions Footer -->
<view v-if="isBatchMode" class="batch-footer">
<view class="left-action" @click="handleSelectAll">
<uni-icons
:type="selectedItems.length > 0 && selectedItems.length === patientList.flatMap(g => g.data).length ? 'checkbox-filled' : 'checkbox'"
size="24" color="#666"></uni-icons>
<text class="footer-text">全选 ({{ selectedItems.length }})</text>
</view>
<view class="right-actions">
<button class="footer-btn plain" @click="cancelBatch">取消</button>
<button class="footer-btn primary" @click="handleTransfer">转移</button>
<button class="footer-btn primary" @click="handleShare">共享</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import dayjs from 'dayjs';
import api from '@/utils/api';
import useAccountStore from '@/store/account';
import useInfoCheck from '@/hooks/useInfoCheck';
import { confirm as uniConfirm, hideLoading, loading as showLoading, toast } from '@/utils/widget';
// State
const teams = ref([]);
const currentTeam = ref(null);
const currentTabKey = ref('all');
const scrollIntoId = ref('');
const teamGroups = ref([]);
const tabs = computed(() => {
const base = [
{ key: 'all', label: '全部', kind: 'all' },
{ key: 'new', label: '新患者', kind: 'new' },
];
const groupTabs = (Array.isArray(teamGroups.value) ? teamGroups.value : [])
.filter((g) => g && g._id && g.groupName)
.map((g) => ({ key: `group:${g._id}`, label: String(g.groupName), kind: 'group', groupId: String(g._id) }));
return [...base, ...groupTabs];
});
const isBatchMode = ref(false);
const selectedItems = ref([]); // Stores patient phone or unique ID
const groupNameMap = computed(() => {
const map = new Map();
(Array.isArray(teamGroups.value) ? teamGroups.value : []).forEach((g) => {
const id = g && g._id ? String(g._id) : '';
const name = g && g.groupName ? String(g.groupName) : '';
if (id && name) map.set(id, name);
});
return map;
});
// Team Members Map
const userNameMap = ref({});
const loadedMembersTeamId = ref('');
const corpMemberNameInflight = new Map(); // userId -> Promise
const corpMemberNameTried = new Set(); // avoid retry storms on failures
let corpMemberBatchInflight = null; // Promise | null
// 新增流程所需状态(认证相关)
const managedArchiveCountAllTeams = ref(0); // 在管档案数(所有团队)
const verifyStatus = ref(''); // unverified | verifying | verified | failed
const isVerified = ref(false); // 是否已认证
const hasVerifyFailedHistory = ref(false); // 是否有历史认证失败
const verifyFailedReason = ref('');
const DETAIL_STORAGE_KEY = 'ykt_case_archive_detail';
const CURRENT_TEAM_STORAGE_KEY = 'ykt_case_current_team';
const NEED_RELOAD_STORAGE_KEY = 'ykt_case_need_reload';
const GROUPS_RELOAD_KEY = 'ykt_case_groups_need_reload';
const BATCH_CUSTOMER_IDS_KEY = 'ykt_case_batch_customer_ids';
const page = ref(1);
const pages = ref(0);
const pageSize = ref(50);
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 accountStore = useAccountStore();
const { account, doctorInfo } = storeToRefs(accountStore);
const { getDoctorInfo } = accountStore;
const { withInfo } = useInfoCheck();
const teamDisplay = computed(() => `${currentTeam.value?.name || ''}`);
function asArray(value) {
return Array.isArray(value) ? value : [];
}
function normalizeUserId(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'object') {
const obj = value;
const picked =
obj.userid ||
obj.userId ||
obj.userID ||
obj.corpUserId ||
obj.corpUserID ||
obj._id ||
obj.id ||
'';
return String(picked || '').trim();
}
return String(value || '').trim();
}
function resolveUserName(userId) {
const id = normalizeUserId(userId);
if (!id) return '';
// 优先使用当前登录人的信息(避免映射未命中时只显示 userid
const d = doctorInfo.value || {};
const doctorId = normalizeUserId(d.userid || d.userId || d.corpUserId || '');
if (doctorId && doctorId === id) {
const display = String(d.anotherName || d.name || d.username || d.userid || '').trim();
if (display) return display;
}
const mapped = userNameMap.value[id] || userNameMap.value[id.toLowerCase()];
if (mapped) return mapped;
// 不阻塞渲染:后台补齐非团队成员姓名(例如其他团队成员创建)
if (isLikelyUserId(id)) void fetchCorpMemberDisplayName(id).catch(() => {});
return id;
}
function isLikelyUserId(value) {
const id = normalizeUserId(value);
if (!id) return false;
if (/[\u4e00-\u9fa5]/.test(id)) return false; // already looks like a name
if (/\s/.test(id)) return false;
return true;
}
function extractDisplayNameFromAny(payload) {
if (!payload) return '';
if (typeof payload === 'string') return payload.trim();
if (Array.isArray(payload)) return extractDisplayNameFromAny(payload[0]);
if (typeof payload !== 'object') return '';
const obj = payload;
const candidate =
obj.anotherName ||
obj.name ||
obj.realName ||
obj.username ||
obj.nickName ||
obj.nickname ||
obj.displayName ||
obj.userName ||
obj.userid ||
obj.userId ||
obj.corpUserId ||
'';
return String(candidate || '').trim();
}
function extractDisplayNameFromCorpMember(row) {
const m = row && typeof row === 'object' ? row : {};
const name = String(m.anotherName || m.name || '').trim();
return name;
}
async function batchFetchCorpMemberDisplayNames(userIds) {
const ids = Array.isArray(userIds) ? userIds.map(normalizeUserId).filter(Boolean) : [];
if (!ids.length) return;
const uniq = Array.from(new Set(ids));
const unresolved = uniq.filter((id) => {
if (!isLikelyUserId(id)) return false;
const cached = userNameMap.value?.[id] || userNameMap.value?.[id.toLowerCase()];
return !cached || cached === id;
});
if (!unresolved.length) return;
if (corpMemberBatchInflight) return corpMemberBatchInflight;
const corpId = getCorpId() || String(account.value?.corpId || doctorInfo.value?.corpId || '');
if (!corpId) return;
corpMemberBatchInflight = (async () => {
try {
const res = await api(
'getCorpMember',
{
page: 1,
pageSize: Math.min(Math.max(unresolved.length, 10), 500),
params: {
corpId,
memberList: unresolved,
},
},
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 next = { ...(userNameMap.value || {}) };
rows.forEach((m) => {
const id = normalizeUserId(m?.userid || m?.userId || m?.corpUserId || '');
if (!id) return;
const display = extractDisplayNameFromCorpMember(m) || id;
const existing = next[id] || next[id.toLowerCase()];
// 仅补缺:不覆盖已有的非空/非同值映射
if (existing && existing !== id) return;
if (display && display !== id) {
next[id] = display;
next[id.toLowerCase()] = display;
}
});
userNameMap.value = next;
} catch {
// ignore
}
})().finally(() => {
corpMemberBatchInflight = null;
});
return corpMemberBatchInflight;
}
async function fetchCorpMemberDisplayName(userId) {
const id = normalizeUserId(userId);
if (!id) return '';
if (!isLikelyUserId(id)) return '';
const cached = userNameMap.value[id] || userNameMap.value[id.toLowerCase()];
if (cached && cached !== id) return cached;
if (corpMemberNameInflight.has(id)) return corpMemberNameInflight.get(id);
if (corpMemberNameTried.has(id)) return '';
const corpId = getCorpId() || String(account.value?.corpId || doctorInfo.value?.corpId || '');
if (!corpId) return '';
const p = (async () => {
corpMemberNameTried.add(id);
// 1) 首选:企业成员主页信息(更可能支持用 userid 查询)
try {
const res = await api('getCorpMemberHomepageInfo', { corpId, corpUserId: id }, false);
if (res?.success) {
const name =
extractDisplayNameFromAny(res?.data) ||
extractDisplayNameFromAny(res?.data?.data) ||
extractDisplayNameFromAny(res?.data?.member) ||
'';
if (name) return name;
}
} catch {
// ignore
}
// 1.1) 有的后端参数名为 userId
try {
const res = await api('getCorpMemberHomepageInfo', { corpId, userId: id }, false);
if (res?.success) {
const name =
extractDisplayNameFromAny(res?.data) ||
extractDisplayNameFromAny(res?.data?.data) ||
extractDisplayNameFromAny(res?.data?.member) ||
'';
if (name) return name;
}
} catch {
// ignore
}
// 2) 兜底:成员数据接口(部分环境可能支持 corpUserId
try {
const res = await api('getCorpMemberData', { corpId, corpUserId: id }, false);
if (res?.success) {
const name =
extractDisplayNameFromAny(res?.data) ||
extractDisplayNameFromAny(res?.data?.data) ||
'';
if (name) return name;
}
} catch {
// ignore
}
// 2.1) 同样尝试 userId
try {
const res = await api('getCorpMemberData', { corpId, userId: id }, false);
if (res?.success) {
const name =
extractDisplayNameFromAny(res?.data) ||
extractDisplayNameFromAny(res?.data?.data) ||
'';
if (name) return name;
}
} catch {
// ignore
}
return '';
})()
.then((name) => {
const display = String(name || '').trim();
if (display) {
const next = { ...(userNameMap.value || {}) };
next[id] = display;
next[id.toLowerCase()] = display;
userNameMap.value = next;
}
return display;
})
.finally(() => {
corpMemberNameInflight.delete(id);
});
corpMemberNameInflight.set(id, p);
return p;
}
async function prefetchUserNamesFromPatients(patients) {
const list = Array.isArray(patients) ? patients : [];
if (!list.length) return;
const ids = new Set();
list.forEach((p) => {
const u1 = normalizeUserId(p?.recentAddOperatorUserId);
const u2 = normalizeUserId(p?.creator);
if (u1) ids.add(u1);
if (u2) ids.add(u2);
});
const targets = Array.from(ids).filter((id) => {
if (!isLikelyUserId(id)) return false;
const cached = userNameMap.value[id] || userNameMap.value[id.toLowerCase()];
return !cached || cached === id;
});
if (!targets.length) return;
// 优先批量补齐(现成后端接口 getCorpMember 支持 memberList
await batchFetchCorpMemberDisplayNames(targets);
const limit = 6;
let idx = 0;
const workers = Array.from({ length: Math.min(limit, targets.length) }, async () => {
while (idx < targets.length) {
const cur = targets[idx++];
try {
const cached = userNameMap.value?.[cur] || userNameMap.value?.[cur.toLowerCase()];
if (cached && cached !== cur) continue;
await fetchCorpMemberDisplayName(cur);
} catch {
// ignore
}
}
});
await Promise.allSettled(workers);
}
function normalizeTeam(raw) {
if (!raw || typeof raw !== 'object') return null;
const teamId = raw.teamId || raw.id || raw._id || '';
const name = raw.name || raw.teamName || raw.team || '';
const corpId = raw.corpId || raw.corpID || '';
const userId = raw.userId || raw.userid || raw.corpUserId || '';
if (!teamId || !name) return null;
return { teamId: String(teamId), name: String(name), corpId: corpId ? String(corpId) : '', userId: userId ? String(userId) : '' };
}
function getUserId() {
const d = doctorInfo.value || {};
const a = account.value || {};
return String(d.userid || d.userId || d.corpUserId || a.userid || a.userId || '') || '';
}
function getCorpId() {
const t = currentTeam.value || {};
const a = account.value || {};
return String(t.corpId || a.corpId || '') || '';
}
function getTeamId() {
return String(currentTeam.value?.teamId || '') || '';
}
function hasCompleteUserInfo() {
const userId = getUserId();
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
const anotherName = String(doctorInfo.value?.anotherName || '').trim();
return Boolean(corpId && userId && anotherName);
}
function ensureUserInfoForFeature() {
if (hasCompleteUserInfo()) return true;
toast('请先完善个人信息再使用该功能');
return false;
}
async function ensureDoctorForQuery() {
if (account.value?.openid) {
try {
const a = account.value || {};
const accountId = normalizeUserId(a.userid || a.userId || a.corpUserId || '');
const d = doctorInfo.value || {};
const doctorId = normalizeUserId(d.userid || d.userId || d.corpUserId || '');
// doctorInfo 可能是旧缓存:当与当前账号不一致时强制刷新
if (!doctorId || (accountId && doctorId && accountId !== doctorId)) {
await getDoctorInfo();
}
} catch {
// ignore
}
}
return Boolean(getUserId());
}
async function loadTeamMembers() {
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId) return;
if (loadedMembersTeamId.value === teamId && Object.keys(userNameMap.value || {}).length > 0) return;
try {
const nextMap = { ...(userNameMap.value || {}) };
// 以团队详情为准getTeamMemberAvatarsAndName 存在不全/不准的情况)
const res = await api('getTeamData', { corpId, teamId });
if (res?.success) {
const t = res?.data && typeof res.data === 'object' ? res.data : {};
const members = Array.isArray(t.memberList) ? t.memberList : [];
members.forEach((m) => {
if (typeof m === 'string') {
const k = normalizeUserId(m);
if (k) {
nextMap[k] = nextMap[k] || k;
nextMap[k.toLowerCase()] = nextMap[k.toLowerCase()] || nextMap[k] || k;
}
return;
}
const display = String(m?.anotherName || m?.name || m?.userid || m?.userId || m?.corpUserId || '').trim();
const keys = [m?.userid, m?.userId, m?.corpUserId].map(normalizeUserId).filter(Boolean);
keys.forEach((k) => {
nextMap[k] = display || nextMap[k] || k;
nextMap[String(k).toLowerCase()] = display || nextMap[String(k).toLowerCase()] || k;
});
});
}
// 补缺:仅当当前没有映射时才用 avatars 接口补齐,避免覆盖正确姓名
try {
const avatarRes = await api('getTeamMemberAvatarsAndName', { corpId, teamId });
const mapObj = avatarRes?.success && avatarRes?.data && typeof avatarRes.data === 'object' ? avatarRes.data : {};
Object.entries(mapObj).forEach(([uid, info]) => {
const k = normalizeUserId(uid);
if (!k) return;
const existing = nextMap[k] || nextMap[k.toLowerCase()];
if (existing && existing !== k) return;
const display = String(info?.name || info?.anotherName || info?.userid || '').trim();
if (!display || display === k) return;
nextMap[k] = display;
nextMap[k.toLowerCase()] = display;
});
} catch {
// ignore
}
userNameMap.value = nextMap;
loadedMembersTeamId.value = teamId;
} catch (e) {
console.error('获取团队成员失败', e);
}
}
function resolveCreatorName(patient) {
const val = patient.creator;
if (!val) return '';
return resolveUserName(val);
}
function resolveRecentAddTime(patient) {
return patient?.recentAddTime || patient?.createTime || '-';
}
function resolveRecentAddOperatorName(patient) {
const nameFromApi = String(patient?.recentAddOperatorName || '').trim();
// 后端部分场景会把 userid 填到 name 字段里,此时仍需通过 userid 解析姓名
if (nameFromApi && !isLikelyUserId(nameFromApi)) return nameFromApi;
const uid = patient?.recentAddOperatorUserId || nameFromApi || patient?.creator || '';
if (!uid) return '';
return resolveUserName(uid);
}
function resolveRecentAddAction(patient) {
const t = String(patient?.recentAddType || '').trim();
if (!t || t === 'create') return '创建';
if (t === 'share') return '共享';
if (t.startsWith('transfer')) return '转移';
return '创建';
}
function resolveRecentAddMeta(patient) {
const name = resolveRecentAddOperatorName(patient);
const action = resolveRecentAddAction(patient);
if (name) return `${name}${action}`;
if (action === '创建') return '患者创建';
return '-';
}
function applyVerifyStatus(status, reason) {
verifyStatus.value = status || '';
isVerified.value = verifyStatus.value === 'verified';
hasVerifyFailedHistory.value = verifyStatus.value === 'failed';
verifyFailedReason.value = hasVerifyFailedHistory.value ? (reason || '') : '';
}
async function refreshVerifyStatus() {
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || getCorpId() || '');
const weChatOpenId = String(account.value?.openid || account.value?.openId || '');
const id = String(doctorInfo.value?._id || doctorInfo.value?.id || '');
if (!corpId || !weChatOpenId || !id) {
applyVerifyStatus(String(doctorInfo.value?.verifyStatus || ''), '');
return;
}
const res = await api('getMemberVerifyStatus', { corpId, weChatOpenId, id });
if (res && res.success) {
applyVerifyStatus(String(res.data?.verifyStatus || ''), String(res.data?.reason || ''));
return;
}
applyVerifyStatus(String(doctorInfo.value?.verifyStatus || ''), '');
}
function sortGroupList(list) {
const { orderList, corpList, restList } = (Array.isArray(list) ? list : []).reduce(
(p, c) => {
if (typeof c?.sortOrder === 'number') p.orderList.push(c);
else if (c?.parentGroupId) p.corpList.push(c);
else p.restList.push(c);
return p;
},
{ orderList: [], corpList: [], restList: [] }
);
orderList.sort((a, b) => a.sortOrder - b.sortOrder);
return [...orderList, ...corpList, ...restList];
}
async function loadGroups() {
if (!currentTeam.value) return;
const corpId = getCorpId();
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 });
if (!res?.success) {
teamGroups.value = [];
return;
}
const list = Array.isArray(res.data) ? res.data : Array.isArray(res.data?.data) ? res.data.data : [];
teamGroups.value = sortGroupList(list);
// 当前 tab 如果是分组,但分组已不存在,则回退到“全部”
if (currentTabKey.value.startsWith('group:')) {
const gid = currentTabKey.value.slice('group:'.length);
if (!teamGroups.value.some((g) => String(g._id) === String(gid))) currentTabKey.value = 'all';
}
}
function getLetter(patient) {
const raw = patient?.firstLetter || patient?.nameFirstLetter || patient?.pinyinFirstLetter || patient?.letter || '';
const candidate = String(raw || '').trim();
if (candidate && /^[A-Za-z]$/.test(candidate)) return candidate.toUpperCase();
const name = String(patient?.name || '').trim();
const first = name ? name[0] : '';
if (/^[A-Za-z]$/.test(first)) return first.toUpperCase();
return '#';
}
function letterToDomId(letter) {
return `letter-${letter === '#' ? 'HASH' : letter}`;
}
function getSelectId(patient) {
return patient?._id || patient?.id || patient?.phone || patient?.mobile || '';
}
function getPatientGroupIds(patient) {
const raw = patient?.groupIds ?? patient?.groupIdList ?? patient?.groupId ?? patient?.groups;
if (Array.isArray(raw)) return raw.map(String).filter(Boolean);
if (typeof raw === 'string' || typeof raw === 'number') return [String(raw)].filter(Boolean);
return [];
}
function resolveGroupTags(patient) {
const ids = getPatientGroupIds(patient);
if (!ids.length) return [];
const map = groupNameMap.value;
return ids.map((id) => map.get(String(id))).filter(Boolean);
}
function parseCreateTime(value) {
if (!value) return null;
if (typeof value === 'number') return dayjs(value);
if (typeof value === 'string' && /^\d{10,13}$/.test(value)) {
const n = Number(value);
const ms = value.length === 10 ? n * 1000 : n;
return dayjs(ms);
}
const asString = String(value);
const d1 = dayjs(asString);
if (d1.isValid()) return d1;
const normalized = asString.replace(/\./g, '-').replace(/\//g, '-');
const d2 = dayjs(normalized);
return d2.isValid() ? d2 : null;
}
function normalizeMedicalType(raw) {
const s = String(raw || '').trim();
if (!s) return '';
const lower = s.toLowerCase();
// 常见后缀xxxRecord后端/历史数据可能返回此形式)
if (lower.endsWith('record') && s.length > 6) {
return normalizeMedicalType(s.slice(0, -6));
}
// 中文兜底(部分接口返回展示名)
if (s.includes('门诊')) return 'outpatient';
if (s.includes('住院') || s.includes('入院')) return 'inhospital';
if (s.includes('预问诊') || s.includes('问诊')) return 'preConsultation';
if (s.includes('体检')) return 'physicalExaminationTemplate';
if (lower.includes('preconsult') || (lower.includes('pre') && lower.includes('consult'))) return 'preConsultation';
if (lower === 'outpatient' || lower === 'out_patient' || lower === 'out-patient') return 'outpatient';
if (lower === 'inhospital' || lower === 'in_hospital' || lower === 'in-hospital' || lower === 'inpatient') return 'inhospital';
if (lower === 'physicalexaminationtemplate' || lower === 'physicalexamination' || lower === 'physical_examination') return 'physicalExaminationTemplate';
if (s === 'outPatient') return 'outpatient';
if (s === 'inHospital') return 'inhospital';
if (s === 'preConsultation') return 'preConsultation';
if (s === 'physicalExaminationTemplate') return 'physicalExaminationTemplate';
return s;
}
function normalizeText(v) {
if (Array.isArray(v)) {
const parts = v
.map((i) => normalizeText(i))
.filter((i) => i !== null && i !== undefined && String(i).trim());
return parts.join('');
}
if (v === 0) return '0';
if (v && typeof v === 'object') {
const o = v;
const candidate = o.label ?? o.name ?? o.text ?? o.title ?? o.value ?? o.diseaseName ?? o.code ?? '';
return candidate ? String(candidate) : '';
}
return v ? String(v) : '';
}
function formatAnyDate(value, fmt = 'YYYY-MM-DD') {
const d = parseCreateTime(value);
return d ? d.format(fmt) : '';
}
function resolveLatestRecord(lr) {
if (!lr || typeof lr !== 'object') return null;
const formData = lr?.formData && typeof lr.formData === 'object' ? lr.formData : null;
const recordData = lr?.recordData && typeof lr.recordData === 'object' ? lr.recordData : null;
const data = lr?.data && typeof lr.data === 'object' ? lr.data : null;
const hasValue = (v) => {
if (v === 0) return true;
if (Array.isArray(v)) return v.length > 0;
if (v && typeof v === 'object') return Object.keys(v).length > 0;
return v !== null && v !== undefined && String(v).trim() !== '';
};
const pick = (...keys) => {
for (const k of keys) {
const v0 = lr?.[k];
if (hasValue(v0)) return v0;
const v1 = formData?.[k];
if (hasValue(v1)) return v1;
const v2 = recordData?.[k];
if (hasValue(v2)) return v2;
const v3 = data?.[k];
if (hasValue(v3)) return v3;
}
return undefined;
};
const rawType = String(pick('medicalType', 'templateType', 'type') || '').trim();
const uiType = normalizeMedicalType(rawType);
// 注意:不要用 type 字段兜底“名称”,很多场景 type 是类型码(如 preConsultation / physicalExaminationTemplate
const typeLabelRaw = String(
pick(
'tempName',
'templateName',
'templateTitle',
'tempTitle',
'recordName',
'medicalName',
'medicalRecordName',
'consultName',
'preConsultationName',
'inspectName',
'physicalName',
'name'
) || ''
).trim();
const typeLabel =
typeLabelRaw && normalizeMedicalType(typeLabelRaw) === uiType ? '' : typeLabelRaw;
const rawDate = pick(
'date',
'visitTime',
'inhosDate',
'consultDate',
'inspectDate',
'sortTime',
'createTime',
'createdAt',
'updateTime',
'updatedAt',
'time'
);
const rawDateStr = String(rawDate ?? '').trim();
const date = (/^\d{10,13}$/.test(rawDateStr) ? (formatAnyDate(rawDateStr, 'YYYY-MM-DD') || rawDateStr) : rawDateStr)
|| formatAnyDate(pick('visitTime', 'inhosDate', 'consultDate', 'inspectDate', 'sortTime', 'createTime', 'updateTime'), 'YYYY-MM-DD')
|| '-';
let third = '';
// 后端若已计算出统一展示字段 diagnosis则优先使用
const directDiagnosis = normalizeText(pick('diagnosis', 'diagnosisName'));
if (String(directDiagnosis || '').trim()) {
third = directDiagnosis;
} else if (uiType === 'outpatient' || uiType === 'inhospital') {
third = normalizeText(pick(
'diagnosisName',
'diagnosis',
'diagnosisList',
'diagnosisNames',
'mainDiagnosis',
'admissionDiagnosis',
'inDiagnosis',
'outDiagnosis',
'outPatientDiagnosis',
'inHospitalDiagnosis'
));
} else if (uiType === 'preConsultation') {
third = normalizeText(
pick(
'diagnosis',
'diagnosisName',
'chiefComplaint',
'chiefComplain',
'chiefComplaintText',
'chiefComplaintContent',
'complaint',
'complaintDesc',
'complaintText',
'mainComplaint',
'mainSuit',
'mainSuitText',
'mainSuitContent',
'chief',
'zs',
'zhuSu',
'cc',
'presentIllness',
'historyOfPresentIllness',
'currentIllness'
)
);
} else if (uiType === 'physicalExaminationTemplate') {
third = normalizeText(
pick(
'diagnosis',
'diagnosisName',
'summary',
'summaryText',
'inspectSummary',
'checkSummary',
'examSummary',
'physicalSummary',
'briefSummary',
'resultSummary',
'conclusion',
'conclusionText',
'inspectConclusion',
'inspectResult',
'finalConclusion',
'finalSummary',
'reportConclusion',
'reportSummary'
)
);
} else {
third = normalizeText(pick('diagnosis', 'diagnosisName', 'summary', 'chiefComplaint'));
}
third = String(third || '').replace(/\s+/g, ' ').trim();
if (!third) {
// 最后的兜底:避免经常展示为 '-'
third = normalizeText(pick('summary', 'inspectSummary', 'chiefComplaint', 'presentIllness', 'treatmentPlan', 'abstract', 'brief'));
third = String(third || '').replace(/\s+/g, ' ').trim();
}
if (!String(third || '').trim()) third = '-';
const type = typeLabel || (uiType === 'outpatient' ? '门诊记录'
: uiType === 'inhospital' ? '住院记录'
: uiType === 'preConsultation' ? '预问诊记录'
: uiType === 'physicalExaminationTemplate' ? '体检档案'
: '-');
return { type, date, diagnosis: third };
}
function formatPatient(raw) {
const name = raw?.name || raw?.customerName || '';
const sex = raw?.sex || raw?.gender || '';
const age = raw?.age ?? '';
const mobiles = asArray(raw?.mobiles).map(String).filter(Boolean);
const mobile = raw?.mobile ? String(raw.mobile) : (mobiles[0] || '');
const createTime = parseCreateTime(raw?.createTime);
const createTimeStr = createTime ? createTime.format('YYYY-MM-DD HH:mm') : '';
const createTimeTs = createTime ? createTime.valueOf() : 0;
// 最近一次“新增到当前团队”的时间(后端计算:创建/转移/共享),没有则退化为 createTime
const recentAddTimeRaw = raw?.recentAddTime ?? raw?.recentAddAt ?? raw?.recentTime;
const recentAddTime = parseCreateTime(recentAddTimeRaw) || createTime;
const recentAddTimeStr = recentAddTime ? recentAddTime.format('YYYY-MM-DD HH:mm') : '';
const recentAddTimeTs = recentAddTime ? recentAddTime.valueOf() : 0;
const recentAddType = String(raw?.recentAddType || (recentAddTimeRaw ? '' : 'create') || '');
const recentAddOperatorUserId = String(raw?.recentAddOperatorUserId || raw?.recentAddOperator || raw?.creator || '');
// 优先使用后端返回的 tagNames标签名称数组
const rawTagNames = asArray(raw?.tagNames).filter((i) => typeof i === 'string' && i.trim());
// 其次使用 tags如果是字符串数组
const rawTags = asArray(raw?.tags).filter((i) => typeof i === 'string' && i.trim());
// 最后才使用 tagIds仅作为兜底不推荐显示
const tagIds = asArray(raw?.tagIds).map(String).filter(Boolean);
// 解析标签:优先 tagNames > tags字符串 > tagIds
const displayTags = rawTagNames.length ? rawTagNames : (rawTags.length ? rawTags : []);
// 解析病历信息
let record = null;
const latestRaw =
raw?.latestRecord ??
raw?.latestMedicalRecord ??
raw?.latestMedicalCase ??
raw?.lastRecord ??
raw?.lastMedicalRecord ??
raw?.recentRecord ??
null;
const latest = Array.isArray(latestRaw) ? latestRaw[0] : latestRaw;
if (latest && typeof latest === 'object') record = resolveLatestRecord(latest);
return {
...raw,
_id: raw?._id || raw?.id || '',
name: String(name || ''),
gender: String(sex || ''),
age,
tags: displayTags,
mobiles,
mobile,
createTime: createTimeStr,
createTimeTs,
recentAddTime: recentAddTimeStr,
recentAddTimeTs,
recentAddType,
recentAddOperatorUserId,
recentAddOperatorName: String(
raw?.recentAddOperatorName
|| raw?.recentAddOperatorAnotherName
|| raw?.recentAddOperatorUserName
|| raw?.recentAddOperatorRealName
|| raw?.recentAddOperatorDisplayName
|| ''
).trim(),
creator: raw?.creatorName || raw?.creator || '',
hospitalId: raw?.customerNumber || raw?.hospitalId || '',
record,
createdByDoctor: raw?.addMethod ? String(raw.addMethod) === 'manual' : Boolean(raw?.createdByDoctor),
hasBindWechat: Boolean(raw?.externalUserId || raw?.unionid || raw?.hasBindWechat),
};
}
function groupByLetter(list) {
const map = new Map();
list.forEach((item) => {
const letter = getLetter(item);
const arr = map.get(letter) || [];
arr.push(item);
map.set(letter, arr);
});
const letters = Array.from(map.keys()).sort((a, b) => {
if (a === '#') return 1;
if (b === '#') return -1;
return a.localeCompare(b);
});
return letters.map((letter) => ({ letter, data: map.get(letter) || [] }));
}
async function loadTeams() {
if (!doctorInfo.value && account.value?.openid) {
try {
await getDoctorInfo();
} catch (e) {
// ignore
}
}
const userId = getUserId();
const corpId = String(account.value?.corpId || doctorInfo.value?.corpId || '') || '';
if (!corpId || !userId) {
toast('缺少用户信息,请先完善个人信息');
return;
}
const res = await api('getTeamBymember', { corpId, corpUserId: userId });
if (!res?.success) {
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;
const saved = uni.getStorageSync(CURRENT_TEAM_STORAGE_KEY);
const savedTeamId = saved?.teamId ? String(saved.teamId) : '';
currentTeam.value = normalized.find((t) => savedTeamId && t.teamId === savedTeamId) || normalized[0] || null;
if (currentTeam.value) uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, currentTeam.value);
}
async function reload(reset = true) {
if (!currentTeam.value) return;
if (loading.value) return;
await ensureDoctorForQuery();
const userId = getUserId();
const corpId = getCorpId();
const teamId = getTeamId();
if (!corpId || !teamId || !userId) {
toast('缺少用户/团队信息,请先完成登录与个人信息');
return;
}
if (reset) {
page.value = 1;
rawPatients.value = [];
pages.value = 0;
totalFromApi.value = 0;
}
const query = {
corpId,
userId,
teamId,
page: page.value,
pageSize: 1000, // 按首字母排序时,一次加载更多数据以显示完整的字母分组
sortByFirstLetter: true, // 按姓名首字母排序
};
if (currentTab.value.kind === 'group' && currentTab.value.groupId) {
query.groupIds = [currentTab.value.groupId];
} else if (currentTab.value.kind === 'new') {
const start = dayjs().subtract(7, 'day').startOf('day').valueOf();
const end = dayjs().endOf('day').valueOf();
// “新患者”= 最近7天新增到当前团队创建 + 转移/共享(时间来自服务记录)
query.startRecentTime = start;
query.endRecentTime = end;
query.includeRecentAddTime = true;
}
loading.value = true;
const res = await api('searchCorpCustomerForCaseList', query);
loading.value = false;
if (!res?.success) {
toast(res?.message || '获取患者列表失败');
return;
}
const payload =
res && typeof res === 'object'
? res.data && typeof res.data === 'object' && !Array.isArray(res.data)
? res.data
: res
: {};
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];
// 补齐创建人/新增人姓名(部分创建人不在当前团队成员列表中)
void prefetchUserNamesFromPatients(next).catch(() => {});
pages.value = Number(payload.pages || 0) || 0;
totalFromApi.value = Number(payload.total || 0) || rawPatients.value.length;
managedArchiveCountAllTeams.value =
Number(
payload.totalAllTeams ||
payload.totalAllTeam ||
payload.totalAllTeamsCount ||
managedArchiveCountAllTeams.value ||
totalFromApi.value ||
0
) || (totalFromApi.value || 0);
}
const handlePatientClick = (patient) => {
if (isBatchMode.value) {
toggleSelect(patient);
return;
}
const id = patient._id || patient.id || patient.mobile || patient.phone || '';
uni.setStorageSync(DETAIL_STORAGE_KEY, {
_id: id,
name: patient.name,
sex: patient.gender,
age: patient.age,
mobile: patient.mobile,
mobiles: Array.isArray(patient.mobiles) ? patient.mobiles : (patient.mobile ? [patient.mobile] : []),
outpatientNo: patient.outpatientNo,
inpatientNo: patient.inpatientNo,
medicalRecordNo: patient.medicalRecordNo,
createTime: patient.createTime,
creator: patient.creator,
createdByDoctor: patient.createdByDoctor,
hasBindWechat: patient.hasBindWechat
});
uni.navigateTo({ url: `/pages/case/archive-detail?id=${encodeURIComponent(String(id))}` });
};
// Computed
const patientList = computed(() => {
const all = rawPatients.value || [];
// New Patient Filter (Last 7 days)
if (currentTab.value.kind === 'new') {
const sevenDaysAgo = dayjs().subtract(7, 'day').startOf('day').valueOf();
const flatList = all
.filter((p) => Number(p?.recentAddTimeTs || p?.createTimeTs || 0) >= sevenDaysAgo)
.slice()
.sort(
(a, b) =>
Number(b?.recentAddTimeTs || b?.createTimeTs || 0) -
Number(a?.recentAddTimeTs || a?.createTimeTs || 0)
);
return [{ letter: '最近7天新增', data: flatList }];
}
return groupByLetter(all);
});
const indexList = computed(() => {
if (currentTab.value.kind === 'new') return []; // No index bar for new patient
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ#'.split('').filter(l => patientList.value.some(g => g.letter === l));
return letters;
});
const totalPatients = computed(() => {
let count = 0;
patientList.value.forEach(g => count += g.data.length);
if (currentTab.value.kind === 'new') return count;
return totalFromApi.value || count;
});
// Methods
const checkBatchMode = () => {
if (isBatchMode.value) {
uni.showToast({ title: '请先完成当前批量设置或点击底部“取消”按钮退出', icon: 'none' });
return true;
}
return false;
};
const scrollToLetter = (letter) => {
if (currentTab.value.kind === 'new') return;
scrollIntoId.value = letterToDomId(letter);
};
const toggleTeamPopup = () => {
if (checkBatchMode()) return;
if (!teams.value.length) {
toast('暂无可选团队');
return;
}
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);
}
});
};
const goToSearch = () => {
if (checkBatchMode()) return;
uni.navigateTo({
url: '/pages/case/search'
});
};
const goToGroupManage = withInfo(() => {
if (checkBatchMode()) return;
uni.navigateTo({
url: '/pages/case/group-manage'
});
});
const toggleBatchMode = withInfo(() => {
if (isBatchMode.value) {
// Already in batch mode, do nothing or prompt?
// Prompt says "click Other operations... prompt please finish".
// Clicking "Batch" itself while in batch mode: user usually expects toggle off or nothing.
// Based on "click Cancel button to exit", I'll assume clicking existing batch button doesn't exit.
return;
}
isBatchMode.value = true;
selectedItems.value = [];
});
const handleCreate = withInfo(() => {
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上限提示去认证
if (!isVerified.value && managedArchiveCountAllTeams.value >= 10) {
if (verifyStatus.value === 'verifying') {
toast('信息认证中,请耐心等待!');
return;
}
uni.showModal({
title: '提示',
content: '当前管理档案数已达上限 10 个,完成认证即可升级至 100 个。',
cancelText: '暂不认证',
confirmText: '去认证',
success: (res) => {
if (res.confirm) {
startVerifyFlow();
}
}
});
return;
}
}
// 未达上限:显示新增入口
uni.showActionSheet({
itemList: ['邀请患者建档', '我帮患者建档'],
success: (res) => {
if (res.tapIndex === 0) {
openInvitePatientEntry();
} else if (res.tapIndex === 1) {
openCreatePatientEntry();
}
}
});
});
// 新增流程:认证分支
const startVerifyFlow = () => {
if (verifyStatus.value === 'verifying') {
toast('信息认证中,请耐心等待!');
return;
}
// 有历史失败记录 -> 展示失败原因 & 重新认证
if (hasVerifyFailedHistory.value) {
uni.showModal({
title: '提示',
content: `您有历史认证未通过记录。失败原因为:\n\n${verifyFailedReason.value}`,
cancelText: '取消',
confirmText: '重新认证',
success: (res) => {
if (res.confirm) {
openVerifyEntry();
}
}
});
return;
}
// 正常去认证
openVerifyEntry();
};
// ===== 预留入口(后续对接真实页面/接口) =====
const openVerifyEntry = () => {
uni.navigateTo({ url: '/pages/work/profile?type=cert' });
};
const openAddCustomerServiceEntry = () => {
uni.navigateTo({ url: '/pages/work/service/contact-service' });
};
const openInvitePatientEntry = () => {
uni.navigateTo({ url: '/pages/work/team/invite/invite-patient' });
};
const openCreatePatientEntry = () => {
uni.navigateTo({ url: '/pages/case/patient-create' });
};
// Batch Operations
const toggleSelect = (patient) => {
if (!isBatchMode.value) return; // Should not happen if click handler is correct
const id = patient._id || patient.id || patient.phone || patient.mobile; // Prefer server id
const index = selectedItems.value.indexOf(id);
if (index > -1) {
selectedItems.value.splice(index, 1);
} else {
selectedItems.value.push(id);
}
};
const handleSelectAll = () => {
// Flatten current list
const currentList = patientList.value.flatMap(group => group.data);
if (selectedItems.value.length === currentList.length) {
selectedItems.value = []; // Unselect All
} else {
selectedItems.value = currentList.map(p => p._id || p.id || p.phone || p.mobile);
}
};
const cancelBatch = () => {
isBatchMode.value = false;
selectedItems.value = [];
};
function showActionSheet(itemList = [], title = '') {
return new Promise((resolve, reject) => {
if (!Array.isArray(itemList) || itemList.length === 0) {
reject(new Error('empty'));
return;
}
uni.showActionSheet({
title,
itemList,
success: ({ tapIndex }) => resolve(tapIndex),
fail: () => reject(new Error('cancel')),
});
});
}
async function fetchTeamMembersByTeamId(teamId) {
const corpId = getCorpId();
if (!corpId || !teamId) return [];
const res = await api('getTeamData', { corpId, teamId });
if (!res?.success) return [];
const t = res?.data && typeof res.data === 'object' ? res.data : {};
return Array.isArray(t.memberList) ? t.memberList : [];
}
async function transferToCustomerPool(customerIds) {
try {
await uniConfirm('客户将与本团队解除服务关系,本团队成员将没有权限查询到客户档案。');
} catch (e) {
return;
}
const corpId = getCorpId();
const currentTeamId = getTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
showLoading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds,
currentTeamId,
operationType: 'transferToCustomerPool',
creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return;
}
toast('操作成功');
cancelBatch();
await reload(true);
} catch (e) {
toast('操作失败');
} finally {
hideLoading();
}
}
async function transferToOtherTeam(customerIds) {
const corpId = getCorpId();
const currentTeamId = getTeamId();
const creatorUserId = getUserId();
if (!corpId || !currentTeamId || !creatorUserId) return toast('缺少用户/团队信息');
if (!teams.value.length) await loadTeams();
const candidates = teams.value.filter((t) => String(t?.teamId || '') !== String(currentTeamId));
if (!candidates.length) return toast('暂无可选团队');
let teamIndex;
try {
teamIndex = await showActionSheet(candidates.map((t) => t.name || ''), '选择新负责团队');
} catch (e) {
return;
}
const pickedTeam = candidates[teamIndex];
if (!pickedTeam?.teamId) return toast('团队信息异常');
let members = [];
try {
members = await fetchTeamMembersByTeamId(pickedTeam.teamId);
} catch (e) {
members = [];
}
if (!members.length) return toast('当前团队暂无可选成员');
const labels = members.map((m) => String(m?.anotherName || m?.name || m?.userid || ''));
let userIndex;
try {
userIndex = await showActionSheet(labels, '选择责任人');
} catch (e) {
return;
}
const pickedUser = members[userIndex] || null;
const targetUserId = String(pickedUser?.userid || '') || '';
if (!targetUserId) return toast('责任人信息异常');
showLoading('保存中...');
try {
const res = await api('transferCustomers', {
corpId,
customerIds,
currentTeamId,
targetTeamId: String(pickedTeam.teamId),
targetUserId,
operationType: 'transferToOtherTeam',
creatorUserId,
});
if (!res?.success) {
toast(res?.message || '操作失败');
return;
}
toast('操作成功');
cancelBatch();
await reload(true);
} catch (e) {
toast('操作失败');
} finally {
hideLoading();
}
}
const handleTransfer = async () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
let tapIndex;
try {
tapIndex = await showActionSheet(['转移给其他团队', '转移至客户公共池']);
} catch (e) {
return;
}
const customerIds = selectedItems.value.slice().map(String).filter(Boolean);
if (!customerIds.length) return toast('请选择患者');
if (tapIndex === 0) {
await transferToOtherTeam(customerIds);
} else if (tapIndex === 1) {
await transferToCustomerPool(customerIds);
}
};
const handleShare = () => {
if (selectedItems.value.length === 0) {
uni.showToast({ title: '请选择患者', icon: 'none' });
return;
}
uni.setStorageSync(BATCH_CUSTOMER_IDS_KEY, selectedItems.value.slice());
// Navigate to Share Page
uni.navigateTo({ url: '/pages/case/batch-share' });
};
function loadMore() {
if (!more.value || loading.value) return;
page.value += 1;
reload(false);
}
watch(currentTeam, (t) => {
if (!t) return;
uni.setStorageSync(CURRENT_TEAM_STORAGE_KEY, t);
});
watch(currentTabKey, () => {
if (!currentTeam.value) return;
loadTeamMembers();
reload(true);
});
function onTabClick(tab) {
if (checkBatchMode()) return;
if (!tab || !tab.key) return;
if (currentTabKey.value === tab.key) return;
if (tab.kind === 'group' && !ensureUserInfoForFeature()) return;
currentTabKey.value = tab.key;
}
onLoad(async () => {
await loadTeams();
if (currentTeam.value) {
await loadGroups();
await loadTeamMembers();
await reload(true);
}
await refreshVerifyStatus();
});
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();
});
</script>
<style lang="scss" scoped>
.container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #fff;
padding-bottom: 0; // Default
// Padding for batch footer
/* &.is-batch {
padding-bottom: 100rpx;
} */
// We can't use &.is-batch because scoped style and root element is tricky depending on uni-app version/style
// Instead we handle it in content-body or separate view
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
border-bottom: 2rpx solid #f0f0f0;
.team-selector {
display: flex;
align-items: center;
font-size: 32rpx;
font-weight: bold;
color: #333;
.team-name {
margin-right: 10rpx;
}
.team-icon {
font-size: 32rpx;
color: #333;
}
}
.header-actions {
display: flex;
gap: 30rpx;
.action-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.action-text {
font-size: 20rpx;
color: #333;
margin-top: 4rpx;
}
}
}
}
.tabs-area {
display: flex;
align-items: center;
background-color: #f5f7fa;
border-bottom: 2rpx solid #eee;
padding-right: 30rpx; // Padding for the count
.tabs-scroll {
flex: 1;
white-space: nowrap;
overflow: hidden;
.tabs-container {
display: flex;
padding: 20rpx 30rpx;
.tab-item {
padding: 10rpx 30rpx;
margin-right: 20rpx;
font-size: 28rpx;
color: #666;
background-color: #fff;
border-radius: 8rpx;
flex-shrink: 0;
&.active {
color: #5d8aff;
background-color: #e6f0ff;
font-weight: bold;
}
}
}
}
.total-count-inline {
font-size: 24rpx;
color: #666;
white-space: nowrap;
flex-shrink: 0;
min-width: 70rpx;
text-align: right;
}
}
.content-body {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.patient-list {
flex: 1;
height: 100%;
background-color: #f7f8fa;
}
.group-title {
padding: 10rpx 30rpx;
font-size: 28rpx;
color: #333;
}
.patient-card {
display: flex;
background-color: #fff;
padding: 30rpx;
margin-bottom: 2rpx; // Separator line
border-bottom: 2rpx solid #f0f0f0;
.checkbox-area {
display: flex;
align-items: center;
margin-right: 20rpx;
}
.card-content {
flex: 1;
}
.card-row-top {
display: flex;
align-items: center;
margin-bottom: 16rpx;
flex-wrap: wrap;
.patient-info {
display: flex;
align-items: flex-end;
margin-right: 20rpx;
.patient-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.patient-meta {
font-size: 24rpx;
color: #999;
margin-bottom: 4rpx;
}
}
.patient-tags {
display: flex;
gap: 10rpx;
.tag {
font-size: 20rpx;
color: #5d8aff;
border: 2rpx solid #5d8aff;
padding: 0 8rpx;
border-radius: 16rpx;
height: 32rpx;
line-height: 28rpx;
}
}
}
.card-row-middle {
font-size: 28rpx;
margin-bottom: 12rpx;
.record-text {
color: #666;
}
.record-ellipsis {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.no-record {
color: #bdc3c7;
}
}
.card-row-bottom {
font-size: 28rpx;
.record-text {
color: #666;
}
.record-ellipsis {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.no-record {
color: #bdc3c7;
}
}
}
.batch-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
z-index: 99;
.left-action {
display: flex;
align-items: center;
.footer-text {
margin-left: 10rpx;
font-size: 28rpx;
color: #333;
}
}
.right-actions {
display: flex;
gap: 20rpx;
.footer-btn {
font-size: 28rpx;
padding: 0 30rpx;
height: 64rpx;
line-height: 64rpx;
margin: 0;
border-radius: 8rpx;
&.plain {
border: 2rpx solid #ddd;
background-color: #fff;
color: #666;
}
&.primary {
background-color: #5d8aff;
color: #fff;
border: none;
}
&::after {
border: none;
}
}
}
}
.sidebar-index {
width: 40rpx;
position: absolute;
right: 0;
top: 40rpx;
bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: transparent;
z-index: 10;
.index-item {
font-size: 20rpx;
color: #555;
padding: 4rpx 0;
width: 40rpx;
text-align: center;
font-weight: 500;
}
}
</style>